From ea9e6bc1b8c6ba1df3bd85b25d36afe7540298ed Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 16:48:10 -0400 Subject: [PATCH 01/34] Create/use global shared retry queue. Importing a fresh copy of graceful-fs then using `require('fs').close` would only initiate retry of the queue from the first copy of graceful-fs, so needed retries associated with subsequent instances of graceful-fs would not be executed. --- graceful-fs.js | 82 +++++++++++++++++++++++++------------------------- test/close.js | 22 +++++++++++--- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index ac20675..bc9c0e9 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -3,10 +3,10 @@ var polyfills = require('./polyfills.js') var legacy = require('./legacy-streams.js') var clone = require('./clone.js') -var queue = [] - var util = require('util') +var gracefulQueue = Symbol.for('graceful-fs.queue') + function noop () {} var debug = noop @@ -19,11 +19,44 @@ else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) console.error(m) } -if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { - process.on('exit', function() { - debug(queue) - require('assert').equal(queue.length, 0) +// Once time initialization +if (!global[gracefulQueue]) { + // This queue can be shared by multiple loaded instances + var queue = [] + Object.defineProperty(global, gracefulQueue, { + get: function() { + return queue + } }) + + // Patch fs.close/closeSync to shared queue version, because we need + // to retry() whenever a close happens *anywhere* in the program. + // This is essential when multiple graceful-fs instances are + // in play at the same time. + fs.close = (function (fs$close) { return function (fd, cb) { + return fs$close.call(fs, fd, function (err) { + // This function uses the graceful-fs shared queue + if (!err) { + retry() + } + + if (typeof cb === 'function') + cb.apply(this, arguments) + }) + }})(fs.close) + + fs.closeSync = (function (fs$closeSync) { return function (fd) { + // This function uses the graceful-fs shared queue + fs$closeSync.apply(fs, arguments) + retry() + }})(fs.closeSync) + + if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { + process.on('exit', function() { + debug(global[gracefulQueue]) + require('assert').equal(global[gracefulQueue].length, 0) + }) + } } module.exports = patch(clone(fs)) @@ -32,39 +65,6 @@ if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { fs.__patched = true; } -// Always patch fs.close/closeSync, because we want to -// retry() whenever a close happens *anywhere* in the program. -// This is essential when multiple graceful-fs instances are -// in play at the same time. -module.exports.close = (function (fs$close) { return function (fd, cb) { - return fs$close.call(fs, fd, function (err) { - if (!err) - retry() - - if (typeof cb === 'function') - cb.apply(this, arguments) - }) -}})(fs.close) - -module.exports.closeSync = (function (fs$closeSync) { return function (fd) { - // Note that graceful-fs also retries when fs.closeSync() fails. - // Looks like a bug to me, although it's probably a harmless one. - var rval = fs$closeSync.apply(fs, arguments) - retry() - return rval -}})(fs.closeSync) - -// Only patch fs once, otherwise we'll run into a memory leak if -// graceful-fs is loaded multiple times, such as in test environments that -// reset the loaded modules between tests. -// We look for the string `graceful-fs` from the comment above. This -// way we are not adding any extra properties and it will detect if older -// versions of graceful-fs are installed. -if (!/\bgraceful-fs\b/.test(fs.closeSync.toString())) { - fs.closeSync = module.exports.closeSync; - fs.close = module.exports.close; -} - function patch (fs) { // Everything that references the open() function needs to be in here polyfills(fs) @@ -267,11 +267,11 @@ function patch (fs) { function enqueue (elem) { debug('ENQUEUE', elem[0].name, elem[1]) - queue.push(elem) + global[gracefulQueue].push(elem) } function retry () { - var elem = queue.shift() + var elem = global[gracefulQueue].shift() if (elem) { debug('RETRY', elem[0].name, elem[1]) elem[0].apply(null, elem[1]) diff --git a/test/close.js b/test/close.js index dd1f002..7210b31 100644 --- a/test/close.js +++ b/test/close.js @@ -1,10 +1,22 @@ -var fs$close = require('fs').close; -var fs$closeSync = require('fs').closeSync; -var fs = require('../'); +var fs = require('fs') +var path = require('path') +var gfsPath = path.resolve(__dirname, '..', 'graceful-fs.js') +var gfs = require(gfsPath) +var importFresh = require('import-fresh') +var fs$close = fs.close +var fs$closeSync = fs.closeSync var test = require('tap').test test('`close` is patched correctly', function(t) { - t.notEqual(fs.close, fs$close, 'patch close'); - t.notEqual(fs.closeSync, fs$closeSync, 'patch closeSync'); + t.match(fs$close.toString(), /graceful-fs shared queue/, 'patch fs.close'); + t.match(fs$closeSync.toString(), /graceful-fs shared queue/, 'patch fs.closeSync'); + t.match(gfs.close.toString(), /graceful-fs shared queue/, 'patch gfs.close'); + t.match(gfs.closeSync.toString(), /graceful-fs shared queue/, 'patch gfs.closeSync'); + + var newGFS = importFresh(gfsPath) + t.equal(fs.close, fs$close) + t.equal(fs.closeSync, fs$closeSync) + t.equal(newGFS.close, fs$close) + t.equal(newGFS.closeSync, fs$closeSync) t.end(); }) From 43ba0b14bbd1728a9a786b016d6cb8dd8cc8b92d Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Thu, 8 Aug 2019 15:44:17 -0400 Subject: [PATCH 02/34] Add test flag allowing reset of fs.close / fs.closeSync --- graceful-fs.js | 70 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index bc9c0e9..b8acc06 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -6,6 +6,7 @@ var clone = require('./clone.js') var util = require('util') var gracefulQueue = Symbol.for('graceful-fs.queue') +var gracefulResetQueue = Symbol.for('graceful-fs.reset-queue') function noop () {} @@ -20,36 +21,59 @@ else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) } // Once time initialization -if (!global[gracefulQueue]) { - // This queue can be shared by multiple loaded instances - var queue = [] - Object.defineProperty(global, gracefulQueue, { - get: function() { - return queue - } - }) +if (!global[gracefulQueue] || global[gracefulResetQueue]) { + global[gracefulResetQueue] = false + + if (!global[gracefulQueue]) { + // This queue can be shared by multiple loaded instances + var queue = [] + Object.defineProperty(global, gracefulQueue, { + get: function() { + return queue + } + }) + } + + var previous = Symbol.for('graceful-fs.previous') + if (fs.close[previous]) { + fs.close = fs.close[previous] + } + + if (fs.closeSync[previous]) { + fs.closeSync = fs.closeSync[previous] + } // Patch fs.close/closeSync to shared queue version, because we need // to retry() whenever a close happens *anywhere* in the program. // This is essential when multiple graceful-fs instances are // in play at the same time. - fs.close = (function (fs$close) { return function (fd, cb) { - return fs$close.call(fs, fd, function (err) { - // This function uses the graceful-fs shared queue - if (!err) { - retry() - } + fs.close = (function (fs$close) { + function close (fd, cb) { + return fs$close.call(fs, fd, function (err) { + // This function uses the graceful-fs shared queue + if (!err) { + retry() + } - if (typeof cb === 'function') - cb.apply(this, arguments) - }) - }})(fs.close) + if (typeof cb === 'function') + cb.apply(this, arguments) + }) + } + + close[previous] = fs$close + return close + })(fs.close) + + fs.closeSync = (function (fs$closeSync) { + function closeSync (fd) { + // This function uses the graceful-fs shared queue + fs$closeSync.apply(fs, arguments) + retry() + } - fs.closeSync = (function (fs$closeSync) { return function (fd) { - // This function uses the graceful-fs shared queue - fs$closeSync.apply(fs, arguments) - retry() - }})(fs.closeSync) + closeSync[previous] = fs$closeSync + return closeSync + })(fs.closeSync) if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { process.on('exit', function() { From a91bd18ddf2be9c0dafcd09f3ec4bbc60dfc969f Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Thu, 8 Aug 2019 16:11:46 -0400 Subject: [PATCH 03/34] Remove changes not needed in v4.x Some changes in the previous commit are not needed in v4.x which will never be tested under nyc. The only thing needed is the ability for the next version of graceful-fs to reset fs.close / fs.closeSync. --- graceful-fs.js | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index b8acc06..a644d57 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -6,7 +6,6 @@ var clone = require('./clone.js') var util = require('util') var gracefulQueue = Symbol.for('graceful-fs.queue') -var gracefulResetQueue = Symbol.for('graceful-fs.reset-queue') function noop () {} @@ -21,27 +20,17 @@ else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) } // Once time initialization -if (!global[gracefulQueue] || global[gracefulResetQueue]) { - global[gracefulResetQueue] = false - - if (!global[gracefulQueue]) { - // This queue can be shared by multiple loaded instances - var queue = [] - Object.defineProperty(global, gracefulQueue, { - get: function() { - return queue - } - }) - } +if (!global[gracefulQueue]) { + // This queue can be shared by multiple loaded instances + var queue = [] + Object.defineProperty(global, gracefulQueue, { + get: function() { + return queue + } + }) + // This is used in testing by future versions var previous = Symbol.for('graceful-fs.previous') - if (fs.close[previous]) { - fs.close = fs.close[previous] - } - - if (fs.closeSync[previous]) { - fs.closeSync = fs.closeSync[previous] - } // Patch fs.close/closeSync to shared queue version, because we need // to retry() whenever a close happens *anywhere* in the program. From 3cf751e1c638ba62a2d3046bbdbd19ad1339ad46 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 19:13:08 -0400 Subject: [PATCH 04/34] Require Node.js 8, update devDependencies --- .travis.yml | 1 - README.md | 4 +- package-lock.json | 1838 +++++++++++++++++++++++++++++++++++++++------ package.json | 11 +- 4 files changed, 1620 insertions(+), 234 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d38b47..dbbbc7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ node_js: - 12 - 10 - 8 - - 6 os: - linux diff --git a/README.md b/README.md index 5273a50..b88c9c6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ resilient to errors. * Queues up `open` and `readdir` calls, and retries them once something closes if there is an EMFILE error from too many file descriptors. -* fixes `lchmod` for Node versions prior to 0.6.2. * implements `fs.lutimes` if possible. Otherwise it becomes a noop. * ignores `EINVAL` and `EPERM` errors in `chown`, `fchown` or `lchown` if the user isn't root. @@ -79,8 +78,7 @@ also imposes the challenge of keeping in sync with the core module. The current approach loads the `fs` module, and then creates a lookalike object that has all the same methods, except a few that are -patched. It is safe to use in all versions of Node from 0.8 through -7.0. +patched. ### v4 diff --git a/package-lock.json b/package-lock.json index be5c67b..473934a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,14 @@ "lodash": "^4.17.13", "source-map": "^0.5.0", "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/helper-function-name": { @@ -72,6 +80,15 @@ "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", "dev": true }, + "@babel/runtime": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz", + "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, "@babel/template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", @@ -124,9 +141,9 @@ } }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, "ansi-styles": { @@ -138,6 +155,16 @@ "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz", + "integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "append-transform": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", @@ -183,6 +210,15 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "async-hook-domain": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-1.1.0.tgz", + "integrity": "sha512-NH7V97d1yCbIanu2oDLyPT2GFNct0esPeJyRfkk8J5hTztHVSQp4UiNfL2O42sCA9XZPU8OgHvzOmt9ewBhVqA==", + "dev": true, + "requires": { + "source-map-support": "^0.5.11" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -216,6 +252,12 @@ "tweetnacl": "^0.14.3" } }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, "bind-obj-methods": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-2.0.0.tgz", @@ -232,6 +274,15 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -254,30 +305,25 @@ "make-dir": "^2.0.0", "package-hash": "^3.0.0", "write-file-atomic": "^2.4.2" - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" + }, + "dependencies": { + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } } }, "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, "camelcase": { @@ -309,23 +355,39 @@ "supports-color": "^5.3.0" } }, - "clean-yaml-object": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz", - "integrity": "sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=", - "dev": true + "chokidar": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz", + "integrity": "sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==", + "dev": true, + "requires": { + "anymatch": "^3.0.1", + "braces": "^3.0.2", + "fsevents": "^2.0.6", + "glob-parent": "^5.0.0", + "is-binary-path": "^2.1.0", + "is-glob": "^4.0.1", + "normalize-path": "^3.0.0", + "readdirp": "^3.1.1" + } }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -399,9 +461,9 @@ "dev": true }, "coveralls": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.5.tgz", - "integrity": "sha512-/KD7PGfZv/tjKB6LoW97jzIgFqem0Tu9tZL9/iwBnBd8zkIZp7vT1ZSHNvnr0GSQMV/LTMxUstWg8WcDDUVQKg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.6.tgz", + "integrity": "sha512-Pgh4v3gCI4T/9VijVrm8Ym5v0OgjvGLKj3zTUwkvsCiwqae/p6VLzpsFNjQS2i6ewV7ef+DjFJ5TSKxYt/mCrA==", "dev": true, "requires": { "growl": "~> 1.10.0", @@ -483,15 +545,9 @@ "dev": true }, "diff": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", - "dev": true - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", "dev": true }, "ecc-jsbn": { @@ -579,6 +635,15 @@ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -599,6 +664,29 @@ "locate-path": "^3.0.0" } }, + "findit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findit/-/findit-2.0.0.tgz", + "integrity": "sha1-ZQnwEmr0wXhVHPqZOU4DLhOk1W4=", + "dev": true + }, + "flow-parser": { + "version": "0.104.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.104.0.tgz", + "integrity": "sha512-S2VGfM/qU4g9NUf2hA5qH/QmQsZIflxFO7victnYN1LR5SoOUsn3JtMhXLKHm2QlnZwwJKIdLt/uYyPr4LiQAA==", + "dev": true + }, + "flow-remove-types": { + "version": "2.104.0", + "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.104.0.tgz", + "integrity": "sha512-4M132BBfZmURXSoN24VPqUn4Q1rxymNG/G8u/l/hDKkDjzfrM3ZiQeHaCBgu8h/qekQ1QNiZp9gfMV0DURq0tA==", + "dev": true, + "requires": { + "flow-parser": "^0.104.0", + "pirates": "^3.0.2", + "vlq": "^0.2.1" + } + }, "foreground-child": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", @@ -638,6 +726,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz", + "integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==", + "dev": true, + "optional": true + }, "function-loop": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-1.0.2.tgz", @@ -673,6 +768,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -680,9 +784,9 @@ "dev": true }, "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.1.tgz", + "integrity": "sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==", "dev": true }, "growl": { @@ -701,14 +805,6 @@ "optimist": "^0.6.1", "source-map": "^0.6.1", "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "har-schema": { @@ -743,10 +839,30 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.2.tgz", + "integrity": "sha512-CyjlXII6LMsPMyUzxpTt8fzh5QwzGqPmQXgY/Jyf4Zfp27t/FvfhwoE/8laaMUcMy816CkWF20I7NeQhwwY88w==", + "dev": true, + "requires": { + "lru-cache": "^5.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + } + } }, "http-signature": { "version": "1.2.0", @@ -760,13 +876,13 @@ } }, "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", + "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", "dev": true, "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } }, "imurmurhash": { @@ -797,12 +913,42 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -872,6 +1018,34 @@ } } }, + "istanbul-lib-processinfo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-1.0.0.tgz", + "integrity": "sha512-FY0cPmWa4WoQNlvB8VOcafiRoB5nB+l2Pz2xGuXHRSy1KM8QFOYfz/rN+bGMCAeejrY3mrpF5oJHcN0s/garCg==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^6.0.5", + "istanbul-lib-coverage": "^2.0.3", + "rimraf": "^2.6.3", + "uuid": "^3.3.2" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, "istanbul-lib-report": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", @@ -905,14 +1079,6 @@ "make-dir": "^2.1.0", "rimraf": "^2.6.3", "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "istanbul-reports": { @@ -924,6 +1090,15 @@ "handlebars": "^4.1.2" } }, + "jackspeak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-1.4.0.tgz", + "integrity": "sha512-VDcSunT+wcccoG46FtzuBAyQKlzhHjli4q31e1fIHGOsRspqNUFjVzGb+7eIFDlTvqLygxapDHPHS0ouT2o/tw==", + "dev": true, + "requires": { + "cliui": "^4.1.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1075,14 +1250,6 @@ "dev": true, "requires": { "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "mime-db": { @@ -1160,6 +1327,18 @@ "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", "dev": true }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -1172,6 +1351,18 @@ "validate-npm-package-license": "^3.0.1" } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, "nyc": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", @@ -1203,14 +1394,6 @@ "uuid": "^3.3.2", "yargs": "^13.2.2", "yargs-parser": "^13.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } } }, "oauth-sign": { @@ -1301,6 +1484,15 @@ "release-zalgo": "^1.0.0" } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -1323,6 +1515,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -1352,12 +1550,27 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "pirates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-3.0.2.tgz", + "integrity": "sha512-c5CgUJq6H2k6MJz72Ak1F5sN9n9wlSlJyEnwvpm9/y3WB4E3pHBDT2c6PEiS1vyJvq2bUxUAIu0EGf8Cx4Ic7Q==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -1444,6 +1657,21 @@ } } }, + "readdirp": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.1.tgz", + "integrity": "sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -1503,9 +1731,9 @@ } }, "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, "rimraf": { @@ -1541,6 +1769,21 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -1548,9 +1791,9 @@ "dev": true }, "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, "source-map-support": { @@ -1561,14 +1804,6 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "spawn-wrap": { @@ -1647,14 +1882,13 @@ "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "strip-ansi": "^4.0.0" } }, "string_decoder": { @@ -1677,12 +1911,12 @@ } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^3.0.0" } }, "strip-bom": { @@ -1701,108 +1935,1169 @@ } }, "tap": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/tap/-/tap-12.7.0.tgz", - "integrity": "sha512-SjglJmRv0pqrQQ7d5ZBEY8ZOqv3nYDBXEX51oyycOH7piuhn82JKT/yDNewwmOsodTD/RZL9MccA96EjDgK+Eg==", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/tap/-/tap-14.6.1.tgz", + "integrity": "sha512-zxENP78NlaF4guEyNPvSs7iKWOJUSjqRYcfN+ERo42OaSyA0hDm27U4yuliQjuBJlW35vvAk9dIfocPVa0eYMw==", "dev": true, "requires": { + "async-hook-domain": "^1.1.0", "bind-obj-methods": "^2.0.0", "browser-process-hrtime": "^1.0.0", "capture-stack-trace": "^1.0.0", - "clean-yaml-object": "^0.1.0", + "chokidar": "^3.0.2", "color-support": "^1.1.0", - "coveralls": "^3.0.2", - "domain-browser": "^1.2.0", - "esm": "^3.2.5", + "coveralls": "^3.0.5", + "diff": "^4.0.1", + "esm": "^3.2.25", + "findit": "^2.0.0", + "flow-remove-types": "^2.101.0", "foreground-child": "^1.3.3", "fs-exists-cached": "^1.0.0", - "function-loop": "^1.0.1", - "glob": "^7.1.3", + "function-loop": "^1.0.2", + "glob": "^7.1.4", + "import-jsx": "^2.0.0", + "ink": "^2.3.0", "isexe": "^2.0.0", - "js-yaml": "^3.13.1", + "istanbul-lib-processinfo": "^1.0.0", + "jackspeak": "^1.4.0", "minipass": "^2.3.5", "mkdirp": "^0.5.1", - "nyc": "^14.0.0", + "nyc": "^14.1.1", "opener": "^1.5.1", - "os-homedir": "^1.0.2", "own-or": "^1.0.0", "own-or-env": "^1.0.1", + "react": "^16.8.6", "rimraf": "^2.6.3", "signal-exit": "^3.0.0", - "source-map-support": "^0.5.10", + "source-map-support": "^0.5.12", "stack-utils": "^1.0.2", - "tap-mocha-reporter": "^3.0.9", - "tap-parser": "^7.0.0", - "tmatch": "^4.0.0", + "tap-mocha-reporter": "^4.0.1", + "tap-parser": "^9.3.2", + "tap-yaml": "^1.0.0", + "tcompare": "^2.3.0", + "treport": "^0.4.0", "trivial-deferred": "^1.0.1", - "ts-node": "^8.0.2", - "tsame": "^2.0.1", - "typescript": "^3.3.3", - "write-file-atomic": "^2.4.2", + "ts-node": "^8.3.0", + "typescript": "^3.5.3", + "which": "^1.3.1", + "write-file-atomic": "^3.0.0", + "yaml": "^1.6.0", "yapool": "^1.0.0" - } - }, - "tap-mocha-reporter": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-3.0.9.tgz", - "integrity": "sha512-VO07vhC9EG27EZdOe7bWBj1ldbK+DL9TnRadOgdQmiQOVZjFpUEQuuqO7+rNSO2kfmkq5hWeluYXDWNG/ytXTQ==", - "dev": true, - "requires": { - "color-support": "^1.1.0", - "debug": "^2.1.3", - "diff": "^1.3.2", - "escape-string-regexp": "^1.0.3", - "glob": "^7.0.5", - "js-yaml": "^3.3.1", - "readable-stream": "^2.1.5", - "tap-parser": "^5.1.0", - "unicode-length": "^1.0.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "@babel/runtime": { + "version": "7.4.5", + "bundled": true, "dev": true, "requires": { - "ms": "2.0.0" + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.2", + "bundled": true, + "dev": true + } } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "@types/prop-types": { + "version": "15.7.1", + "bundled": true, "dev": true }, - "tap-parser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-5.4.0.tgz", - "integrity": "sha512-BIsIaGqv7uTQgTW1KLTMNPSEQf4zDDPgYOBRdgOfuB+JFOLRBfEu6cLa/KvMvmqggu1FKXDfitjLwsq4827RvA==", + "@types/react": { + "version": "16.8.22", + "bundled": true, "dev": true, "requires": { - "events-to-array": "^1.0.1", - "js-yaml": "^3.2.7", - "readable-stream": "^2" + "@types/prop-types": "*", + "csstype": "^2.2.0" } - } - } - }, - "tap-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-7.0.0.tgz", - "integrity": "sha512-05G8/LrzqOOFvZhhAk32wsGiPZ1lfUrl+iV7+OkKgfofZxiceZWMHkKmow71YsyVQ8IvGBP2EjcIjE5gL4l5lA==", - "dev": true, - "requires": { - "events-to-array": "^1.0.1", - "js-yaml": "^3.2.7", - "minipass": "^2.2.0" - } - }, - "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", - "dev": true, + }, + "ansi-escapes": { + "version": "3.2.0", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true + }, + "ansicolors": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "auto-bind": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "@types/react": "^16.8.12" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.3", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "bundled": true, + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true + } + } + }, + "babel-helper-builder-react-jsx": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "esutils": "^2.0.2" + } + }, + "babel-helpers": { + "version": "6.24.1", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "bundled": true, + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "bundled": true, + "dev": true + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-react-jsx": { + "version": "6.24.1", + "bundled": true, + "dev": true, + "requires": { + "babel-helper-builder-react-jsx": "^6.24.1", + "babel-plugin-syntax-jsx": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-register": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "bundled": true, + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "caller-callsite": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "cardinal": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "ci-info": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-truncate": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "slice-ansi": "^1.0.0", + "string-width": "^2.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "bundled": true, + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "core-js": { + "version": "2.6.5", + "bundled": true, + "dev": true + }, + "csstype": { + "version": "2.6.5", + "bundled": true, + "dev": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "bundled": true, + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "esprima": { + "version": "4.0.1", + "bundled": true, + "dev": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true, + "dev": true + }, + "events-to-array": { + "version": "1.1.2", + "bundled": true, + "dev": true + }, + "globals": { + "version": "9.18.0", + "bundled": true, + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "import-jsx": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "babel-core": "^6.25.0", + "babel-plugin-transform-es2015-destructuring": "^6.23.0", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-jsx": "^6.24.1", + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "ink": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "requires": { + "@types/react": "^16.8.6", + "arrify": "^1.0.1", + "auto-bind": "^2.0.0", + "chalk": "^2.4.1", + "cli-cursor": "^2.1.0", + "cli-truncate": "^1.1.0", + "is-ci": "^2.0.0", + "lodash.throttle": "^4.1.1", + "log-update": "^3.0.0", + "prop-types": "^15.6.2", + "react-reconciler": "^0.20.0", + "scheduler": "^0.13.2", + "signal-exit": "^3.0.2", + "slice-ansi": "^1.0.0", + "string-length": "^2.0.0", + "widest-line": "^2.0.0", + "wrap-ansi": "^5.0.0", + "yoga-layout-prebuilt": "^1.9.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "bundled": true, + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + } + } + }, + "invariant": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true, + "dev": true + }, + "json5": { + "version": "0.5.1", + "bundled": true, + "dev": true + }, + "lodash": { + "version": "4.17.14", + "bundled": true, + "dev": true + }, + "lodash.throttle": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "log-update": { + "version": "3.2.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "cli-cursor": "^2.1.0", + "wrap-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "bundled": true, + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + } + } + }, + "loose-envify": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "onetime": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "bundled": true, + "dev": true + } + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "private": { + "version": "0.1.8", + "bundled": true, + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "punycode": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "react": { + "version": "16.8.6", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "react-is": { + "version": "16.8.6", + "bundled": true, + "dev": true + }, + "react-reconciler": { + "version": "0.20.4", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "redeyed": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "esprima": "~4.0.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "bundled": true, + "dev": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "scheduler": { + "version": "0.13.6", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "slash": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-length": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "tap-parser": { + "version": "9.3.2", + "bundled": true, + "dev": true, + "requires": { + "events-to-array": "^1.0.1", + "minipass": "^2.2.0", + "tap-yaml": "^1.0.0" + } + }, + "tap-yaml": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "yaml": "^1.5.0" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "treport": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "requires": { + "cardinal": "^2.1.1", + "chalk": "^2.4.2", + "import-jsx": "^2.0.0", + "ink": "^2.1.1", + "ms": "^2.1.1", + "react": "^16.8.6", + "string-length": "^2.0.0", + "tap-parser": "^9.3.2", + "unicode-length": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "bundled": true, + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "ms": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "unicode-length": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "punycode": "^2.0.0", + "strip-ansi": "^3.0.1" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "widest-line": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^2.1.1" + } + }, + "yaml": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5" + } + }, + "yoga-layout-prebuilt": { + "version": "1.9.3", + "bundled": true, + "dev": true + } + } + }, + "tap-mocha-reporter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-4.0.1.tgz", + "integrity": "sha512-/KfXaaYeSPn8qBi5Be8WSIP3iKV83s2uj2vzImJAXmjNu22kzqZ+1Dv1riYWa53sPCiyo1R1w1jbJrftF8SpcQ==", + "dev": true, + "requires": { + "color-support": "^1.1.0", + "debug": "^2.1.3", + "diff": "^1.3.2", + "escape-string-regexp": "^1.0.3", + "glob": "^7.0.5", + "readable-stream": "^2.1.5", + "tap-parser": "^8.0.0", + "tap-yaml": "0 || 1", + "unicode-length": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "tap-parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-8.1.0.tgz", + "integrity": "sha512-GgOzgDwThYLxhVR83RbS1JUR1TxcT+jfZsrETgPAvFdr12lUOnuvrHOBaUQgpkAp6ZyeW6r2Nwd91t88M0ru3w==", + "dev": true, + "requires": { + "events-to-array": "^1.0.1", + "minipass": "^2.2.0", + "tap-yaml": "0 || 1" + } + }, + "tap-yaml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.0.tgz", + "integrity": "sha512-Rxbx4EnrWkYk0/ztcm5u3/VznbyFJpyXO12dDBHKWiDVxy7O2Qw6MRrwO5H6Ww0U5YhRY/4C/VzWmFPhBQc4qQ==", + "dev": true, + "requires": { + "yaml": "^1.5.0" + } + }, + "tcompare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-2.3.0.tgz", + "integrity": "sha512-fAfA73uFtFGybWGt4+IYT6UPLYVZQ4NfsP+IXEZGY0vh8e2IF7LVKafcQNMRBLqP0wzEA65LM9Tqj+FSmO8GLw==", + "dev": true + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, "requires": { "glob": "^7.1.3", "minimatch": "^3.0.4", @@ -1810,18 +3105,21 @@ "require-main-filename": "^2.0.0" } }, - "tmatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-4.0.0.tgz", - "integrity": "sha512-Ynn2Gsp+oCvYScQXeV+cCs7citRDilq0qDXA6tuvFwDgiYyyaq7D5vKUlAPezzZR5NDobc/QMeN6e5guOYmvxg==", - "dev": true - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -1863,22 +3161,8 @@ "make-error": "^1.1.1", "source-map-support": "^0.5.6", "yn": "^3.0.0" - }, - "dependencies": { - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true - } } }, - "tsame": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tsame/-/tsame-2.0.1.tgz", - "integrity": "sha512-jxyxgKVKa4Bh5dPcO42TJL22lIvfd9LOVJwdovKOnJa4TLLrHxquK+DlGm4rkGmrcur+GRx+x4oW00O2pY/fFw==", - "dev": true - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -1894,6 +3178,15 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", @@ -1909,15 +3202,6 @@ "requires": { "commander": "~2.20.0", "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } } }, "unicode-length": { @@ -1996,6 +3280,12 @@ "extsprintf": "^1.2.0" } }, + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -2018,14 +3308,50 @@ "dev": true }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } } }, "wrappy": { @@ -2035,14 +3361,15 @@ "dev": true }, "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.0.tgz", + "integrity": "sha512-EIgkf60l2oWsffja2Sf2AL384dx328c0B+cIYPTQq5q2rOYuDV00/iPFBOUiDKKwKMOhkymH8AidPaRvzfxY+Q==", "dev": true, "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "y18n": { @@ -2057,6 +3384,15 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yaml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.6.0.tgz", + "integrity": "sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5" + } + }, "yapool": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/yapool/-/yapool-1.0.0.tgz", @@ -2079,6 +3415,56 @@ "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^13.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + } } }, "yargs-parser": { diff --git a/package.json b/package.json index ebf1efc..387eba9 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,13 @@ ], "license": "ISC", "devDependencies": { - "import-fresh": "^2.0.0", - "mkdirp": "^0.5.0", - "rimraf": "^2.2.8", - "tap": "^12.7.0" + "import-fresh": "^3.1.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.3", + "tap": "^14.6.1" + }, + "engines": { + "node": ">=8" }, "files": [ "fs.js", From 99b67e5de0e1381e91a4729e636f44a8ef91751d Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 19:16:40 -0400 Subject: [PATCH 05/34] Drop legacy-streams.js --- graceful-fs.js | 7 --- legacy-streams.js | 118 ---------------------------------------------- package.json | 6 +-- 3 files changed, 2 insertions(+), 129 deletions(-) delete mode 100644 legacy-streams.js diff --git a/graceful-fs.js b/graceful-fs.js index a644d57..cfe2856 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -1,6 +1,5 @@ var fs = require('fs') var polyfills = require('./polyfills.js') -var legacy = require('./legacy-streams.js') var clone = require('./clone.js') var util = require('util') @@ -182,12 +181,6 @@ function patch (fs) { return fs$readdir.apply(fs, args) } - if (process.version.substr(0, 4) === 'v0.8') { - var legStreams = legacy(fs) - ReadStream = legStreams.ReadStream - WriteStream = legStreams.WriteStream - } - var fs$ReadStream = fs.ReadStream if (fs$ReadStream) { ReadStream.prototype = Object.create(fs$ReadStream.prototype) diff --git a/legacy-streams.js b/legacy-streams.js deleted file mode 100644 index d617b50..0000000 --- a/legacy-streams.js +++ /dev/null @@ -1,118 +0,0 @@ -var Stream = require('stream').Stream - -module.exports = legacy - -function legacy (fs) { - return { - ReadStream: ReadStream, - WriteStream: WriteStream - } - - function ReadStream (path, options) { - if (!(this instanceof ReadStream)) return new ReadStream(path, options); - - Stream.call(this); - - var self = this; - - this.path = path; - this.fd = null; - this.readable = true; - this.paused = false; - - this.flags = 'r'; - this.mode = 438; /*=0666*/ - this.bufferSize = 64 * 1024; - - options = options || {}; - - // Mixin options into this - var keys = Object.keys(options); - for (var index = 0, length = keys.length; index < length; index++) { - var key = keys[index]; - this[key] = options[key]; - } - - if (this.encoding) this.setEncoding(this.encoding); - - if (this.start !== undefined) { - if ('number' !== typeof this.start) { - throw TypeError('start must be a Number'); - } - if (this.end === undefined) { - this.end = Infinity; - } else if ('number' !== typeof this.end) { - throw TypeError('end must be a Number'); - } - - if (this.start > this.end) { - throw new Error('start must be <= end'); - } - - this.pos = this.start; - } - - if (this.fd !== null) { - process.nextTick(function() { - self._read(); - }); - return; - } - - fs.open(this.path, this.flags, this.mode, function (err, fd) { - if (err) { - self.emit('error', err); - self.readable = false; - return; - } - - self.fd = fd; - self.emit('open', fd); - self._read(); - }) - } - - function WriteStream (path, options) { - if (!(this instanceof WriteStream)) return new WriteStream(path, options); - - Stream.call(this); - - this.path = path; - this.fd = null; - this.writable = true; - - this.flags = 'w'; - this.encoding = 'binary'; - this.mode = 438; /*=0666*/ - this.bytesWritten = 0; - - options = options || {}; - - // Mixin options into this - var keys = Object.keys(options); - for (var index = 0, length = keys.length; index < length; index++) { - var key = keys[index]; - this[key] = options[key]; - } - - if (this.start !== undefined) { - if ('number' !== typeof this.start) { - throw TypeError('start must be a Number'); - } - if (this.start < 0) { - throw new Error('start must be >= zero'); - } - - this.pos = this.start; - } - - this.busy = false; - this._queue = []; - - if (this.fd === null) { - this._open = fs.open; - this._queue.push([this._open, this.path, this.flags, this.mode, undefined]); - this.flush(); - } - } -} diff --git a/package.json b/package.json index 387eba9..00c38bb 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,9 @@ "node": ">=8" }, "files": [ - "fs.js", + "clone.js", "graceful-fs.js", - "legacy-streams.js", - "polyfills.js", - "clone.js" + "polyfills.js" ], "dependencies": {} } From 91ed07a9f1a5fd929c1e76288bccdffbfdf20468 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 19:21:58 -0400 Subject: [PATCH 06/34] Remove v0.6.2 backport fix for fs.lchmod --- polyfills.js | 50 -------------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/polyfills.js b/polyfills.js index a5808d2..b43501d 100644 --- a/polyfills.js +++ b/polyfills.js @@ -25,13 +25,6 @@ module.exports = patch function patch (fs) { // (re-)implement some things that are known busted or missing. - // lchmod, broken prior to 0.6.2 - // back-port the fix here. - if (constants.hasOwnProperty('O_SYMLINK') && - process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) { - patchLchmod(fs) - } - // lutimes implementation, or no-op if (!fs.lutimes) { patchLutimes(fs) @@ -151,49 +144,6 @@ function patch (fs) { } }})(fs.readSync) - function patchLchmod (fs) { - fs.lchmod = function (path, mode, callback) { - fs.open( path - , constants.O_WRONLY | constants.O_SYMLINK - , mode - , function (err, fd) { - if (err) { - if (callback) callback(err) - return - } - // prefer to return the chmod error, if one occurs, - // but still try to close, and report closing errors if they occur. - fs.fchmod(fd, mode, function (err) { - fs.close(fd, function(err2) { - if (callback) callback(err || err2) - }) - }) - }) - } - - fs.lchmodSync = function (path, mode) { - var fd = fs.openSync(path, constants.O_WRONLY | constants.O_SYMLINK, mode) - - // prefer to return the chmod error, if one occurs, - // but still try to close, and report closing errors if they occur. - var threw = true - var ret - try { - ret = fs.fchmodSync(fd, mode) - threw = false - } finally { - if (threw) { - try { - fs.closeSync(fd) - } catch (er) {} - } else { - fs.closeSync(fd) - } - } - return ret - } - } - function patchLutimes (fs) { if (constants.hasOwnProperty("O_SYMLINK")) { fs.lutimes = function (path, at, mt, cb) { From cb8ca432b8613de9d6a8b79851c892067633e040 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 19:53:19 -0400 Subject: [PATCH 07/34] Remove fs.*stat* gid/uid fixup This was for an issue with versions of node.js that are EOL. Node.js commits which fixed this issue: * v4.6.2+ - nodejs/node@3d6f107a2f * v6.8.0+ - nodejs/node@07d97f4f3e * v7.0.0+ - nodejs/node@3d6f107a2f Also remove associated testing. --- polyfills.js | 42 ----------------------------------------- test/enoent.js | 33 -------------------------------- test/stats-uid-gid.js | 44 ------------------------------------------- test/stats.js | 12 ------------ 4 files changed, 131 deletions(-) delete mode 100644 test/stats-uid-gid.js delete mode 100644 test/stats.js diff --git a/polyfills.js b/polyfills.js index b43501d..1f2ade0 100644 --- a/polyfills.js +++ b/polyfills.js @@ -51,14 +51,6 @@ function patch (fs) { fs.fchmodSync = chmodFixSync(fs.fchmodSync) fs.lchmodSync = chmodFixSync(fs.lchmodSync) - fs.stat = statFix(fs.stat) - fs.fstat = statFix(fs.fstat) - fs.lstat = statFix(fs.lstat) - - fs.statSync = statFixSync(fs.statSync) - fs.fstatSync = statFixSync(fs.fstatSync) - fs.lstatSync = statFixSync(fs.lstatSync) - // if lchmod/lchown do not exist, then make them no-ops if (!fs.lchmod) { fs.lchmod = function (path, mode, cb) { @@ -228,40 +220,6 @@ function patch (fs) { } } - function statFix (orig) { - if (!orig) return orig - // Older versions of Node erroneously returned signed integers for - // uid + gid. - return function (target, options, cb) { - if (typeof options === 'function') { - cb = options - options = null - } - function callback (er, stats) { - if (stats) { - if (stats.uid < 0) stats.uid += 0x100000000 - if (stats.gid < 0) stats.gid += 0x100000000 - } - if (cb) cb.apply(this, arguments) - } - return options ? orig.call(fs, target, options, callback) - : orig.call(fs, target, callback) - } - } - - function statFixSync (orig) { - if (!orig) return orig - // Older versions of Node erroneously returned signed integers for - // uid + gid. - return function (target, options) { - var stats = options ? orig.call(fs, target, options) - : orig.call(fs, target) - if (stats.uid < 0) stats.uid += 0x100000000 - if (stats.gid < 0) stats.gid += 0x100000000 - return stats; - } - } - // ENOSYS means that the fs doesn't support the op. Just ignore // that, because it doesn't matter. // diff --git a/test/enoent.js b/test/enoent.js index 98d5c1d..87170f3 100644 --- a/test/enoent.js +++ b/test/enoent.js @@ -2,37 +2,11 @@ // some other kind of throw. var g = require('../') - -var NODE_VERSION_MAJOR_WITH_BIGINT = 10 -var NODE_VERSION_MINOR_WITH_BIGINT = 5 -var NODE_VERSION_PATCH_WITH_BIGINT = 0 -var nodeVersion = process.versions.node.split('.') -var nodeVersionMajor = Number.parseInt(nodeVersion[0], 10) -var nodeVersionMinor = Number.parseInt(nodeVersion[1], 10) -var nodeVersionPatch = Number.parseInt(nodeVersion[2], 10) - -function nodeSupportsBigInt () { - if (nodeVersionMajor > NODE_VERSION_MAJOR_WITH_BIGINT) { - return true - } else if (nodeVersionMajor === NODE_VERSION_MAJOR_WITH_BIGINT) { - if (nodeVersionMinor > NODE_VERSION_MINOR_WITH_BIGINT) { - return true - } else if (nodeVersionMinor === NODE_VERSION_MINOR_WITH_BIGINT) { - if (nodeVersionPatch >= NODE_VERSION_PATCH_WITH_BIGINT) { - return true - } - } - } - return false -} - var t = require('tap') var file = 'this file does not exist even a little bit' var methods = [ ['open', 'r'], ['readFile'], - ['stat'], - ['lstat'], ['utimes', new Date(), new Date()], ['readdir'] ] @@ -42,13 +16,6 @@ if (process.version.match(/^v([6-9]|[1-9][0-9])\./)) { methods.push(['readdir', {}]) } -// any version > v10.5 can do stat(path, options, cb) -if (nodeSupportsBigInt()) { - methods.push(['stat', {}]) - methods.push(['lstat', {}]) -} - - t.plan(methods.length) methods.forEach(function (method) { t.test(method[0], runTest(method)) diff --git a/test/stats-uid-gid.js b/test/stats-uid-gid.js deleted file mode 100644 index 7422e5d..0000000 --- a/test/stats-uid-gid.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; -var util = require('util') -var fs = require('fs') -var test = require('tap').test - -// mock fs.statSync to return signed uids/gids -var realStatSync = fs.statSync -fs.statSync = function(path) { - var stats = realStatSync.call(fs, path) - stats.uid = -2 - stats.gid = -2 - return stats -} - -var gfs = require('../graceful-fs.js') - -test('graceful fs uses same stats constructor as fs', function (t) { - t.equal(gfs.Stats, fs.Stats, 'should reference the same constructor') - - if (!process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH) { - t.equal(fs.statSync(__filename).uid, -2) - t.equal(fs.statSync(__filename).gid, -2) - } - - t.equal(gfs.statSync(__filename).uid, 0xfffffffe) - t.equal(gfs.statSync(__filename).gid, 0xfffffffe) - - t.end() -}) - -test('does not throw when async stat fails', function (t) { - gfs.stat(__filename + ' this does not exist', function (er, stats) { - t.ok(er) - t.notOk(stats) - t.end() - }) -}) - -test('throws ENOENT when sync stat fails', function (t) { - t.throws(function() { - gfs.statSync(__filename + ' this does not exist') - }, /ENOENT/) - t.end() -}) diff --git a/test/stats.js b/test/stats.js deleted file mode 100644 index b6c45b9..0000000 --- a/test/stats.js +++ /dev/null @@ -1,12 +0,0 @@ -var fs = require('fs') -var gfs = require('../graceful-fs.js') -var test = require('tap').test - -test('graceful fs uses same stats constructor as fs', function (t) { - t.equal(gfs.Stats, fs.Stats, 'should reference the same constructor') - t.ok(fs.statSync(__filename) instanceof fs.Stats, - 'should be instance of fs.Stats') - t.ok(gfs.statSync(__filename) instanceof fs.Stats, - 'should be instance of fs.Stats') - t.end() -}) From d70ba367f3c3cc223e82dc712b286becc383fd6b Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 20:09:25 -0400 Subject: [PATCH 08/34] Drop fallback for util.debuglog --- graceful-fs.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index cfe2856..fa52edd 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -6,17 +6,7 @@ var util = require('util') var gracefulQueue = Symbol.for('graceful-fs.queue') -function noop () {} - -var debug = noop -if (util.debuglog) - debug = util.debuglog('gfs4') -else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) - debug = function() { - var m = util.format.apply(util, arguments) - m = 'GFS4: ' + m.split(/\n/).join('\nGFS4: ') - console.error(m) - } +const debug = util.debuglog('gfs4') // Once time initialization if (!global[gracefulQueue]) { From 7e52d7fe6669053e95b8abe220ca969376272c60 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 20:23:17 -0400 Subject: [PATCH 09/34] Unify ENFILE/EMFILE error checks Properly handle DEP0013 in ENFILE/EMFILE patched functions depending on node.js version. --- check-for-callback.js | 42 ++++++++++++ graceful-fs.js | 149 ++++++++++-------------------------------- normalize-args.js | 17 +++++ package.json | 2 + 4 files changed, 95 insertions(+), 115 deletions(-) create mode 100644 check-for-callback.js create mode 100644 normalize-args.js diff --git a/check-for-callback.js b/check-for-callback.js new file mode 100644 index 0000000..4a78589 --- /dev/null +++ b/check-for-callback.js @@ -0,0 +1,42 @@ +'use strict' + +const nodeMajor = process.versions.node.split('.')[0] + +/* This function emulates the functionality of node.js 7, 8 and 9 by emitting a + * deprecation warning. In node.js 10+ a TypeError is produced. */ +function checkForCallback (cb, ctor) { + if (typeof cb === 'function') { + return true + } + + if (nodeMajor >= 10) { + /* It's possible that the caller provided something incorrect for the callback + * but more likely they omitted the argument. + * + * The error is technically wrong if someone provides something for the callback + * argument which is not a function, for example `fs.mkdir('/path', {}, 'cb')` + * should say that a string was provided. Providing the correct exception for + * this off nominal case would require argument processing to be specific to each + * patched function. */ + const error = new TypeError('Callback must be a function. Received undefined') + error.code = 'ERR_INVALID_CALLBACK' + + /* The next 4 lines are copied from node.js, lib/internal/errors.js:addCodeToName() */ + error.name = `${error.name} [${error.code}]` + Error.captureStackTrace(error, ctor) + error.stack // eslint-disable-line no-unused-expressions + delete error.name + + throw error + } + + process.emitWarning( + 'Calling an asynchronous function without callback is deprecated.', + 'DeprecationWarning', + 'DEP0013', + ctor || checkForCallback + ) + return false +} + +module.exports = checkForCallback diff --git a/graceful-fs.js b/graceful-fs.js index fa52edd..c3324c7 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -1,6 +1,7 @@ var fs = require('fs') var polyfills = require('./polyfills.js') var clone = require('./clone.js') +const normalizeArgs = require('./normalize-args.js') var util = require('util') @@ -67,6 +68,25 @@ if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { fs.__patched = true; } +function patchENFILE (origImpl, setupArgs) { + function internalImpl (implArgs, cb) { + return origImpl(...implArgs, (err, ...args) => { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) { + enqueue([internalImpl, [implArgs, cb]]) + } else { + cb(err, ...args) + retry() + } + }) + } + + if (setupArgs) { + return (...args) => internalImpl(...setupArgs(...normalizeArgs(args))) + } + + return (...args) => internalImpl(...normalizeArgs(args)) +} + function patch (fs) { // Everything that references the open() function needs to be in here polyfills(fs) @@ -75,101 +95,21 @@ function patch (fs) { fs.FileWriteStream = WriteStream; // Legacy name. fs.createReadStream = createReadStream fs.createWriteStream = createWriteStream - var fs$readFile = fs.readFile - fs.readFile = readFile - function readFile (path, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$readFile(path, options, cb) - - function go$readFile (path, options, cb) { - return fs$readFile(path, options, function (err) { - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$readFile, [path, options, cb]]) - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() - } - }) - } - } - - var fs$writeFile = fs.writeFile - fs.writeFile = writeFile - function writeFile (path, data, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$writeFile(path, data, options, cb) - - function go$writeFile (path, data, options, cb) { - return fs$writeFile(path, data, options, function (err) { - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$writeFile, [path, data, options, cb]]) - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() - } - }) - } - } - - var fs$appendFile = fs.appendFile - if (fs$appendFile) - fs.appendFile = appendFile - function appendFile (path, data, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$appendFile(path, data, options, cb) - function go$appendFile (path, data, options, cb) { - return fs$appendFile(path, data, options, function (err) { - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$appendFile, [path, data, options, cb]]) - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() - } - }) - } - } - - var fs$readdir = fs.readdir - fs.readdir = readdir - function readdir (path, options, cb) { - var args = [path] - if (typeof options !== 'function') { - args.push(options) - } else { - cb = options - } - args.push(go$readdir$cb) - - return go$readdir(args) - - function go$readdir$cb (err, files) { - if (files && files.sort) - files.sort() - - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$readdir, [args]]) - - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() + fs.open = patchENFILE(fs.open) + fs.readFile = patchENFILE(fs.readFile) + fs.writeFile = patchENFILE(fs.writeFile) + fs.appendFile = patchENFILE(fs.appendFile) + fs.readdir = patchENFILE(fs.readdir, (args, cb) => [ + args, + (err, files) => { + if (files && files.sort) { + files = files.sort() } - } - } - function go$readdir (args) { - return fs$readdir.apply(fs, args) - } + cb(err, files) + } + ]) var fs$ReadStream = fs.ReadStream if (fs$ReadStream) { @@ -195,7 +135,7 @@ function patch (fs) { function ReadStream$open () { var that = this - open(that.path, that.flags, that.mode, function (err, fd) { + fs.open(that.path, that.flags, that.mode, function (err, fd) { if (err) { if (that.autoClose) that.destroy() @@ -218,7 +158,7 @@ function patch (fs) { function WriteStream$open () { var that = this - open(that.path, that.flags, that.mode, function (err, fd) { + fs.open(that.path, that.flags, that.mode, function (err, fd) { if (err) { that.destroy() that.emit('error', err) @@ -237,27 +177,6 @@ function patch (fs) { return new WriteStream(path, options) } - var fs$open = fs.open - fs.open = open - function open (path, flags, mode, cb) { - if (typeof mode === 'function') - cb = mode, mode = null - - return go$open(path, flags, mode, cb) - - function go$open (path, flags, mode, cb) { - return fs$open(path, flags, mode, function (err, fd) { - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$open, [path, flags, mode, cb]]) - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() - } - }) - } - } - return fs } diff --git a/normalize-args.js b/normalize-args.js new file mode 100644 index 0000000..a04e11c --- /dev/null +++ b/normalize-args.js @@ -0,0 +1,17 @@ +'use strict' + +const checkForCallback = require('./check-for-callback.js') + +function normalizeArgs (args) { + let cb = args.slice(-1)[0] + if (checkForCallback(cb, normalizeArgs)) { + args.splice(-1, 1) + } else { + /* This is for node.js < 10 only, newer versions will throw in checkForCallback. */ + cb = () => {} + } + + return [args, cb] +} + +module.exports = normalizeArgs diff --git a/package.json b/package.json index 00c38bb..8fee50d 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,10 @@ "node": ">=8" }, "files": [ + "check-for-callback.js", "clone.js", "graceful-fs.js", + "normalize-args.js", "polyfills.js" ], "dependencies": {} From be2498f951411be9e6c0b83c6decc6c018a606f4 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 20:03:12 -0400 Subject: [PATCH 10/34] Create unified ReadStream / WriteStream patching function * Add autoClose support to WriteStream.open * Prevent accidental monkey-patching of fs.*Stream --- graceful-fs.js | 128 +++++++++++++++++++++++-------------------------- 1 file changed, 59 insertions(+), 69 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index c3324c7..79675de 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -87,14 +87,67 @@ function patchENFILE (origImpl, setupArgs) { return (...args) => internalImpl(...normalizeArgs(args)) } +function patchStream (fs, isRead) { + const name = isRead ? 'ReadStream' : 'WriteStream' + const origImpl = fs[name] + + function PatchedStream (...args) { + if (this instanceof PatchedStream) { + origImpl.apply(this, args) + return this + } + + return new PatchedStream(...args) + } + + PatchedStream.prototype = Object.create(origImpl.prototype) + PatchedStream.prototype.open = PatchedStream$open + + function PatchedStream$open () { + fs.open(this.path, this.flags, this.mode, (err, fd) => { + if (err) { + if (this.autoClose) { + this.destroy() + } + + this.emit('error', err) + } else { + this.fd = fd + this.emit('open', fd) + if (isRead) { + this.read() + } + } + }) + } + + let Klass = PatchedStream + + fs[`create${name}`] = (...args) => new Klass(...args) + + Object.defineProperties(fs, { + [name]: { + get () { + return Klass + }, + set (value) { + Klass = value + }, + enumerable: true + }, + // Legacy name + [`File${name}`]: { + value: PatchedStream, + writable: true, + enumerable: true + } + }) +} + function patch (fs) { // Everything that references the open() function needs to be in here polyfills(fs) fs.gracefulify = patch - fs.FileReadStream = ReadStream; // Legacy name. - fs.FileWriteStream = WriteStream; // Legacy name. - fs.createReadStream = createReadStream - fs.createWriteStream = createWriteStream fs.open = patchENFILE(fs.open) fs.readFile = patchENFILE(fs.readFile) @@ -111,71 +164,8 @@ function patch (fs) { } ]) - var fs$ReadStream = fs.ReadStream - if (fs$ReadStream) { - ReadStream.prototype = Object.create(fs$ReadStream.prototype) - ReadStream.prototype.open = ReadStream$open - } - - var fs$WriteStream = fs.WriteStream - if (fs$WriteStream) { - WriteStream.prototype = Object.create(fs$WriteStream.prototype) - WriteStream.prototype.open = WriteStream$open - } - - fs.ReadStream = ReadStream - fs.WriteStream = WriteStream - - function ReadStream (path, options) { - if (this instanceof ReadStream) - return fs$ReadStream.apply(this, arguments), this - else - return ReadStream.apply(Object.create(ReadStream.prototype), arguments) - } - - function ReadStream$open () { - var that = this - fs.open(that.path, that.flags, that.mode, function (err, fd) { - if (err) { - if (that.autoClose) - that.destroy() - - that.emit('error', err) - } else { - that.fd = fd - that.emit('open', fd) - that.read() - } - }) - } - - function WriteStream (path, options) { - if (this instanceof WriteStream) - return fs$WriteStream.apply(this, arguments), this - else - return WriteStream.apply(Object.create(WriteStream.prototype), arguments) - } - - function WriteStream$open () { - var that = this - fs.open(that.path, that.flags, that.mode, function (err, fd) { - if (err) { - that.destroy() - that.emit('error', err) - } else { - that.fd = fd - that.emit('open', fd) - } - }) - } - - function createReadStream (path, options) { - return new ReadStream(path, options) - } - - function createWriteStream (path, options) { - return new WriteStream(path, options) - } + patchStream(fs, true) + patchStream(fs, false) return fs } From 2bb23c3d6c3a4ea565d4171c015e4a28f198d8a0 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 21:46:32 -0400 Subject: [PATCH 11/34] Modernize / simplify clone.js --- clone.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/clone.js b/clone.js index 028356c..5aaa57a 100644 --- a/clone.js +++ b/clone.js @@ -3,17 +3,11 @@ module.exports = clone function clone (obj) { - if (obj === null || typeof obj !== 'object') - return obj + const copy = Object.create(Object.getPrototypeOf(obj)) - if (obj instanceof Object) - var copy = { __proto__: obj.__proto__ } - else - var copy = Object.create(null) - - Object.getOwnPropertyNames(obj).forEach(function (key) { + for (const key of Object.getOwnPropertyNames(obj)) { Object.defineProperty(copy, key, Object.getOwnPropertyDescriptor(obj, key)) - }) + } return copy } From 82736a84531af1bf388e34d1403828ce230d8c9b Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 5 Aug 2019 22:11:41 -0400 Subject: [PATCH 12/34] Modernize graceful-fs.js Properly handle DEP0013 in patched fs.close. --- graceful-fs.js | 68 ++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index 79675de..0a61b8d 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -1,20 +1,22 @@ -var fs = require('fs') -var polyfills = require('./polyfills.js') -var clone = require('./clone.js') -const normalizeArgs = require('./normalize-args.js') +'use strict' -var util = require('util') +const fs = require('fs') +const util = require('util') -var gracefulQueue = Symbol.for('graceful-fs.queue') +const polyfills = require('./polyfills.js') +const clone = require('./clone.js') +const normalizeArgs = require('./normalize-args.js') const debug = util.debuglog('gfs4') +const gracefulQueue = Symbol.for('graceful-fs.queue') + // Once time initialization if (!global[gracefulQueue]) { // This queue can be shared by multiple loaded instances - var queue = [] + const queue = [] Object.defineProperty(global, gracefulQueue, { - get: function() { + get () { return queue } }) @@ -26,38 +28,32 @@ if (!global[gracefulQueue]) { // to retry() whenever a close happens *anywhere* in the program. // This is essential when multiple graceful-fs instances are // in play at the same time. - fs.close = (function (fs$close) { - function close (fd, cb) { - return fs$close.call(fs, fd, function (err) { - // This function uses the graceful-fs shared queue - if (!err) { - retry() - } - - if (typeof cb === 'function') - cb.apply(this, arguments) - }) - } - - close[previous] = fs$close - return close - })(fs.close) + const {close, closeSync} = fs + fs.close = (fd, cb) => { + cb = normalizeArgs([cb])[1] - fs.closeSync = (function (fs$closeSync) { - function closeSync (fd) { + close(fd, err => { // This function uses the graceful-fs shared queue - fs$closeSync.apply(fs, arguments) - retry() - } + if (!err) { + retry() + } - closeSync[previous] = fs$closeSync - return closeSync - })(fs.closeSync) + cb(err) + }) + } + fs.close[previous] = close + + fs.closeSync = fd => { + // This function uses the graceful-fs shared queue + closeSync(fd) + retry() + } + fs.closeSync[previous] = closeSync if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { - process.on('exit', function() { + process.on('exit', () => { debug(global[gracefulQueue]) - require('assert').equal(global[gracefulQueue].length, 0) + require('assert').strictEqual(global[gracefulQueue].length, 0) }) } } @@ -176,9 +172,9 @@ function enqueue (elem) { } function retry () { - var elem = global[gracefulQueue].shift() + const elem = global[gracefulQueue].shift() if (elem) { debug('RETRY', elem[0].name, elem[1]) - elem[0].apply(null, elem[1]) + elem[0](...elem[1]) } } From 5af296f005e1f25c5e1bc939ed08786bc847fe8b Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 14:06:17 -0400 Subject: [PATCH 13/34] Modernize win32 fs.rename polyfill Process callback argument to ensure proper handling if missing. --- polyfills.js | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/polyfills.js b/polyfills.js index 1f2ade0..7bdeb73 100644 --- a/polyfills.js +++ b/polyfills.js @@ -1,4 +1,5 @@ var constants = require('constants') +const normalizeArgs = require('./normalize-args.js') var origCwd = process.cwd var cwd = null @@ -75,28 +76,36 @@ function patch (fs) { // CPU to a busy looping process, which can cause the program causing the lock // contention to be starved of CPU by node, so the contention doesn't resolve. if (platform === "win32") { - fs.rename = (function (fs$rename) { return function (from, to, cb) { - var start = Date.now() - var backoff = 0; - fs$rename(from, to, function CB (er) { - if (er - && (er.code === "EACCES" || er.code === "EPERM") - && Date.now() - start < 60000) { - setTimeout(function() { - fs.stat(to, function (stater, st) { - if (stater && stater.code === "ENOENT") - fs$rename(from, to, CB); - else + const accessErrors = new Set(['EACCES', 'EPERM']) + const {rename} = fs + + fs.rename = (from, to, cb) => { + const start = Date.now() + let backoff = 0 + cb = normalizeArgs([cb])[1] + + rename(from, to, function CB (er) { + if (er && accessErrors.has(er.code) && Date.now() - start < 60000) { + setTimeout(() => { + fs.stat(to, stater => { + if (stater && stater.code === 'ENOENT') { + rename(from, to, CB) + } else { cb(er) + } }) }, backoff) - if (backoff < 100) - backoff += 10; - return; + + if (backoff < 100) { + backoff += 10 + } + + return } - if (cb) cb(er) + + cb(er) }) - }})(fs.rename) + } } // if read() returns EAGAIN, then just try it again. From 99851efb8300c389b417fa4e6133b99335e5abd2 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 14:42:30 -0400 Subject: [PATCH 14/34] Modernize fs.read / fs.readSync polyfills --- polyfills.js | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/polyfills.js b/polyfills.js index 7bdeb73..15d49a3 100644 --- a/polyfills.js +++ b/polyfills.js @@ -1,4 +1,5 @@ var constants = require('constants') +const checkForCallback = require('./check-for-callback.js') const normalizeArgs = require('./normalize-args.js') var origCwd = process.cwd @@ -108,42 +109,40 @@ function patch (fs) { } } + const {read, readSync} = fs // if read() returns EAGAIN, then just try it again. - fs.read = (function (fs$read) { - function read (fd, buffer, offset, length, position, callback_) { - var callback - if (callback_ && typeof callback_ === 'function') { - var eagCounter = 0 - callback = function (er, _, __) { - if (er && er.code === 'EAGAIN' && eagCounter < 10) { - eagCounter ++ - return fs$read.call(fs, fd, buffer, offset, length, position, callback) - } - callback_.apply(this, arguments) - } + fs.read = (fd, buffer, offset, length, position, cb) => { + checkForCallback(cb) + + let eagCounter = 0 + read(fd, buffer, offset, length, position, function CB (er, ...args) { + if (er && er.code === 'EAGAIN' && eagCounter < 10) { + eagCounter ++ + read(fd, buffer, offset, length, position, CB) + return } - return fs$read.call(fs, fd, buffer, offset, length, position, callback) - } - // This ensures `util.promisify` works as it does for native `fs.read`. - read.__proto__ = fs$read - return read - })(fs.read) + cb(er, ...args) + }) + } + // This ensures `util.promisify` works as it does for native `fs.read`. + Object.setPrototypeOf(fs.read, read) - fs.readSync = (function (fs$readSync) { return function (fd, buffer, offset, length, position) { - var eagCounter = 0 + fs.readSync = (...args) => { + let eagCounter = 0 while (true) { try { - return fs$readSync.call(fs, fd, buffer, offset, length, position) + return readSync(...args) } catch (er) { if (er.code === 'EAGAIN' && eagCounter < 10) { eagCounter ++ continue } + throw er } } - }})(fs.readSync) + } function patchLutimes (fs) { if (constants.hasOwnProperty("O_SYMLINK")) { From da0b91866cfe3e8dd7847e99cf63fe933b221f3e Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 15:40:24 -0400 Subject: [PATCH 15/34] Unify chown/chmod polyfills --- polyfills.js | 87 ++++++++++++++++++++-------------------------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/polyfills.js b/polyfills.js index 15d49a3..74fac8f 100644 --- a/polyfills.js +++ b/polyfills.js @@ -37,21 +37,21 @@ function patch (fs) { // It should not fail on enosys ever, as this just indicates // that a fs doesn't support the intended operation. - fs.chown = chownFix(fs.chown) - fs.fchown = chownFix(fs.fchown) - fs.lchown = chownFix(fs.lchown) + fs.chown = patchChownErFilter(fs.chown) + fs.fchown = patchChownErFilter(fs.fchown) + fs.lchown = patchChownErFilter(fs.lchown) - fs.chmod = chmodFix(fs.chmod) - fs.fchmod = chmodFix(fs.fchmod) - fs.lchmod = chmodFix(fs.lchmod) + fs.chmod = patchChownErFilter(fs.chmod) + fs.fchmod = patchChownErFilter(fs.fchmod) + fs.lchmod = patchChownErFilter(fs.lchmod) - fs.chownSync = chownFixSync(fs.chownSync) - fs.fchownSync = chownFixSync(fs.fchownSync) - fs.lchownSync = chownFixSync(fs.lchownSync) + fs.chownSync = patchChownSyncErFilter(fs.chownSync) + fs.fchownSync = patchChownSyncErFilter(fs.fchownSync) + fs.lchownSync = patchChownSyncErFilter(fs.lchownSync) - fs.chmodSync = chmodFixSync(fs.chmodSync) - fs.fchmodSync = chmodFixSync(fs.fchmodSync) - fs.lchmodSync = chmodFixSync(fs.lchmodSync) + fs.chmodSync = patchChownSyncErFilter(fs.chmodSync) + fs.fchmodSync = patchChownSyncErFilter(fs.fchmodSync) + fs.lchmodSync = patchChownSyncErFilter(fs.lchmodSync) // if lchmod/lchown do not exist, then make them no-ops if (!fs.lchmod) { @@ -185,45 +185,29 @@ function patch (fs) { } } - function chmodFix (orig) { - if (!orig) return orig - return function (target, mode, cb) { - return orig.call(fs, target, mode, function (er) { - if (chownErOk(er)) er = null - if (cb) cb.apply(this, arguments) - }) + function patchChownErFilter (orig) { + if (!orig) { + return orig } - } - function chmodFixSync (orig) { - if (!orig) return orig - return function (target, mode) { - try { - return orig.call(fs, target, mode) - } catch (er) { - if (!chownErOk(er)) throw er - } + return (...userArgs) => { + const [args, cb] = normalizeArgs(userArgs) + return orig(...args, (er, ...cbArgs) => cb(chownErFilter(er), ...cbArgs)) } } - - function chownFix (orig) { - if (!orig) return orig - return function (target, uid, gid, cb) { - return orig.call(fs, target, uid, gid, function (er) { - if (chownErOk(er)) er = null - if (cb) cb.apply(this, arguments) - }) + function patchChownSyncErFilter (orig) { + if (!orig) { + return orig } - } - function chownFixSync (orig) { - if (!orig) return orig - return function (target, uid, gid) { + return (...args) => { try { - return orig.call(fs, target, uid, gid) + return orig(...args) } catch (er) { - if (!chownErOk(er)) throw er + if (chownErFilter(er)) { + throw er + } } } } @@ -240,19 +224,16 @@ function patch (fs) { // // When running as root, or if other types of errors are // encountered, then it's strict. - function chownErOk (er) { - if (!er) - return true - - if (er.code === "ENOSYS") - return true + function chownErFilter (er) { + if (!er || er.code === 'ENOSYS') { + return + } - var nonroot = !process.getuid || process.getuid() !== 0 - if (nonroot) { - if (er.code === "EINVAL" || er.code === "EPERM") - return true + const nonroot = !process.getuid || process.getuid() !== 0 + if (nonroot && (er.code === 'EINVAL' || er.code === 'EPERM')) { + return } - return false + return er } } From 8e34dfc9fd21b79a2785c646f7e995ba5cf6828a Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 17:25:04 -0400 Subject: [PATCH 16/34] Remove TEST_GRACEFUL_FS_GLOBAL_PATCH, test with gfs.gracefulify --- graceful-fs.js | 13 +++++++++---- test.js | 2 +- test/avoid-memory-leak.js | 12 ++++++++++-- test/chown-er-ok.js | 2 +- test/close.js | 5 +++-- test/enoent.js | 2 +- test/helpers/graceful-fs.js | 5 +++++ test/max-open.js | 2 +- test/open.js | 2 +- test/read-write-stream.js | 2 +- test/readdir-options.js | 2 +- test/readdir-sort.js | 2 +- test/readfile.js | 2 +- test/windows-rename-polyfill.js | 2 +- test/write-then-read.js | 2 +- 15 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 test/helpers/graceful-fs.js diff --git a/graceful-fs.js b/graceful-fs.js index 0a61b8d..421d84a 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -9,6 +9,7 @@ const normalizeArgs = require('./normalize-args.js') const debug = util.debuglog('gfs4') +const gracefulPatched = Symbol.for('graceful-fs.patched') const gracefulQueue = Symbol.for('graceful-fs.queue') // Once time initialization @@ -59,10 +60,6 @@ if (!global[gracefulQueue]) { } module.exports = patch(clone(fs)) -if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { - module.exports = patch(fs) - fs.__patched = true; -} function patchENFILE (origImpl, setupArgs) { function internalImpl (implArgs, cb) { @@ -141,6 +138,14 @@ function patchStream (fs, isRead) { } function patch (fs) { + if (fs[gracefulPatched]) { + return fs + } + + Object.defineProperty(fs, gracefulPatched, { + value: true + }) + // Everything that references the open() function needs to be in here polyfills(fs) fs.gracefulify = patch diff --git a/test.js b/test.js index 54da6b5..a0311cd 100644 --- a/test.js +++ b/test.js @@ -8,7 +8,7 @@ var env = Object.keys(process.env).reduce(function (env, k) { env[k] = process.env[k] return env }, { - TEST_GRACEFUL_FS_GLOBAL_PATCH: 1 + TEST_GFS_GLOBAL_PATCH: '1' }) files.filter(function (f) { diff --git a/test/avoid-memory-leak.js b/test/avoid-memory-leak.js index d30392f..9e26655 100644 --- a/test/avoid-memory-leak.js +++ b/test/avoid-memory-leak.js @@ -1,3 +1,4 @@ +var path = require('path') var importFresh = require('import-fresh'); var t = require('tap') var v8 @@ -21,13 +22,20 @@ function checkHeap (t) { } t.test('no memory leak when loading multiple times', function(t) { + const gfsPath = path.resolve(__dirname, '../graceful-fs.js') + const gfsHelper = path.join(__dirname, './helpers/graceful-fs.js') + importFresh(gfsHelper) + t.plan(1); - importFresh(process.cwd() + '/graceful-fs.js') // node 0.10-5 were getting: Cannot find module '../' previousHeapStats = process.memoryUsage() // simulate project with 4000 tests var i = 0; function importFreshGracefulFs() { - importFresh(process.cwd() + '/graceful-fs.js'); + delete require.cache[gfsPath] + // We have to use absolute path because importFresh cannot find + // relative paths when run from the callback of `process.nextTick`. + importFresh(gfsHelper) + if (i < 4000) { i++; process.nextTick(() => importFreshGracefulFs()); diff --git a/test/chown-er-ok.js b/test/chown-er-ok.js index aad7815..1447d76 100644 --- a/test/chown-er-ok.js +++ b/test/chown-er-ok.js @@ -24,7 +24,7 @@ function makeErr (path, method) { return err } -var fs = require('../') +var fs = require('./helpers/graceful-fs.js') var t = require('tap') var errs = ['ENOSYS', 'EINVAL', 'EPERM'] diff --git a/test/close.js b/test/close.js index 7210b31..9041cfd 100644 --- a/test/close.js +++ b/test/close.js @@ -1,12 +1,13 @@ var fs = require('fs') var path = require('path') -var gfsPath = path.resolve(__dirname, '..', 'graceful-fs.js') -var gfs = require(gfsPath) +var gfs = require('./helpers/graceful-fs.js') var importFresh = require('import-fresh') var fs$close = fs.close var fs$closeSync = fs.closeSync var test = require('tap').test +var gfsPath = path.resolve(__dirname, '..', 'graceful-fs.js') + test('`close` is patched correctly', function(t) { t.match(fs$close.toString(), /graceful-fs shared queue/, 'patch fs.close'); t.match(fs$closeSync.toString(), /graceful-fs shared queue/, 'patch fs.closeSync'); diff --git a/test/enoent.js b/test/enoent.js index 87170f3..3b725c9 100644 --- a/test/enoent.js +++ b/test/enoent.js @@ -1,7 +1,7 @@ // this test makes sure that various things get enoent, instead of // some other kind of throw. -var g = require('../') +var g = require('./helpers/graceful-fs.js') var t = require('tap') var file = 'this file does not exist even a little bit' var methods = [ diff --git a/test/helpers/graceful-fs.js b/test/helpers/graceful-fs.js new file mode 100644 index 0000000..5119008 --- /dev/null +++ b/test/helpers/graceful-fs.js @@ -0,0 +1,5 @@ +'use strict' + +const gfs = require('../../graceful-fs.js') + +module.exports = process.env.TEST_GFS_GLOBAL_PATCH ? gfs.gracefulify(require('fs')) : gfs diff --git a/test/max-open.js b/test/max-open.js index 743331f..a2a2369 100644 --- a/test/max-open.js +++ b/test/max-open.js @@ -1,4 +1,4 @@ -var fs = require('../') +var fs = require('./helpers/graceful-fs.js') var test = require('tap').test test('open lots of stuff', function (t) { diff --git a/test/open.js b/test/open.js index ee0d8bc..5304e93 100644 --- a/test/open.js +++ b/test/open.js @@ -1,4 +1,4 @@ -var fs = require('../') +var fs = require('./helpers/graceful-fs.js') var test = require('tap').test test('open an existing file works', function (t) { diff --git a/test/read-write-stream.js b/test/read-write-stream.js index c6511cb..c2e5351 100644 --- a/test/read-write-stream.js +++ b/test/read-write-stream.js @@ -1,6 +1,6 @@ 'use strict' -var fs = require('../') +var fs = require('./helpers/graceful-fs.js') var rimraf = require('rimraf') var mkdirp = require('mkdirp') var test = require('tap').test diff --git a/test/readdir-options.js b/test/readdir-options.js index 07c2530..5dd9000 100644 --- a/test/readdir-options.js +++ b/test/readdir-options.js @@ -45,7 +45,7 @@ fs.readdir = function(path, options, cb) { }) } -var g = require("../") +var g = require('./helpers/graceful-fs.js') var encodings = ['buffer', 'hex', 'utf8', null] encodings.forEach(function (enc) { diff --git a/test/readdir-sort.js b/test/readdir-sort.js index 6d3ea28..4073d89 100644 --- a/test/readdir-sort.js +++ b/test/readdir-sort.js @@ -7,7 +7,7 @@ fs.readdir = function(path, cb) { }) } -var g = require("../") +var g = require('./helpers/graceful-fs.js') var test = require("tap").test test("readdir reorder", function (t) { diff --git a/test/readfile.js b/test/readfile.js index ce4f04f..d9430c8 100644 --- a/test/readfile.js +++ b/test/readfile.js @@ -1,6 +1,6 @@ 'use strict' -var fs = require('../') +var fs = require('./helpers/graceful-fs.js') var rimraf = require('rimraf') var mkdirp = require('mkdirp') var test = require('tap').test diff --git a/test/windows-rename-polyfill.js b/test/windows-rename-polyfill.js index f48ba74..d644023 100644 --- a/test/windows-rename-polyfill.js +++ b/test/windows-rename-polyfill.js @@ -9,7 +9,7 @@ fs.rename = function (a, b, cb) { }) } -var gfs = require('../') +var gfs = require('./helpers/graceful-fs.js') var t = require('tap') var a = __dirname + '/a' var b = __dirname + '/b' diff --git a/test/write-then-read.js b/test/write-then-read.js index 3a66df3..4aeded7 100644 --- a/test/write-then-read.js +++ b/test/write-then-read.js @@ -1,4 +1,4 @@ -var fs = require('../'); +var fs = require('./helpers/graceful-fs.js'); var rimraf = require('rimraf'); var mkdirp = require('mkdirp'); var test = require('tap').test; From 67dfc90a64fd170965c4cc24e3cd5182324cb7b3 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 18:05:42 -0400 Subject: [PATCH 17/34] Split win32 fs.rename patcher to separate source file * Create windows-rename-polyfill.js * Eliminate process.env.GRACEFUL_FS_PLATFORM * Modernize windows-rename-polyfill test --- package.json | 3 +- polyfills.js | 35 ++------------------ test/windows-rename-polyfill.js | 58 ++++++++++++++++++++++----------- windows-rename-polyfill.js | 39 ++++++++++++++++++++++ 4 files changed, 82 insertions(+), 53 deletions(-) create mode 100644 windows-rename-polyfill.js diff --git a/package.json b/package.json index 8fee50d..789cd26 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "clone.js", "graceful-fs.js", "normalize-args.js", - "polyfills.js" + "polyfills.js", + "windows-rename-polyfill.js" ], "dependencies": {} } diff --git a/polyfills.js b/polyfills.js index 74fac8f..23673a7 100644 --- a/polyfills.js +++ b/polyfills.js @@ -5,8 +5,6 @@ const normalizeArgs = require('./normalize-args.js') var origCwd = process.cwd var cwd = null -var platform = process.env.GRACEFUL_FS_PLATFORM || process.platform - process.cwd = function() { if (!cwd) cwd = origCwd.call(process) @@ -76,37 +74,8 @@ function patch (fs) { // failures. Also, take care to yield the scheduler. Windows scheduling gives // CPU to a busy looping process, which can cause the program causing the lock // contention to be starved of CPU by node, so the contention doesn't resolve. - if (platform === "win32") { - const accessErrors = new Set(['EACCES', 'EPERM']) - const {rename} = fs - - fs.rename = (from, to, cb) => { - const start = Date.now() - let backoff = 0 - cb = normalizeArgs([cb])[1] - - rename(from, to, function CB (er) { - if (er && accessErrors.has(er.code) && Date.now() - start < 60000) { - setTimeout(() => { - fs.stat(to, stater => { - if (stater && stater.code === 'ENOENT') { - rename(from, to, CB) - } else { - cb(er) - } - }) - }, backoff) - - if (backoff < 100) { - backoff += 10 - } - - return - } - - cb(er) - }) - } + if (process.platform === 'win32') { + require('./windows-rename-polyfill.js')(fs) } const {read, readSync} = fs diff --git a/test/windows-rename-polyfill.js b/test/windows-rename-polyfill.js index d644023..ae68ab4 100644 --- a/test/windows-rename-polyfill.js +++ b/test/windows-rename-polyfill.js @@ -1,30 +1,50 @@ -process.env.GRACEFUL_FS_PLATFORM = 'win32' - -var fs = require('fs') -fs.rename = function (a, b, cb) { - setTimeout(function () { - var er = new Error('EPERM blerg') - er.code = 'EPERM' - cb(er) - }) +'use strict' + +const path = require('path') +const fs = require('fs') +const windowsRenamePolyfill = require('../windows-rename-polyfill.js') + +function createPolyfilledObject(code) { + const pfs = { + stat: fs.stat, + rename(a, b, cb) { + /* original rename */ + cb(Object.assign(new Error(code), {code})) + } + } + + windowsRenamePolyfill(pfs) + + return pfs } -var gfs = require('./helpers/graceful-fs.js') -var t = require('tap') -var a = __dirname + '/a' -var b = __dirname + '/b' +const t = require('tap') +const a = path.join(__dirname, 'a') +const b = path.join(__dirname, 'b') +const c = path.join(__dirname, 'c') + +t.test('setup', t => { + const pfs = createPolyfilledObject('EPERM') + t.notMatch(pfs.rename.toString(), /original rename/) + + try { + fs.mkdirSync(a) + } catch (e) {} + + try { + fs.mkdirSync(b) + } catch (e) {} -t.test('setup', function (t) { - try { fs.mkdirSync(a) } catch (e) {} - try { fs.mkdirSync(b) } catch (e) {} t.end() }) -t.test('rename', { timeout: 100 }, function (t) { - t.plan(1) +t.test('rename EPERM', { timeout: 100 }, t => { + t.plan(2) - gfs.rename(a, b, function (er) { + const pfs = createPolyfilledObject('EPERM') + pfs.rename(a, b, er => { t.ok(er) + t.is(er.code, 'EPERM') }) }) diff --git a/windows-rename-polyfill.js b/windows-rename-polyfill.js new file mode 100644 index 0000000..ca356c2 --- /dev/null +++ b/windows-rename-polyfill.js @@ -0,0 +1,39 @@ +'use strict' + +const normalizeArgs = require('./normalize-args.js') + +const accessErrors = new Set(['EACCES', 'EPERM']) + +function windowsRenamePolyfill (fs) { + const {rename} = fs + + fs.rename = (from, to, cb) => { + const start = Date.now() + let backoff = 0 + cb = normalizeArgs([cb])[1] + + rename(from, to, function CB (er) { + if (er && accessErrors.has(er.code) && Date.now() - start < 60000) { + setTimeout(() => { + fs.stat(to, stater => { + if (stater && stater.code === 'ENOENT') { + rename(from, to, CB) + } else { + cb(er) + } + }) + }, backoff) + + if (backoff < 100) { + backoff += 10 + } + + return + } + + cb(er) + }) + } +} + +module.exports = windowsRenamePolyfill From b28befc85b1e909df653b04e7d8da88f9d99af60 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 18:15:00 -0400 Subject: [PATCH 18/34] Create noop.js --- noop.js | 16 ++++++++++++++++ package.json | 1 + polyfills.js | 18 ++++++++---------- 3 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 noop.js diff --git a/noop.js b/noop.js new file mode 100644 index 0000000..96b54e7 --- /dev/null +++ b/noop.js @@ -0,0 +1,16 @@ +'use strict' + +const normalizeArgs = require('./normalize-args.js') + +function noop (...args) { + const cb = normalizeArgs(args)[1] + process.nextTick(cb) +} + +function noopSync () { +} + +module.exports = { + noop, + noopSync +} diff --git a/package.json b/package.json index 789cd26..2373535 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "check-for-callback.js", "clone.js", "graceful-fs.js", + "noop.js", "normalize-args.js", "polyfills.js", "windows-rename-polyfill.js" diff --git a/polyfills.js b/polyfills.js index 23673a7..71e1f48 100644 --- a/polyfills.js +++ b/polyfills.js @@ -1,6 +1,7 @@ var constants = require('constants') const checkForCallback = require('./check-for-callback.js') const normalizeArgs = require('./normalize-args.js') +const {noop, noopSync} = require('./noop.js') var origCwd = process.cwd var cwd = null @@ -53,16 +54,13 @@ function patch (fs) { // if lchmod/lchown do not exist, then make them no-ops if (!fs.lchmod) { - fs.lchmod = function (path, mode, cb) { - if (cb) process.nextTick(cb) - } - fs.lchmodSync = function () {} + fs.lchmod = noop + fs.lchmodSync = noopSync } + if (!fs.lchown) { - fs.lchown = function (path, uid, gid, cb) { - if (cb) process.nextTick(cb) - } - fs.lchownSync = function () {} + fs.lchown = noop + fs.lchownSync = noopSync } // on Windows, A/V software can lock the directory, causing this @@ -149,8 +147,8 @@ function patch (fs) { } } else { - fs.lutimes = function (_a, _b, _c, cb) { if (cb) process.nextTick(cb) } - fs.lutimesSync = function () {} + fs.lutimes = noop + fs.lutimesSync = noopSync } } From c0da35c9bd627eabeada7738097399ee36b4c389 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 18:35:58 -0400 Subject: [PATCH 19/34] Move lutimes polyfills to separate file --- lutimes-polyfill.js | 54 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + polyfills.js | 43 +----------------------------------- 3 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 lutimes-polyfill.js diff --git a/lutimes-polyfill.js b/lutimes-polyfill.js new file mode 100644 index 0000000..1c58279 --- /dev/null +++ b/lutimes-polyfill.js @@ -0,0 +1,54 @@ +'use strict' + +const {constants} = require('fs') +const {noop, noopSync} = require('./noop.js') + +function patchLutimes (fs) { + if (typeof constants.O_SYMLINK === 'undefined') { + fs.lutimes = noop + fs.lutimesSync = noopSync + return + } + + fs.lutimes = (path, at, mt, cb) => { + fs.open(path, constants.O_SYMLINK, (er, fd) => { + if (er) { + if (cb) { + cb(er) + } + + return + } + + fs.futimes(fd, at, mt, er => { + fs.close(fd, er2 => { + if (cb) { + cb(er || er2) + } + }) + }) + }) + } + + fs.lutimesSync = (path, at, mt) => { + const fd = fs.openSync(path, constants.O_SYMLINK) + let ret + let threw = true + try { + ret = fs.futimesSync(fd, at, mt) + threw = false + } finally { + if (threw) { + try { + fs.closeSync(fd) + } catch (er) {} + } else { + fs.closeSync(fd) + } + } + + return ret + } +} + +module.exports = patchLutimes diff --git a/package.json b/package.json index 2373535..83d388b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "check-for-callback.js", "clone.js", "graceful-fs.js", + "lutimes-polyfill.js", "noop.js", "normalize-args.js", "polyfills.js", diff --git a/polyfills.js b/polyfills.js index 71e1f48..d4badf1 100644 --- a/polyfills.js +++ b/polyfills.js @@ -28,7 +28,7 @@ function patch (fs) { // lutimes implementation, or no-op if (!fs.lutimes) { - patchLutimes(fs) + require('./lutimes-polyfill.js')(fs) } // https://github.com/isaacs/node-graceful-fs/issues/4 @@ -111,47 +111,6 @@ function patch (fs) { } } - function patchLutimes (fs) { - if (constants.hasOwnProperty("O_SYMLINK")) { - fs.lutimes = function (path, at, mt, cb) { - fs.open(path, constants.O_SYMLINK, function (er, fd) { - if (er) { - if (cb) cb(er) - return - } - fs.futimes(fd, at, mt, function (er) { - fs.close(fd, function (er2) { - if (cb) cb(er || er2) - }) - }) - }) - } - - fs.lutimesSync = function (path, at, mt) { - var fd = fs.openSync(path, constants.O_SYMLINK) - var ret - var threw = true - try { - ret = fs.futimesSync(fd, at, mt) - threw = false - } finally { - if (threw) { - try { - fs.closeSync(fd) - } catch (er) {} - } else { - fs.closeSync(fd) - } - } - return ret - } - - } else { - fs.lutimes = noop - fs.lutimesSync = noopSync - } - } - function patchChownErFilter (orig) { if (!orig) { return orig From 01fdd0b10b52936a56f35c9ca15951590d27fbb4 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 20:27:16 -0400 Subject: [PATCH 20/34] Collect code coverage --- nyc.config.js | 16 ++++++++++++++++ package.json | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 nyc.config.js diff --git a/nyc.config.js b/nyc.config.js new file mode 100644 index 0000000..7c500d8 --- /dev/null +++ b/nyc.config.js @@ -0,0 +1,16 @@ +'use strict' + +const {constants} = require('fs') +const glob = require('glob') + +module.exports = { + all: true, + lines: 100, + statements: 100, + functions: 100, + branches: 100, + include: glob.sync('*.js', { + cwd: __dirname, + ignore: 'O_SYMLINK' in constants ? [] : ['lutimes-polyfill.js'] + }) +} diff --git a/package.json b/package.json index 83d388b..e0ced1d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "preversion": "npm test", "postversion": "npm publish", "postpublish": "git push origin --follow-tags", - "test": "node test.js | tap -" + "test": "nyc -r none node test.js | tap -", + "posttest": "nyc report -r text" }, "keywords": [ "fs", @@ -34,8 +35,10 @@ ], "license": "ISC", "devDependencies": { + "glob": "^7.1.4", "import-fresh": "^3.1.0", "mkdirp": "^0.5.1", + "nyc": "^14.1.1", "rimraf": "^2.6.3", "tap": "^14.6.1" }, From 6d8dae1f4a7aa5cd417bcc679161edbe01847c6a Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 7 Aug 2019 20:28:13 -0400 Subject: [PATCH 21/34] Full coverage --- .travis.yml | 3 +- check-for-callback.js | 2 +- graceful-fs.js | 1 + lutimes-polyfill.js | 13 ++-- package.json | 2 +- polyfills.js | 9 ++- test.js | 4 +- test/close.js | 12 ++++ test/eagain.js | 84 +++++++++++++++++++++++++ test/enoent.js | 4 +- test/lutimes.js | 78 +++++++++++++++++++++++ test/noop.js | 46 ++++++++++++++ test/normalize-args-v10.js | 27 ++++++++ test/normalize-args-v8.js | 41 ++++++++++++ test/read-write-stream.js | 107 ++++++++++++++++++++++++++++++-- test/windows-rename-polyfill.js | 40 ++++++++++++ 16 files changed, 452 insertions(+), 21 deletions(-) create mode 100644 test/eagain.js create mode 100644 test/lutimes.js create mode 100644 test/noop.js create mode 100644 test/normalize-args-v10.js create mode 100644 test/normalize-args-v8.js diff --git a/.travis.yml b/.travis.yml index dbbbc7b..c10d2c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,9 @@ node_js: - 8 os: - - linux - windows + - osx + - linux cache: directories: diff --git a/check-for-callback.js b/check-for-callback.js index 4a78589..54a2785 100644 --- a/check-for-callback.js +++ b/check-for-callback.js @@ -34,7 +34,7 @@ function checkForCallback (cb, ctor) { 'Calling an asynchronous function without callback is deprecated.', 'DeprecationWarning', 'DEP0013', - ctor || checkForCallback + ctor ) return false } diff --git a/graceful-fs.js b/graceful-fs.js index 421d84a..6e9b4ea 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -51,6 +51,7 @@ if (!global[gracefulQueue]) { } fs.closeSync[previous] = closeSync + /* istanbul ignore next */ if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { process.on('exit', () => { debug(global[gracefulQueue]) diff --git a/lutimes-polyfill.js b/lutimes-polyfill.js index 1c58279..34d0671 100644 --- a/lutimes-polyfill.js +++ b/lutimes-polyfill.js @@ -2,8 +2,10 @@ const {constants} = require('fs') const {noop, noopSync} = require('./noop.js') +const normalizeArgs = require('./normalize-args.js') function patchLutimes (fs) { + /* istanbul ignore if: coverage for this file is ignored if O_SYMLINK is not defined. */ if (typeof constants.O_SYMLINK === 'undefined') { fs.lutimes = noop fs.lutimesSync = noopSync @@ -11,20 +13,17 @@ function patchLutimes (fs) { } fs.lutimes = (path, at, mt, cb) => { + cb = normalizeArgs([cb])[1] + fs.open(path, constants.O_SYMLINK, (er, fd) => { if (er) { - if (cb) { - cb(er) - } - + cb(er) return } fs.futimes(fd, at, mt, er => { fs.close(fd, er2 => { - if (cb) { - cb(er || er2) - } + cb(er || er2) }) }) }) diff --git a/package.json b/package.json index e0ced1d..8e870c4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "postversion": "npm publish", "postpublish": "git push origin --follow-tags", "test": "nyc -r none node test.js | tap -", - "posttest": "nyc report -r text" + "posttest": "nyc report -r text --check-coverage" }, "keywords": [ "fs", diff --git a/polyfills.js b/polyfills.js index d4badf1..d266bd1 100644 --- a/polyfills.js +++ b/polyfills.js @@ -1,5 +1,3 @@ -var constants = require('constants') -const checkForCallback = require('./check-for-callback.js') const normalizeArgs = require('./normalize-args.js') const {noop, noopSync} = require('./noop.js') @@ -53,11 +51,13 @@ function patch (fs) { fs.lchmodSync = patchChownSyncErFilter(fs.lchmodSync) // if lchmod/lchown do not exist, then make them no-ops + /* istanbul ignore next */ if (!fs.lchmod) { fs.lchmod = noop fs.lchmodSync = noopSync } + /* istanbul ignore next */ if (!fs.lchown) { fs.lchown = noop fs.lchownSync = noopSync @@ -72,6 +72,7 @@ function patch (fs) { // failures. Also, take care to yield the scheduler. Windows scheduling gives // CPU to a busy looping process, which can cause the program causing the lock // contention to be starved of CPU by node, so the contention doesn't resolve. + /* istanbul ignore next */ if (process.platform === 'win32') { require('./windows-rename-polyfill.js')(fs) } @@ -79,7 +80,7 @@ function patch (fs) { const {read, readSync} = fs // if read() returns EAGAIN, then just try it again. fs.read = (fd, buffer, offset, length, position, cb) => { - checkForCallback(cb) + cb = normalizeArgs([cb])[1] let eagCounter = 0 read(fd, buffer, offset, length, position, function CB (er, ...args) { @@ -112,6 +113,7 @@ function patch (fs) { } function patchChownErFilter (orig) { + /* istanbul ignore if */ if (!orig) { return orig } @@ -123,6 +125,7 @@ function patch (fs) { } function patchChownSyncErFilter (orig) { + /* istanbul ignore if */ if (!orig) { return orig } diff --git a/test.js b/test.js index a0311cd..118ff83 100644 --- a/test.js +++ b/test.js @@ -14,11 +14,11 @@ var env = Object.keys(process.env).reduce(function (env, k) { files.filter(function (f) { if (/\.js$/.test(f) && fs.statSync(dir + '/' + f).isFile()) { // expose-gc is so we can check for memory leaks - tap.spawn(node, ['--expose-gc', 'test/' + f]) + tap.spawn(node, ['--no-warnings', '--expose-gc', 'test/' + f]) return true } }).forEach(function (f) { - tap.spawn(node, ['--expose-gc', 'test/' + f], { + tap.spawn(node, ['--no-warnings', '--expose-gc', 'test/' + f], { env: env }, '🐵 test/' + f) }) diff --git a/test/close.js b/test/close.js index 9041cfd..13c3f36 100644 --- a/test/close.js +++ b/test/close.js @@ -21,3 +21,15 @@ test('`close` is patched correctly', function(t) { t.equal(newGFS.closeSync, fs$closeSync) t.end(); }) + +test('close error', t => { + /* Open and close an fd to test fs.close / fs.closeSync errors */ + const fd = fs.openSync(__filename, 'r') + gfs.closeSync(fd) + + t.throws(() => gfs.closeSync(fd), { code: 'EBADF' }) + gfs.close(fd, err => { + t.ok(err && err.code === 'EBADF') + t.end() + }) +}) diff --git a/test/eagain.js b/test/eagain.js new file mode 100644 index 0000000..456e838 --- /dev/null +++ b/test/eagain.js @@ -0,0 +1,84 @@ +'use strict' + +const {test} = require('tap') + +const eagain = () => Object.assign(new Error('EAGAIN'), {code: 'EAGAIN'}) + +let readPing +let readSyncPing +let cbArgs = [] + +// We can't hijack the actual `fs` module so we have to fake it +const fs = require('../graceful-fs.js').gracefulify({ + ...require('fs'), + read (...args) { + const cb = args.slice(-1)[0] + readPing(args) + + cb(...cbArgs) + }, + readSync (...args) { + return readSyncPing(...args) + } +}) + +test('read unresolved EAGAIN', t => { + let counter = 0 + readPing = () => { + counter++ + } + cbArgs = [eagain()] + + fs.read(null, null, 0, 0, 0, err => { + t.ok(err && err.code === 'EAGAIN', 'unresolved eagain') + t.is(counter, 11) + t.end() + }) +}) + +test('read EAGAIN loop', t => { + let counter = 0 + + cbArgs = [eagain()] + readPing = () => { + counter++ + if (counter === 5) { + cbArgs = [null, 'success'] + } + } + + fs.read(null, null, 0, 0, 0, (err, msg) => { + t.is(counter, 5, 'retried 5 times') + t.notOk(err) + t.is(msg, 'success', 'resolved after retries') + t.end() + }) +}) + +test('readSync unresolved EAGAIN', t => { + let counter = 0 + readSyncPing = () => { + counter++ + throw eagain() + } + + t.throws(() => fs.readSync(), {code: 'EAGAIN'}) + t.is(counter, 11) + t.end() +}) + +test('readSync unresolved EAGAIN', t => { + let counter = 0 + readSyncPing = () => { + counter++ + if (counter !== 5) { + throw eagain() + } + + return 'success' + } + + t.is(fs.readSync(), 'success') + t.is(counter, 5) + t.end() +}) diff --git a/test/enoent.js b/test/enoent.js index 3b725c9..1e38a6c 100644 --- a/test/enoent.js +++ b/test/enoent.js @@ -8,7 +8,9 @@ var methods = [ ['open', 'r'], ['readFile'], ['utimes', new Date(), new Date()], - ['readdir'] + ['readdir'], + ['chown', 0, 0], + ['chmod', 0] ] // any version > v6 can do readdir(path, options, cb) diff --git a/test/lutimes.js b/test/lutimes.js new file mode 100644 index 0000000..19b3034 --- /dev/null +++ b/test/lutimes.js @@ -0,0 +1,78 @@ +'use strict' + +const path = require('path') +const mkdirp = require('mkdirp') +const rimraf = require('rimraf') +const {test} = require('tap') +const fs = require('./helpers/graceful-fs.js') + +const dir = path.resolve(__dirname, 'files') +const ln = path.resolve(dir, 'symlink') + +if (!('O_SYMLINK' in fs.constants)) { + test('stubs', t => { + fs.lutimes(ln, 0, 0, () => {}) + fs.lutimesSync(ln, 0, 0) + t.end() + }) + + // nothing to test on this platform + process.exit(0) +} + +test('setup', t => { + mkdirp.sync(dir) + t.end() +}) + +test('lutimes', t => { + fs.symlinkSync(__filename, ln) + fs.lutimesSync(ln, 0, 0) + + let stat = fs.lstatSync(ln) + t.is(stat.atimeMs, 0) + t.is(stat.mtimeMs, 0) + + fs.lutimes(ln, 1, 1, er => { + t.notOk(er) + stat = fs.lstatSync(ln) + t.is(stat.atimeMs, 1000) + t.is(stat.mtimeMs, 1000) + + fs.unlinkSync(ln) + t.end() + }) +}) + +test('futimes error', t => { + const error = new Error('test error') + + fs.symlinkSync(__filename, ln) + + fs.futimes = (fd, at, mt, cb) => cb(error) + fs.futimesSync = () => { + throw error + } + + t.throws(() => fs.lutimesSync(ln, 0, 0), error, 'futimesSync error') + + fs.lutimes(ln, 1, 1, er => { + t.is(er, error) + + fs.unlinkSync(ln) + t.end() + }) +}) + +test('lutimes open error', t => { + fs.lutimes(ln, 1, 1, er => { + t.match(er, {code: 'ENOENT'}) + + t.end() + }) +}) + +test('cleanup', t => { + rimraf.sync(dir) + t.end() +}) diff --git a/test/noop.js b/test/noop.js new file mode 100644 index 0000000..4ae929d --- /dev/null +++ b/test/noop.js @@ -0,0 +1,46 @@ +'use strict' + +const {test} = require('tap') +const {noop, noopSync} = require('../noop.js') + +test('noopSync', t => { + noopSync() + noopSync('a', 'b', 'c', 'd', 'e', () => t.fail('should not run callback')) + + // Wait a few ticks + setTimeout(() => { + t.end() + }, 50) +}) + +test('noop', t => { + const data = { + cbOnly: 0, + withArgs: 0 + } + + t.doesNotThrow(() => { + noop((er, ...args) => { + t.notOk(er) + t.is(args.length, 0) + data.cbOnly++ + }) + }, 'cb only') + t.is(data.cbOnly, 0, 'callback is not sync') + + t.doesNotThrow(() => { + noop('a', 'b', 'c', 'd', (er, ...args) => { + t.notOk(er) + t.is(args.length, 0) + data.withArgs++ + }) + }, 'with args') + t.is(data.withArgs, 0, 'callback is not sync') + + // Wait a few ticks + setTimeout(() => { + t.is(data.cbOnly, 1) + t.is(data.withArgs, 1) + t.end() + }, 50) +}) diff --git a/test/normalize-args-v10.js b/test/normalize-args-v10.js new file mode 100644 index 0000000..a6f239e --- /dev/null +++ b/test/normalize-args-v10.js @@ -0,0 +1,27 @@ +'use strict' + +Object.defineProperty(process.versions, 'node', {value: '10.0.0'}) + +const {test} = require('tap') +const normalizeArgs = require('../normalize-args.js') + +const stackTracer = (...args) => normalizeArgs(args) + +test('throw on no callback', t => { + const matchError = { + code: 'ERR_INVALID_CALLBACK', + name: 'TypeError', + message: 'Callback must be a function. Received undefined', + stack: /^TypeError \[ERR_INVALID_CALLBACK\]: Callback must be a function. Received undefined\n\s*at stackTracer/ + } + + t.throws(() => stackTracer([]), matchError) + t.throws(() => stackTracer(['blue', 'green']), matchError) + + const newCB = () => {} + const result = stackTracer('blue', 'green', newCB) + t.deepEqual(result[0], ['blue', 'green']) + t.is(result[1], newCB) + + t.end() +}) diff --git a/test/normalize-args-v8.js b/test/normalize-args-v8.js new file mode 100644 index 0000000..051c741 --- /dev/null +++ b/test/normalize-args-v8.js @@ -0,0 +1,41 @@ +'use strict' + +Object.defineProperty(process.versions, 'node', {value: '8.0.0'}) + +const {test} = require('tap') +const normalizeArgs = require('../normalize-args.js') + +const stackTracer = (...args) => normalizeArgs(args) + +test('warns on no callback', t => { + let hits = 0 + process.on('warning', warning => { + hits++ + t.match(warning, { + name: 'DeprecationWarning', + message: 'Calling an asynchronous function without callback is deprecated.', + code: 'DEP0013', + stack: /^DeprecationWarning: Calling an asynchronous function without callback is deprecated\.\n\s*at stackTracer/ + }, `warning ${hits}`) + }) + + let result = stackTracer() + t.deepEqual(result[0], []) + t.type(result[1], 'function') + t.notThrow(() => result[1]()) + t.notThrow(() => result[1](new Error('ignored'))) + + result = stackTracer('blue', 'green') + t.deepEqual(result[0], ['blue', 'green']) + t.type(result[1], 'function') + + const newCB = () => {} + result = stackTracer('blue', 'green', newCB) + t.deepEqual(result[0], ['blue', 'green']) + t.is(result[1], newCB) + + process.nextTick(() => { + t.is(hits, 2) + t.end() + }) +}) diff --git a/test/read-write-stream.js b/test/read-write-stream.js index c2e5351..fe45236 100644 --- a/test/read-write-stream.js +++ b/test/read-write-stream.js @@ -4,7 +4,8 @@ var fs = require('./helpers/graceful-fs.js') var rimraf = require('rimraf') var mkdirp = require('mkdirp') var test = require('tap').test -var p = require('path').resolve(__dirname, 'files') +var path = require('path') +var p = path.resolve(__dirname, 'files') process.chdir(__dirname) @@ -18,10 +19,22 @@ test('write files', function (t) { rimraf.sync(p) mkdirp.sync(p) - t.plan(num) + t.plan(num * 2) for (var i = 0; i < num; ++i) { paths[i] = 'files/file-' + i - var stream = fs.createWriteStream(paths[i]) + let stream + switch (i % 3) { + case 0: + stream = fs.createWriteStream(paths[i]) + break + case 1: + stream = fs.WriteStream(paths[i]) + break + case 2: + stream = new fs.WriteStream(paths[i]) + break + } + t.type(stream, fs.WriteStream) stream.on('finish', function () { t.pass('success') }) @@ -32,9 +45,21 @@ test('write files', function (t) { test('read files', function (t) { // now read them - t.plan(num) + t.plan(num * 2) for (var i = 0; i < num; ++i) (function (i) { - var stream = fs.createReadStream(paths[i]) + let stream + switch (i % 3) { + case 0: + stream = fs.createReadStream(paths[i]) + break + case 1: + stream = fs.ReadStream(paths[i]) + break + case 2: + stream = new fs.ReadStream(paths[i]) + break + } + t.type(stream, fs.ReadStream) var data = '' stream.on('data', function (c) { data += c @@ -45,6 +70,78 @@ test('read files', function (t) { })(i) }) +function streamErrors(t, read, autoClose) { + const events = [] + const initializer = read ? 'createReadStream' : 'createWriteStream' + const stream = fs[initializer]( + path.join(__dirname, 'this dir does not exist', 'filename'), + {autoClose} + ) + const matchDestroy = autoClose ? ['destroy'] : ['error', 'destroy'] + const matchError = autoClose ? ['destroy', 'error'] : ['error'] + const destroy = stream.destroy + stream.destroy = () => { + events.push('destroy') + t.deepEqual(events, matchDestroy, 'got destroy') + destroy.call(stream) + } + + stream.on('error', () => { + events.push('error') + t.deepEqual(events, matchError, 'got error') + if (!autoClose) { + stream.destroy() + } + + setTimeout(() => t.end(), 50) + }) +} + +test('read error autoClose', t => streamErrors(t, true, true)) +test('read error no autoClose', t => streamErrors(t, true, false)) +test('write error autoClose', t => streamErrors(t, false, true)) +test('write error no autoClose', t => streamErrors(t, false, false)) + +test('ReadStream replacement', t => { + const testArgs = [__filename, {}] + let called = 0 + + class FakeReplacement { + constructor(...args) { + t.deepEqual(args, testArgs) + called++ + } + } + + const {ReadStream} = fs + fs.ReadStream = FakeReplacement + const rs = fs.createReadStream(...testArgs) + fs.ReadStream = ReadStream + t.type(rs, FakeReplacement) + t.is(called, 1) + t.end() +}) + +test('WriteStream replacement', t => { + const testArgs = [__filename, {}] + let called = 0 + + class FakeReplacement { + constructor(...args) { + t.deepEqual(args, testArgs) + called++ + } + } + + const {WriteStream} = fs + fs.WriteStream = FakeReplacement + const rs = fs.createWriteStream(...testArgs) + fs.WriteStream = WriteStream + t.type(rs, FakeReplacement) + t.is(called, 1) + t.end() +}) + test('cleanup', function (t) { rimraf.sync(p) t.end() diff --git a/test/windows-rename-polyfill.js b/test/windows-rename-polyfill.js index ae68ab4..e0da6e8 100644 --- a/test/windows-rename-polyfill.js +++ b/test/windows-rename-polyfill.js @@ -48,6 +48,46 @@ t.test('rename EPERM', { timeout: 100 }, t => { }) }) +t.test('rename EACCES', { timeout: 100 }, t => { + t.plan(2) + + const pfs = createPolyfilledObject('EACCES') + pfs.rename(a, b, er => { + t.ok(er) + t.is(er.code, 'EACCES') + }) +}) + +t.test('rename ENOENT', { timeout: 100 }, t => { + t.plan(2) + + const pfs = createPolyfilledObject('ENOENT') + pfs.rename(a, b, er => { + t.ok(er) + t.is(er.code, 'ENOENT') + }) +}) + +t.test('rename EPERM then stat ENOENT', { timeout: 2000 }, t => { + t.plan(3) + + const pfs = createPolyfilledObject('EPERM') + let enoent = 12 + pfs.stat = (p, cb) => { + if (--enoent) { + cb(Object.assign(new Error('ENOENT'), {code: 'ENOENT'})) + } else { + fs.stat(p, cb) + } + } + + pfs.rename(a, b, er => { + t.notOk(enoent) + t.ok(er) + t.is(er.code, 'EPERM') + }) +}) + t.test('cleanup', function (t) { try { fs.rmdirSync(a) } catch (e) {} try { fs.rmdirSync(b) } catch (e) {} From 8853947a835d64e78199679ff8126ffcfc8927a5 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Thu, 8 Aug 2019 13:56:37 -0400 Subject: [PATCH 22/34] Modernize tests, make style consistent --- test.js | 37 ++++++++---------- test/avoid-memory-leak.js | 52 +++++++++++++------------- test/chdir.js | 15 ++++++++ test/chown-er-ok.js | 66 +++++++++++++++------------------ test/close.js | 41 ++++++++++---------- test/enoent.js | 56 ++++++++++++---------------- test/max-open.js | 55 ++++++++++++++++----------- test/open.js | 44 +++++++++++----------- test/read-write-stream.js | 55 +++++++++++++-------------- test/readdir-options.js | 47 +++++++++++------------ test/readdir-sort.js | 25 +++++++------ test/readfile.js | 39 +++++++++---------- test/windows-rename-polyfill.js | 26 ++++++++----- test/write-then-read.js | 52 ++++++++++++-------------- 14 files changed, 307 insertions(+), 303 deletions(-) create mode 100644 test/chdir.js diff --git a/test.js b/test.js index 118ff83..05d2e02 100644 --- a/test.js +++ b/test.js @@ -1,24 +1,19 @@ -var fs = require('fs') -var tap = require('tap') -var dir = __dirname + '/test' -var node = process.execPath +'use strict' -var files = fs.readdirSync(dir) -var env = Object.keys(process.env).reduce(function (env, k) { - env[k] = process.env[k] - return env -}, { +const path = require('path') +const tap = require('tap') +const glob = require('glob') + +const node = process.execPath +const env = { + ...process.env, TEST_GFS_GLOBAL_PATCH: '1' -}) +} +const files = glob.sync('*.js', {cwd: path.join(__dirname, 'test')}) + .map(f => path.join('test', f)) -files.filter(function (f) { - if (/\.js$/.test(f) && fs.statSync(dir + '/' + f).isFile()) { - // expose-gc is so we can check for memory leaks - tap.spawn(node, ['--no-warnings', '--expose-gc', 'test/' + f]) - return true - } -}).forEach(function (f) { - tap.spawn(node, ['--no-warnings', '--expose-gc', 'test/' + f], { - env: env - }, '🐵 test/' + f) -}) +for (const f of files) { + const args = ['--no-warnings', '--expose-gc', f] + tap.spawn(node, args, {}, f) + tap.spawn(node, args, {env}, `${f} [🐵]`) +} diff --git a/test/avoid-memory-leak.js b/test/avoid-memory-leak.js index 9e26655..04ffdfa 100644 --- a/test/avoid-memory-leak.js +++ b/test/avoid-memory-leak.js @@ -1,49 +1,51 @@ -var path = require('path') -var importFresh = require('import-fresh'); -var t = require('tap') -var v8 -try { - v8 = require('v8') -} catch (er) {} +'use strict' -var previousHeapStats +const path = require('path') +const v8 = require('v8') +const importFresh = require('import-fresh') +const t = require('tap') + +let previousHeapStats function checkHeap (t) { - var v8stats = v8 ? v8.getHeapStatistics() : {} - var stats = process.memoryUsage() - if (typeof v8stats.number_of_detached_contexts === 'number') + const v8stats = v8.getHeapStatistics() + const stats = process.memoryUsage() + if (typeof v8stats.number_of_detached_contexts === 'number') { t.equal(v8stats.number_of_detached_contexts, 0, 'no detached contexts') - else { - const memoryUsage = stats.heapUsed - previousHeapStats.heapUsed - const memoryUsageMB = Math.round(memoryUsage / Math.pow(1024, 2)) - t.ok(memoryUsageMB < 2, 'expect less than 2MB difference, ' - + memoryUsageMB + 'MB difference found.'); } + + const memoryUsage = stats.heapUsed - previousHeapStats.heapUsed + const memoryUsageKB = Math.round(memoryUsage / 1024) + t.ok( + memoryUsageKB < 2048, + `expect less than 2048KB difference, ${memoryUsageKB}KB difference found.` + ) } -t.test('no memory leak when loading multiple times', function(t) { +t.test('no memory leak when loading multiple times', t => { const gfsPath = path.resolve(__dirname, '../graceful-fs.js') const gfsHelper = path.join(__dirname, './helpers/graceful-fs.js') importFresh(gfsHelper) - t.plan(1); + global.gc() previousHeapStats = process.memoryUsage() // simulate project with 4000 tests - var i = 0; - function importFreshGracefulFs() { + let i = 0 + function importFreshGracefulFs () { delete require.cache[gfsPath] // We have to use absolute path because importFresh cannot find // relative paths when run from the callback of `process.nextTick`. importFresh(gfsHelper) if (i < 4000) { - i++; - process.nextTick(() => importFreshGracefulFs()); + i++ + process.nextTick(() => importFreshGracefulFs()) } else { global.gc() - checkHeap(t); - t.end(); + checkHeap(t) + t.end() } } - importFreshGracefulFs(); + + importFreshGracefulFs() }) diff --git a/test/chdir.js b/test/chdir.js new file mode 100644 index 0000000..f7544ea --- /dev/null +++ b/test/chdir.js @@ -0,0 +1,15 @@ +'use strict' + +const path = require('path') +const t = require('tap') + +// Side-effect only to force replacement of process.chdir / process.cwd +require('./helpers/graceful-fs.js') + +const project = path.resolve(__dirname, '..') + +process.chdir(__dirname) +t.is(process.cwd(), __dirname, 'chdir(__dirname) worked') +process.chdir(project) +t.is(process.cwd(), project, 'chdir(project) worked') +t.end() diff --git a/test/chown-er-ok.js b/test/chown-er-ok.js index 1447d76..a377178 100644 --- a/test/chown-er-ok.js +++ b/test/chown-er-ok.js @@ -1,53 +1,47 @@ -var realFs = require('fs') +'use strict' -var methods = ['chown', 'chownSync', 'chmod', 'chmodSync'] -methods.forEach(function (method) { - causeErr(method, realFs[method]) -}) +const realFs = require('fs') -function causeErr (method, original) { - realFs[method] = function (path) { - var err = makeErr(path, method) - if (!/Sync$/.test(method)) { - var cb = arguments[arguments.length - 1] - process.nextTick(cb.bind(null, err)) - } else { - throw err - } +// For the fchown / fchmod do not accept `path` as the first parameter but +// gfs doesn't duplicate this check so we can still verify that the errors +// are ignored without added complexity in this test +const methods = ['chown', 'fchown', 'lchown', 'chmod', 'fchmod', 'lchmod'] +methods.forEach(method => { + realFs[`${method}Sync`] = path => { + throw makeErr(path, `${method}Sync`) } -} + + realFs[method] = (path, ...args) => { + const cb = args.pop() + const err = makeErr(path, method) + process.nextTick(() => cb(err)) + } +}) function makeErr (path, method) { - var err = new Error('this is fine') - err.syscall = method.replace(/Sync$/, '') - err.code = path.toUpperCase() - return err + return Object.assign(new Error('this is fine'), { + syscall: method.replace(/Sync$/, ''), + code: path.toUpperCase() + }) } -var fs = require('./helpers/graceful-fs.js') -var t = require('tap') +const fs = require('./helpers/graceful-fs.js') +const t = require('tap') -var errs = ['ENOSYS', 'EINVAL', 'EPERM'] -t.plan(errs.length * methods.length) +const errs = ['ENOSYS', 'EINVAL', 'EPERM'] +t.plan(errs.length * methods.length * 2) -errs.forEach(function (err) { - methods.forEach(function (method) { - var args = [err] +errs.forEach(err => { + methods.forEach(method => { + const args = [err] if (/chmod/.test(method)) { args.push('some mode') } else { args.push('some uid', 'some gid') } - if (method.match(/Sync$/)) { - t.doesNotThrow(function () { - fs[method].apply(fs, args) - }) - } else { - args.push(function (err) { - t.notOk(err) - }) - fs[method].apply(fs, args) - } + t.doesNotThrow(() => fs[`${method}Sync`](...args), `${method}Sync does not throw ${err}`) + args.push(e => t.notOk(e, `${method} does not throw ${err}`)) + fs[method](...args) }) }) diff --git a/test/close.js b/test/close.js index 13c3f36..f0315f7 100644 --- a/test/close.js +++ b/test/close.js @@ -1,25 +1,26 @@ -var fs = require('fs') -var path = require('path') -var gfs = require('./helpers/graceful-fs.js') -var importFresh = require('import-fresh') -var fs$close = fs.close -var fs$closeSync = fs.closeSync -var test = require('tap').test +'use strict' -var gfsPath = path.resolve(__dirname, '..', 'graceful-fs.js') +const fs = require('fs') +const path = require('path') +const importFresh = require('import-fresh') +const gfs = require('./helpers/graceful-fs.js') +const {test} = require('tap') -test('`close` is patched correctly', function(t) { - t.match(fs$close.toString(), /graceful-fs shared queue/, 'patch fs.close'); - t.match(fs$closeSync.toString(), /graceful-fs shared queue/, 'patch fs.closeSync'); - t.match(gfs.close.toString(), /graceful-fs shared queue/, 'patch gfs.close'); - t.match(gfs.closeSync.toString(), /graceful-fs shared queue/, 'patch gfs.closeSync'); +const {close, closeSync} = fs +const gfsPath = path.resolve(__dirname, '..', 'graceful-fs.js') - var newGFS = importFresh(gfsPath) - t.equal(fs.close, fs$close) - t.equal(fs.closeSync, fs$closeSync) - t.equal(newGFS.close, fs$close) - t.equal(newGFS.closeSync, fs$closeSync) - t.end(); +test('`close` is patched correctly', t => { + t.match(close.toString(), /graceful-fs shared queue/, 'patch fs.close') + t.match(closeSync.toString(), /graceful-fs shared queue/, 'patch fs.closeSync') + t.match(gfs.close.toString(), /graceful-fs shared queue/, 'patch gfs.close') + t.match(gfs.closeSync.toString(), /graceful-fs shared queue/, 'patch gfs.closeSync') + + const newGFS = importFresh(gfsPath) + t.equal(fs.close, close) + t.equal(fs.closeSync, closeSync) + t.equal(newGFS.close, close) + t.equal(newGFS.closeSync, closeSync) + t.end() }) test('close error', t => { @@ -27,7 +28,7 @@ test('close error', t => { const fd = fs.openSync(__filename, 'r') gfs.closeSync(fd) - t.throws(() => gfs.closeSync(fd), { code: 'EBADF' }) + t.throws(() => gfs.closeSync(fd), {code: 'EBADF'}) gfs.close(fd, err => { t.ok(err && err.code === 'EBADF') t.end() diff --git a/test/enoent.js b/test/enoent.js index 1e38a6c..a3122ed 100644 --- a/test/enoent.js +++ b/test/enoent.js @@ -1,46 +1,38 @@ +'use strict' + // this test makes sure that various things get enoent, instead of // some other kind of throw. -var g = require('./helpers/graceful-fs.js') -var t = require('tap') -var file = 'this file does not exist even a little bit' -var methods = [ +const g = require('./helpers/graceful-fs.js') +const t = require('tap') + +const file = 'this file does not exist even a little bit' +const methods = [ ['open', 'r'], ['readFile'], ['utimes', new Date(), new Date()], ['readdir'], + ['readdir', {}], ['chown', 0, 0], ['chmod', 0] ] -// any version > v6 can do readdir(path, options, cb) -if (process.version.match(/^v([6-9]|[1-9][0-9])\./)) { - methods.push(['readdir', {}]) -} - t.plan(methods.length) -methods.forEach(function (method) { - t.test(method[0], runTest(method)) -}) +methods.forEach(([method, ...args]) => { + t.test(method, t => { + const methodSync = `${method}Sync` + t.isa(g[method], 'function') + t.isa(g[methodSync], 'function') -function runTest (args) { return function (t) { - var method = args.shift() - args.unshift(file) - var methodSync = method + 'Sync' - t.isa(g[methodSync], 'function') - t.throws(function () { - g[methodSync].apply(g, args) - }, { code: 'ENOENT' }) - // add the callback - args.push(verify(t)) - t.isa(g[method], 'function') - t.doesNotThrow(function () { - g[method].apply(g, args) - }) -}} + args.unshift(file) + t.throws(() => g[methodSync](...args), {code: 'ENOENT'}) -function verify (t) { return function (er) { - t.isa(er, Error) - t.equal(er.code, 'ENOENT') - t.end() -}} + // add the callback + args.push(er => { + t.isa(er, Error) + t.is(er.code, 'ENOENT') + t.end() + }) + t.doesNotThrow(() => g[method](...args)) + }) +}) diff --git a/test/max-open.js b/test/max-open.js index a2a2369..d603558 100644 --- a/test/max-open.js +++ b/test/max-open.js @@ -1,42 +1,51 @@ -var fs = require('./helpers/graceful-fs.js') -var test = require('tap').test +'use strict' -test('open lots of stuff', function (t) { +const fs = require('./helpers/graceful-fs.js') +const {test} = require('tap') + +test('open lots of stuff', t => { // Get around EBADF from libuv by making sure that stderr is opened // Otherwise Darwin will refuse to give us a FD for stderr! process.stderr.write('') // How many parallel open()'s to do - var n = 1024 - var opens = 0 - var fds = [] - var going = true - var closing = false - var doneCalled = 0 + const n = 1024 + let opens = 0 + const fds = [] + let going = true + let closing = false + let doneCalled = 0 - for (var i = 0; i < n; i++) { + for (let i = 0; i < n; i++) { go() } - function go() { + function go () { opens++ - fs.open(__filename, 'r', function (er, fd) { - if (er) throw er + fs.open(__filename, 'r', (er, fd) => { + if (er) { + throw er + } + fds.push(fd) - if (going) go() + if (going) { + go() + } }) } // should hit ulimit pretty fast - setTimeout(function () { + setTimeout(() => { going = false t.equal(opens - fds.length, n) done() }, 100) - function done () { - if (closing) return + if (closing) { + return + } + doneCalled++ if (fds.length === 0) { @@ -50,18 +59,20 @@ test('open lots of stuff', function (t) { } closing = true - setTimeout(function () { + setTimeout(() => { // console.error('do closing again') closing = false done() }, 100) // console.error('closing time') - var closes = fds.slice(0) + const closes = fds.slice(0) fds.length = 0 - closes.forEach(function (fd) { - fs.close(fd, function (er) { - if (er) throw er + closes.forEach(fd => { + fs.close(fd, er => { + if (er) { + throw er + } }) }) } diff --git a/test/open.js b/test/open.js index 5304e93..be41a84 100644 --- a/test/open.js +++ b/test/open.js @@ -1,34 +1,36 @@ -var fs = require('./helpers/graceful-fs.js') -var test = require('tap').test +'use strict' -test('open an existing file works', function (t) { - var fd = fs.openSync(__filename, 'r') +const fs = require('./helpers/graceful-fs.js') +const {test} = require('tap') + +test('open an existing file works', t => { + const fd = fs.openSync(__filename, 'r') fs.closeSync(fd) - fs.open(__filename, 'r', function (er, fd) { - if (er) throw er - fs.close(fd, function (er) { - if (er) throw er + fs.open(__filename, 'r', (er, fd) => { + if (er) { + throw er + } + + fs.close(fd, er => { + if (er) { + throw er + } + t.pass('works') t.end() }) }) }) -test('open a non-existing file throws', function (t) { - var er - try { - var fd = fs.openSync('this file does not exist', 'r') - } catch (x) { - er = x - } - t.ok(er, 'should throw') - t.notOk(fd, 'should not get an fd') - t.equal(er.code, 'ENOENT') +test('open a non-existing file throws', t => { + t.throws( + () => fs.openSync('this file does not exist', 'r'), + {code: 'ENOENT'} + ) - fs.open('neither does this file', 'r', function (er, fd) { - t.ok(er, 'should throw') + fs.open('neither does this file', 'r', (er, fd) => { + t.ok(er && er.code === 'ENOENT', 'should throw ENOENT') t.notOk(fd, 'should not get an fd') - t.equal(er.code, 'ENOENT') t.end() }) }) diff --git a/test/read-write-stream.js b/test/read-write-stream.js index fe45236..07d41ba 100644 --- a/test/read-write-stream.js +++ b/test/read-write-stream.js @@ -1,27 +1,24 @@ 'use strict' -var fs = require('./helpers/graceful-fs.js') -var rimraf = require('rimraf') -var mkdirp = require('mkdirp') -var test = require('tap').test -var path = require('path') -var p = path.resolve(__dirname, 'files') +const path = require('path') +const fs = require('./helpers/graceful-fs.js') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const {test} = require('tap') -process.chdir(__dirname) +const p = path.resolve(__dirname, 'files-read-write-stream') // Make sure to reserve the stderr fd process.stderr.write('') -var num = 4097 -var paths = new Array(num) +const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) -test('write files', function (t) { +test('write files', t => { rimraf.sync(p) mkdirp.sync(p) - t.plan(num * 2) - for (var i = 0; i < num; ++i) { - paths[i] = 'files/file-' + i + t.plan(paths.length * 2) + for (const i in paths) { let stream switch (i % 3) { case 0: @@ -34,19 +31,18 @@ test('write files', function (t) { stream = new fs.WriteStream(paths[i]) break } + t.type(stream, fs.WriteStream) - stream.on('finish', function () { - t.pass('success') - }) + stream.on('finish', () => t.pass('success')) stream.write('content') stream.end() } }) -test('read files', function (t) { +test('read files', t => { // now read them - t.plan(num * 2) - for (var i = 0; i < num; ++i) (function (i) { + t.plan(paths.length * 2) + for (const i in paths) { let stream switch (i % 3) { case 0: @@ -59,18 +55,17 @@ test('read files', function (t) { stream = new fs.ReadStream(paths[i]) break } + t.type(stream, fs.ReadStream) - var data = '' - stream.on('data', function (c) { + let data = '' + stream.on('data', c => { data += c }) - stream.on('end', function () { - t.equal(data, 'content') - }) - })(i) + stream.on('end', () => t.equal(data, 'content')) + } }) -function streamErrors(t, read, autoClose) { +function streamErrors (t, read, autoClose) { const events = [] const initializer = read ? 'createReadStream' : 'createWriteStream' const stream = fs[initializer]( @@ -79,7 +74,7 @@ function streamErrors(t, read, autoClose) { ) const matchDestroy = autoClose ? ['destroy'] : ['error', 'destroy'] const matchError = autoClose ? ['destroy', 'error'] : ['error'] - const destroy = stream.destroy + const {destroy} = stream stream.destroy = () => { events.push('destroy') t.deepEqual(events, matchDestroy, 'got destroy') @@ -107,7 +102,7 @@ test('ReadStream replacement', t => { let called = 0 class FakeReplacement { - constructor(...args) { + constructor (...args) { t.deepEqual(args, testArgs) called++ } @@ -127,7 +122,7 @@ test('WriteStream replacement', t => { let called = 0 class FakeReplacement { - constructor(...args) { + constructor (...args) { t.deepEqual(args, testArgs) called++ } @@ -142,7 +137,7 @@ test('WriteStream replacement', t => { t.end() }) -test('cleanup', function (t) { +test('cleanup', t => { rimraf.sync(p) t.end() }) diff --git a/test/readdir-options.js b/test/readdir-options.js index 5dd9000..f457608 100644 --- a/test/readdir-options.js +++ b/test/readdir-options.js @@ -1,11 +1,13 @@ -var fs = require("fs") -var t = require("tap") +'use strict' -var currentTest +const fs = require('fs') +const t = require('tap') -var strings = ['b', 'z', 'a'] -var buffs = strings.map(function (s) { return Buffer.from(s) }) -var hexes = buffs.map(function (b) { return b.toString('hex') }) +let currentTest + +const strings = ['b', 'z', 'a'] +const buffs = strings.map(s => Buffer.from(s)) +const hexes = buffs.map(b => b.toString('hex')) function getRet (encoding) { switch (encoding) { @@ -18,17 +20,14 @@ function getRet (encoding) { } } -var readdir = fs.readdir -var failed = false -fs.readdir = function(path, options, cb) { +let failed = false +fs.readdir = (path, options, cb) => { if (!failed) { // simulate an EMFILE and then open and close a thing to retry failed = true - process.nextTick(function () { - var er = new Error('synthetic emfile') - er.code = 'EMFILE' - cb(er) - process.nextTick(function () { + process.nextTick(() => { + cb(Object.assign(new Error('synthetic emfile'), {code: 'EMFILE'})) + process.nextTick(() => { g.closeSync(fs.openSync(__filename, 'r')) }) }) @@ -39,22 +38,20 @@ fs.readdir = function(path, options, cb) { currentTest.isa(cb, 'function') currentTest.isa(options, 'object') currentTest.ok(options) - process.nextTick(function() { - var ret = getRet(options.encoding) - cb(null, ret) + process.nextTick(() => { + cb(null, getRet(options.encoding)) }) } -var g = require('./helpers/graceful-fs.js') +const g = require('./helpers/graceful-fs.js') -var encodings = ['buffer', 'hex', 'utf8', null] -encodings.forEach(function (enc) { - t.test('encoding=' + enc, function (t) { +const encodings = ['buffer', 'hex', 'utf8', null] +encodings.forEach(encoding => { + t.test('encoding=' + encoding, t => { currentTest = t - g.readdir("whatevers", { encoding: enc }, function (er, files) { - if (er) - throw er - t.same(files, getRet(enc).sort()) + g.readdir('whatevers', {encoding}, (er, files) => { + t.error(er) + t.same(files, getRet(encoding).sort()) t.end() }) }) diff --git a/test/readdir-sort.js b/test/readdir-sort.js index 4073d89..894c23a 100644 --- a/test/readdir-sort.js +++ b/test/readdir-sort.js @@ -1,20 +1,23 @@ -var fs = require("fs") +'use strict' -var readdir = fs.readdir -fs.readdir = function(path, cb) { - process.nextTick(function() { - cb(null, ["b", "z", "a"]) +const fs = require('fs') + +fs.readdir = (path, cb) => { + process.nextTick(() => { + cb(null, ['b', 'z', 'a']) }) } -var g = require('./helpers/graceful-fs.js') -var test = require("tap").test +const g = require('./helpers/graceful-fs.js') +const {test} = require('tap') -test("readdir reorder", function (t) { - g.readdir("whatevers", function (er, files) { - if (er) +test('readdir reorder', t => { + g.readdir('whatevers', (er, files) => { + if (er) { throw er - t.same(files, [ "a", "b", "z" ]) + } + + t.same(files, ['a', 'b', 'z']) t.end() }) }) diff --git a/test/readfile.js b/test/readfile.js index d9430c8..c410194 100644 --- a/test/readfile.js +++ b/test/readfile.js @@ -1,47 +1,42 @@ 'use strict' -var fs = require('./helpers/graceful-fs.js') -var rimraf = require('rimraf') -var mkdirp = require('mkdirp') -var test = require('tap').test -var p = require('path').resolve(__dirname, 'files') +const fs = require('./helpers/graceful-fs.js') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const {test} = require('tap') -process.chdir(__dirname) +const p = require('path').resolve(__dirname, 'files-readfile') // Make sure to reserve the stderr fd process.stderr.write('') -var num = 4097 -var paths = new Array(num) +const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) -test('write files', function (t) { +test('write files', t => { rimraf.sync(p) mkdirp.sync(p) - t.plan(num) - for (var i = 0; i < num; ++i) { - paths[i] = 'files/file-' + i - fs.writeFile(paths[i], 'content', 'ascii', function (er) { - if (er) - throw er + t.plan(paths.length * 2) + for (const i in paths) { + fs.writeFile(paths[i], 'content', 'ascii', er => { + t.error(er) t.pass('written') }) } }) -test('read files', function (t) { +test('read files', t => { // now read them - t.plan(num) - for (var i = 0; i < num; ++i) { - fs.readFile(paths[i], 'ascii', function (er, data) { - if (er) - throw er + t.plan(paths.length * 2) + for (const i in paths) { + fs.readFile(paths[i], 'ascii', (er, data) => { + t.error(er) t.equal(data, 'content') }) } }) -test('cleanup', function (t) { +test('cleanup', t => { rimraf.sync(p) t.end() }) diff --git a/test/windows-rename-polyfill.js b/test/windows-rename-polyfill.js index e0da6e8..88b1033 100644 --- a/test/windows-rename-polyfill.js +++ b/test/windows-rename-polyfill.js @@ -4,10 +4,10 @@ const path = require('path') const fs = require('fs') const windowsRenamePolyfill = require('../windows-rename-polyfill.js') -function createPolyfilledObject(code) { +function createPolyfilledObject (code) { const pfs = { stat: fs.stat, - rename(a, b, cb) { + rename (a, b, cb) { /* original rename */ cb(Object.assign(new Error(code), {code})) } @@ -19,9 +19,9 @@ function createPolyfilledObject(code) { } const t = require('tap') + const a = path.join(__dirname, 'a') const b = path.join(__dirname, 'b') -const c = path.join(__dirname, 'c') t.test('setup', t => { const pfs = createPolyfilledObject('EPERM') @@ -38,7 +38,7 @@ t.test('setup', t => { t.end() }) -t.test('rename EPERM', { timeout: 100 }, t => { +t.test('rename EPERM', {timeout: 100}, t => { t.plan(2) const pfs = createPolyfilledObject('EPERM') @@ -48,7 +48,7 @@ t.test('rename EPERM', { timeout: 100 }, t => { }) }) -t.test('rename EACCES', { timeout: 100 }, t => { +t.test('rename EACCES', {timeout: 100}, t => { t.plan(2) const pfs = createPolyfilledObject('EACCES') @@ -58,7 +58,7 @@ t.test('rename EACCES', { timeout: 100 }, t => { }) }) -t.test('rename ENOENT', { timeout: 100 }, t => { +t.test('rename ENOENT', {timeout: 100}, t => { t.plan(2) const pfs = createPolyfilledObject('ENOENT') @@ -68,7 +68,7 @@ t.test('rename ENOENT', { timeout: 100 }, t => { }) }) -t.test('rename EPERM then stat ENOENT', { timeout: 2000 }, t => { +t.test('rename EPERM then stat ENOENT', {timeout: 2000}, t => { t.plan(3) const pfs = createPolyfilledObject('EPERM') @@ -88,8 +88,14 @@ t.test('rename EPERM then stat ENOENT', { timeout: 2000 }, t => { }) }) -t.test('cleanup', function (t) { - try { fs.rmdirSync(a) } catch (e) {} - try { fs.rmdirSync(b) } catch (e) {} +t.test('cleanup', t => { + try { + fs.rmdirSync(a) + } catch (e) {} + + try { + fs.rmdirSync(b) + } catch (e) {} + t.end() }) diff --git a/test/write-then-read.js b/test/write-then-read.js index 4aeded7..f020fa1 100644 --- a/test/write-then-read.js +++ b/test/write-then-read.js @@ -1,43 +1,39 @@ -var fs = require('./helpers/graceful-fs.js'); -var rimraf = require('rimraf'); -var mkdirp = require('mkdirp'); -var test = require('tap').test; -var p = require('path').resolve(__dirname, 'files'); +'use strict' -process.chdir(__dirname) +const fs = require('./helpers/graceful-fs.js') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const {test} = require('tap') +const p = require('path').resolve(__dirname, 'files-write-then-read') // Make sure to reserve the stderr fd -process.stderr.write(''); +process.stderr.write('') -var num = 4097; -var paths = new Array(num); +const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) -test('make files', function (t) { - rimraf.sync(p); - mkdirp.sync(p); +test('make files', t => { + rimraf.sync(p) + mkdirp.sync(p) - for (var i = 0; i < num; ++i) { - paths[i] = 'files/file-' + i; - fs.writeFileSync(paths[i], 'content'); + for (const i in paths) { + fs.writeFileSync(paths[i], 'content') } - t.end(); + t.end() }) test('read files', function (t) { // now read them - t.plan(num) - for (var i = 0; i < num; ++i) { - fs.readFile(paths[i], 'ascii', function(err, data) { - if (err) - throw err; - + t.plan(paths.length * 2) + for (const i in paths) { + fs.readFile(paths[i], 'ascii', (err, data) => { + t.error(err) t.equal(data, 'content') - }); + }) } -}); +}) -test('cleanup', function (t) { - rimraf.sync(p); - t.end(); -}); +test('cleanup', t => { + rimraf.sync(p) + t.end() +}) From ad402e9f135701bc4ee3ab32c0ca297746505a02 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Thu, 8 Aug 2019 08:50:20 -0400 Subject: [PATCH 23/34] Prevent multiple patching of process --- polyfills.js | 44 ++++++++++++++++++++++++++------------- test/avoid-memory-leak.js | 12 +++++++++++ test/chdir.js | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/polyfills.js b/polyfills.js index d266bd1..3f163d5 100644 --- a/polyfills.js +++ b/polyfills.js @@ -1,24 +1,38 @@ +'use strict' + const normalizeArgs = require('./normalize-args.js') const {noop, noopSync} = require('./noop.js') -var origCwd = process.cwd -var cwd = null +function patchProcess () { + if (/graceful-fs replacement/.test(process.cwd.toString())) { + // Don't patch more than once + return + } -process.cwd = function() { - if (!cwd) - cwd = origCwd.call(process) - return cwd -} -try { - process.cwd() -} catch (er) {} - -var chdir = process.chdir -process.chdir = function(d) { - cwd = null - chdir.call(process, d) + const {cwd, chdir} = process + let pwd = null + + process.cwd = () => { + /* graceful-fs replacement */ + if (pwd === null) { + pwd = cwd() + } + + return pwd + } + + process.chdir = dir => { + pwd = null + chdir(dir) + } + + try { + process.cwd() + } catch (er) {} } +patchProcess() + module.exports = patch function patch (fs) { diff --git a/test/avoid-memory-leak.js b/test/avoid-memory-leak.js index 04ffdfa..10fb6b4 100644 --- a/test/avoid-memory-leak.js +++ b/test/avoid-memory-leak.js @@ -49,3 +49,15 @@ t.test('no memory leak when loading multiple times', t => { importFreshGracefulFs() }) + +t.test('process is not repeatedly patched', t => { + const polyfills = path.resolve(__dirname, '../polyfills.js') + importFresh(polyfills) + + let {cwd, chdir} = process + + importFresh(polyfills) + t.is(cwd, process.cwd) + t.is(chdir, process.chdir) + t.end() +}) diff --git a/test/chdir.js b/test/chdir.js index f7544ea..781f50f 100644 --- a/test/chdir.js +++ b/test/chdir.js @@ -3,13 +3,53 @@ const path = require('path') const t = require('tap') +const {chdir, cwd} = process +let hits = { + chdir: 0, + cwd: 0 +} + +/* nyc has already caused graceful-fs to be loaded from node_modules. + * If that version of graceful-fs contains the multiple load protection + * it will prevent our polyfills.js from replacing process.chdir and + * process.cwd. Replacing these functions cause our polyfills.js to + * still perform process function replacements. */ +process.chdir = dir => { + hits.chdir++ + chdir(dir) +} + +process.cwd = () => { + hits.cwd++ + return cwd() +} + // Side-effect only to force replacement of process.chdir / process.cwd require('./helpers/graceful-fs.js') const project = path.resolve(__dirname, '..') +// Ignore any calls that happen during initialization +hits.chdir = 0 +hits.cwd = 0 + process.chdir(__dirname) +t.is(hits.chdir, 1, 'our chdir was called') +t.is(hits.cwd, 0, 'no calls to cwd') + t.is(process.cwd(), __dirname, 'chdir(__dirname) worked') +t.is(hits.cwd, 1, 'our cwd was called') + +t.is(process.cwd(), __dirname, 'cwd twice in a row') +t.is(hits.cwd, 1, 'repeat calls to cwd use cached value') +t.is(hits.chdir, 1, 'no unexpected calls to chdir') + process.chdir(project) +t.is(hits.chdir, 2, 'our chdir was called') +t.is(hits.cwd, 1, 'no unexpected calls to cwd') + t.is(process.cwd(), project, 'chdir(project) worked') +t.is(hits.cwd, 2) +t.is(hits.chdir, 2, 'no unexpected calls to chdir') + t.end() From 159c190cd2ffcc58feea4b579e857d6201040386 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Thu, 8 Aug 2019 15:40:05 -0400 Subject: [PATCH 24/34] Fix testing for future versions of nyc --- graceful-fs.js | 33 +++++++++++++++++++++++++-------- test/helpers/graceful-fs.js | 4 ++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index 6e9b4ea..3049c06 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -11,16 +11,33 @@ const debug = util.debuglog('gfs4') const gracefulPatched = Symbol.for('graceful-fs.patched') const gracefulQueue = Symbol.for('graceful-fs.queue') +const gracefulResetQueue = Symbol.for('graceful-fs.reset-queue') // Once time initialization -if (!global[gracefulQueue]) { - // This queue can be shared by multiple loaded instances - const queue = [] - Object.defineProperty(global, gracefulQueue, { - get () { - return queue - } - }) +if (!global[gracefulQueue] || global[gracefulResetQueue]) { + delete global[gracefulResetQueue] + + /* istanbul ignore next: nyc already created this variable, this is untestable */ + if (!global[gracefulQueue]) { + // This queue can be shared by multiple loaded instances + const queue = [] + Object.defineProperty(global, gracefulQueue, { + get () { + return queue + } + }) + } + + const previous = Symbol.for('graceful-fs.previous') + /* istanbul ignore else: this is always true when running under nyc */ + if (fs.close[previous]) { + fs.close = fs.close[previous] + } + + /* istanbul ignore else: this is always true when running under nyc */ + if (fs.closeSync[previous]) { + fs.closeSync = fs.closeSync[previous] + } // This is used in testing by future versions var previous = Symbol.for('graceful-fs.previous') diff --git a/test/helpers/graceful-fs.js b/test/helpers/graceful-fs.js index 5119008..ab33684 100644 --- a/test/helpers/graceful-fs.js +++ b/test/helpers/graceful-fs.js @@ -1,5 +1,9 @@ 'use strict' +/* This is to force use of *our* fs.close / fs.closeSync even if + * nyc is using a version of graceful-fs with the shared queue */ +global[Symbol.for('graceful-fs.reset-queue')] = true + const gfs = require('../../graceful-fs.js') module.exports = process.env.TEST_GFS_GLOBAL_PATCH ? gfs.gracefulify(require('fs')) : gfs From 438495b1906839161a4b3589375f86fef7cf234a Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Fri, 9 Aug 2019 08:17:47 -0400 Subject: [PATCH 25/34] Use fs.mkdtempSync in tests --- .gitignore | 1 + package.json | 2 +- test/lutimes.js | 14 ++++---------- test/read-write-stream.js | 7 +------ test/readfile.js | 8 ++------ test/write-then-read.js | 7 ++----- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 2f24c57..909c219 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ coverage/ .nyc_output/ +test/temp-files-*/ diff --git a/package.json b/package.json index 8e870c4..6bb432b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "preversion": "npm test", "postversion": "npm publish", "postpublish": "git push origin --follow-tags", + "pretest": "rimraf test/temp-files-*", "test": "nyc -r none node test.js | tap -", "posttest": "nyc report -r text --check-coverage" }, @@ -37,7 +38,6 @@ "devDependencies": { "glob": "^7.1.4", "import-fresh": "^3.1.0", - "mkdirp": "^0.5.1", "nyc": "^14.1.1", "rimraf": "^2.6.3", "tap": "^14.6.1" diff --git a/test/lutimes.js b/test/lutimes.js index 19b3034..2886a91 100644 --- a/test/lutimes.js +++ b/test/lutimes.js @@ -1,18 +1,14 @@ 'use strict' const path = require('path') -const mkdirp = require('mkdirp') const rimraf = require('rimraf') const {test} = require('tap') const fs = require('./helpers/graceful-fs.js') -const dir = path.resolve(__dirname, 'files') -const ln = path.resolve(dir, 'symlink') - if (!('O_SYMLINK' in fs.constants)) { test('stubs', t => { - fs.lutimes(ln, 0, 0, () => {}) - fs.lutimesSync(ln, 0, 0) + fs.lutimes('ln', 0, 0, () => {}) + fs.lutimesSync('ln', 0, 0) t.end() }) @@ -20,10 +16,8 @@ if (!('O_SYMLINK' in fs.constants)) { process.exit(0) } -test('setup', t => { - mkdirp.sync(dir) - t.end() -}) +const dir = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) +const ln = path.resolve(dir, 'symlink') test('lutimes', t => { fs.symlinkSync(__filename, ln) diff --git a/test/read-write-stream.js b/test/read-write-stream.js index 07d41ba..227d4b7 100644 --- a/test/read-write-stream.js +++ b/test/read-write-stream.js @@ -3,20 +3,15 @@ const path = require('path') const fs = require('./helpers/graceful-fs.js') const rimraf = require('rimraf') -const mkdirp = require('mkdirp') const {test} = require('tap') -const p = path.resolve(__dirname, 'files-read-write-stream') - // Make sure to reserve the stderr fd process.stderr.write('') +const p = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) test('write files', t => { - rimraf.sync(p) - mkdirp.sync(p) - t.plan(paths.length * 2) for (const i in paths) { let stream diff --git a/test/readfile.js b/test/readfile.js index c410194..bb42793 100644 --- a/test/readfile.js +++ b/test/readfile.js @@ -1,21 +1,17 @@ 'use strict' +const path = require('path') const fs = require('./helpers/graceful-fs.js') const rimraf = require('rimraf') -const mkdirp = require('mkdirp') const {test} = require('tap') -const p = require('path').resolve(__dirname, 'files-readfile') - // Make sure to reserve the stderr fd process.stderr.write('') +const p = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) test('write files', t => { - rimraf.sync(p) - mkdirp.sync(p) - t.plan(paths.length * 2) for (const i in paths) { fs.writeFile(paths[i], 'content', 'ascii', er => { diff --git a/test/write-then-read.js b/test/write-then-read.js index f020fa1..e25fb5e 100644 --- a/test/write-then-read.js +++ b/test/write-then-read.js @@ -1,20 +1,17 @@ 'use strict' +const path = require('path') const fs = require('./helpers/graceful-fs.js') const rimraf = require('rimraf') -const mkdirp = require('mkdirp') const {test} = require('tap') -const p = require('path').resolve(__dirname, 'files-write-then-read') // Make sure to reserve the stderr fd process.stderr.write('') +const p = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) test('make files', t => { - rimraf.sync(p) - mkdirp.sync(p) - for (const i in paths) { fs.writeFileSync(paths[i], 'content') } From 1743a28e5eaacb773c6ee205d386e7059d32a442 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Fri, 9 Aug 2019 16:40:17 -0400 Subject: [PATCH 26/34] Implement fs.promises patching --- chown-er-filter.js | 28 ++++++ graceful-fs.js | 89 ++--------------- nyc.config.js | 14 ++- package.json | 4 + polyfills.js | 26 +---- promise-windows-rename-polyfill.js | 46 +++++++++ promises.js | 154 +++++++++++++++++++++++++++++ retry-queue.js | 88 +++++++++++++++++ test/avoid-memory-leak.js | 47 +++++---- test/chown-er-ok.js | 64 +++++++++--- test/eagain.js | 34 +++++++ test/enoent.js | 14 ++- test/helpers/promises.js | 9 ++ test/open.js | 18 ++-- test/readdir-options.js | 35 +++++-- test/readdir-sort.js | 20 ++-- test/readfile.js | 59 ++++++++++- test/windows-rename-polyfill.js | 98 ++++++++++++------ test/write-then-read.js | 36 ------- 19 files changed, 644 insertions(+), 239 deletions(-) create mode 100644 chown-er-filter.js create mode 100644 promise-windows-rename-polyfill.js create mode 100644 promises.js create mode 100644 retry-queue.js create mode 100644 test/helpers/promises.js delete mode 100644 test/write-then-read.js diff --git a/chown-er-filter.js b/chown-er-filter.js new file mode 100644 index 0000000..804d5d6 --- /dev/null +++ b/chown-er-filter.js @@ -0,0 +1,28 @@ +'use strict' + +// ENOSYS means that the fs doesn't support the op. Just ignore +// that, because it doesn't matter. +// +// if there's no getuid, or if getuid() is something other +// than 0, and the error is EINVAL or EPERM, then just ignore +// it. +// +// This specific case is a silent failure in cp, install, tar, +// and most other unix tools that manage permissions. +// +// When running as root, or if other types of errors are +// encountered, then it's strict. +function chownErFilter (er) { + if (!er || er.code === 'ENOSYS') { + return + } + + const nonroot = !process.getuid || process.getuid() !== 0 + if (nonroot && (er.code === 'EINVAL' || er.code === 'EPERM')) { + return + } + + return er +} + +module.exports = chownErFilter diff --git a/graceful-fs.js b/graceful-fs.js index 3049c06..36b0e76 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -1,81 +1,15 @@ 'use strict' const fs = require('fs') -const util = require('util') const polyfills = require('./polyfills.js') const clone = require('./clone.js') const normalizeArgs = require('./normalize-args.js') - -const debug = util.debuglog('gfs4') +const {initQueue, retry, enqueue} = require('./retry-queue.js') const gracefulPatched = Symbol.for('graceful-fs.patched') -const gracefulQueue = Symbol.for('graceful-fs.queue') -const gracefulResetQueue = Symbol.for('graceful-fs.reset-queue') - -// Once time initialization -if (!global[gracefulQueue] || global[gracefulResetQueue]) { - delete global[gracefulResetQueue] - - /* istanbul ignore next: nyc already created this variable, this is untestable */ - if (!global[gracefulQueue]) { - // This queue can be shared by multiple loaded instances - const queue = [] - Object.defineProperty(global, gracefulQueue, { - get () { - return queue - } - }) - } - - const previous = Symbol.for('graceful-fs.previous') - /* istanbul ignore else: this is always true when running under nyc */ - if (fs.close[previous]) { - fs.close = fs.close[previous] - } - - /* istanbul ignore else: this is always true when running under nyc */ - if (fs.closeSync[previous]) { - fs.closeSync = fs.closeSync[previous] - } - - // This is used in testing by future versions - var previous = Symbol.for('graceful-fs.previous') - - // Patch fs.close/closeSync to shared queue version, because we need - // to retry() whenever a close happens *anywhere* in the program. - // This is essential when multiple graceful-fs instances are - // in play at the same time. - const {close, closeSync} = fs - fs.close = (fd, cb) => { - cb = normalizeArgs([cb])[1] - - close(fd, err => { - // This function uses the graceful-fs shared queue - if (!err) { - retry() - } - cb(err) - }) - } - fs.close[previous] = close - - fs.closeSync = fd => { - // This function uses the graceful-fs shared queue - closeSync(fd) - retry() - } - fs.closeSync[previous] = closeSync - - /* istanbul ignore next */ - if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { - process.on('exit', () => { - debug(global[gracefulQueue]) - require('assert').strictEqual(global[gracefulQueue].length, 0) - }) - } -} +initQueue() module.exports = patch(clone(fs)) @@ -186,18 +120,11 @@ function patch (fs) { patchStream(fs, true) patchStream(fs, false) - return fs -} - -function enqueue (elem) { - debug('ENQUEUE', elem[0].name, elem[1]) - global[gracefulQueue].push(elem) -} - -function retry () { - const elem = global[gracefulQueue].shift() - if (elem) { - debug('RETRY', elem[0].name, elem[1]) - elem[0](...elem[1]) + const promises = Object.getOwnPropertyDescriptor(fs, 'promises') + /* istanbul ignore next */ + if (promises) { + require('./promises.js')(fs, promises) } + + return fs } diff --git a/nyc.config.js b/nyc.config.js index 7c500d8..728d777 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -1,8 +1,18 @@ 'use strict' -const {constants} = require('fs') +const fs = require('fs') const glob = require('glob') +const ignore = [] + +if (!('O_SYMLINK' in fs.constants)) { + ignore.push('lutimes-polyfill.js') +} + +if (!Object.getOwnPropertyDescriptor(fs, 'promises')) { + ignore.push('promises.js', 'promise-windows-rename-polyfill.js') +} + module.exports = { all: true, lines: 100, @@ -11,6 +21,6 @@ module.exports = { branches: 100, include: glob.sync('*.js', { cwd: __dirname, - ignore: 'O_SYMLINK' in constants ? [] : ['lutimes-polyfill.js'] + ignore }) } diff --git a/package.json b/package.json index 6bb432b..704c842 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,16 @@ }, "files": [ "check-for-callback.js", + "chown-er-filter.js", "clone.js", "graceful-fs.js", "lutimes-polyfill.js", "noop.js", "normalize-args.js", "polyfills.js", + "promises.js", + "promise-windows-rename-polyfill.js", + "retry-queue.js", "windows-rename-polyfill.js" ], "dependencies": {} diff --git a/polyfills.js b/polyfills.js index 3f163d5..dd15ba9 100644 --- a/polyfills.js +++ b/polyfills.js @@ -2,6 +2,7 @@ const normalizeArgs = require('./normalize-args.js') const {noop, noopSync} = require('./noop.js') +const chownErFilter = require('./chown-er-filter.js') function patchProcess () { if (/graceful-fs replacement/.test(process.cwd.toString())) { @@ -154,29 +155,4 @@ function patch (fs) { } } } - - // ENOSYS means that the fs doesn't support the op. Just ignore - // that, because it doesn't matter. - // - // if there's no getuid, or if getuid() is something other - // than 0, and the error is EINVAL or EPERM, then just ignore - // it. - // - // This specific case is a silent failure in cp, install, tar, - // and most other unix tools that manage permissions. - // - // When running as root, or if other types of errors are - // encountered, then it's strict. - function chownErFilter (er) { - if (!er || er.code === 'ENOSYS') { - return - } - - const nonroot = !process.getuid || process.getuid() !== 0 - if (nonroot && (er.code === 'EINVAL' || er.code === 'EPERM')) { - return - } - - return er - } } diff --git a/promise-windows-rename-polyfill.js b/promise-windows-rename-polyfill.js new file mode 100644 index 0000000..fa1a8d6 --- /dev/null +++ b/promise-windows-rename-polyfill.js @@ -0,0 +1,46 @@ +'use strict' + +const {promisify} = require('util') + +const accessErrors = new Set(['EACCES', 'EPERM']) + +const delay = promisify(setTimeout) + +function promiseWindowsRenamePolyfill (fs) { + const {rename} = fs + + fs.rename = async (from, to) => { + const start = Date.now() + let backoff = 0 + + while (true) { + try { + return await rename(from, to) + } catch (er) { + if (!accessErrors.has(er.code) || Date.now() - start >= 60000) { + throw er + } + + await delay(backoff) + + // The only way this `await` resolves is if fs.stat throws ENOENT. + await fs.stat(to).then( + () => { + throw er + }, + stater => { + if (stater.code !== 'ENOENT') { + throw er + } + } + ) + + if (backoff < 100) { + backoff += 10 + } + } + } + } +} + +module.exports = promiseWindowsRenamePolyfill diff --git a/promises.js b/promises.js new file mode 100644 index 0000000..3ad930a --- /dev/null +++ b/promises.js @@ -0,0 +1,154 @@ +'use strict' + +const {promisify} = require('util') + +const clone = require('./clone.js') +const chownErFilter = require('./chown-er-filter.js') +const {retry, enqueue} = require('./retry-queue.js') + +// This is the native C++ FileHandle +const {FileHandle} = process.binding('fs') + +// We need the constructor of the object returned by +// fs.promises.open so we can extend it +async function getPromisesFileHandle (open) { + const handle = await open(__filename, 'r') + const PromisesFileHandle = handle.constructor + + await handle.close() + + return PromisesFileHandle +} + +function patchChown (orig) { + return (...args) => orig(...args).catch(er => { + if (chownErFilter(er)) { + throw er + } + }) +} + +function patchAsyncENFILE (origImpl, next = res => res) { + const attempt = (args, resolve, reject) => { + origImpl(...args) + .then(res => { + process.nextTick(retry) + resolve(next(res)) + }) + .catch(err => { + if (err.code === 'EMFILE' || err.code === 'ENFILE') { + enqueue([attempt, [args, resolve, reject]]) + } else { + process.nextTick(retry) + reject(err) + } + }) + } + + return (...args) => new Promise((resolve, reject) => attempt(args, resolve, reject)) +} + +async function setupOpen (fs, promises) { + const PromisesFileHandle = await getPromisesFileHandle(promises.open) + class GracefulFileHandle extends PromisesFileHandle { + // constructor (filehandle) + // getAsyncId () + // get fd () + // datasync () + // sync () + // stat (options) + // truncate (len = 0) + // utimes (atime, mtime) + // write (buffer, offset, length, position) + + // fd is already open so no need to use `patchAsyncENFILE` functions from here + // appendFile (data, options) + // readFile (options) + // writeFile (data, options) + + async chmod (mode) { + return super.chmod(mode).catch(er => { + if (chownErFilter(er)) { + throw er + } + }) + } + + async chown (uid, gid) { + return super.chown(uid, gid).catch(er => { + if (chownErFilter(er)) { + throw er + } + }) + } + + async read (buffer, offset, length, position) { + let eagCounter = 0 + while (true) { + try { + return await super.read(buffer, offset, length, position) + } catch (er) { + if (er.code === 'EAGAIN' && eagCounter < 10) { + eagCounter ++ + continue + } + + throw er + } + } + } + + async close () { + await super.close() + retry() + } + } + + // fs.open is already patched, do not use patchAsyncENFILE here + const open = promisify(fs.open) + promises.open = async (...args) => { + return new GracefulFileHandle(new FileHandle(await open(...args))) + } +} + +function patchPromises (fs, orig) { + let promises + Object.defineProperty(fs, 'promises', { + configurable: true, + get () { + if (!promises) { + // Checking `orig.value` handles situations where a user does + // something like require('graceful-fs).gracefulify({...require('fs')}) + promises = clone(orig.value || orig.get()) + const initOpen = setupOpen(fs, promises) + + // This is temporary because node.js doesn't directly expose + // everything we need, some async operations are required to + // construct the real replacement for open + promises.open = async (...args) => { + await initOpen + return promises.open(...args) + } + + promises.readFile = patchAsyncENFILE(promises.readFile) + promises.writeFile = patchAsyncENFILE(promises.writeFile) + promises.appendFile = patchAsyncENFILE(promises.appendFile) + promises.readdir = patchAsyncENFILE(promises.readdir, files => files.sort()) + + promises.chmod = patchChown(promises.chmod) + promises.lchmod = patchChown(promises.lchmod) + promises.chown = patchChown(promises.chown) + promises.lchown = patchChown(promises.lchown) + + /* istanbul ignore next */ + if (process.platform === 'win32') { + require('./promise-windows-rename-polyfill.js')(promises) + } + } + + return promises + } + }) +} + +module.exports = patchPromises diff --git a/retry-queue.js b/retry-queue.js new file mode 100644 index 0000000..c55dffc --- /dev/null +++ b/retry-queue.js @@ -0,0 +1,88 @@ +'use strict' + +const fs = require('fs') +const util = require('util') +const normalizeArgs = require('./normalize-args.js') + +const debug = util.debuglog('gfs4') + +const gracefulQueue = Symbol.for('graceful-fs.queue') +const gracefulResetQueue = Symbol.for('graceful-fs.reset-queue') + +// Once time initialization +function initQueue () { + if (!global[gracefulQueue] || global[gracefulResetQueue]) { + delete global[gracefulResetQueue] + + /* istanbul ignore next: nyc already created this variable, this is untestable */ + if (!global[gracefulQueue]) { + // This queue can be shared by multiple loaded instances + const queue = [] + Object.defineProperty(global, gracefulQueue, { + get () { + return queue + } + }) + } + + const previous = Symbol.for('graceful-fs.previous') + /* istanbul ignore else: this is always true when running under nyc */ + if (fs.close[previous]) { + fs.close = fs.close[previous] + } + + /* istanbul ignore else: this is always true when running under nyc */ + if (fs.closeSync[previous]) { + fs.closeSync = fs.closeSync[previous] + } + + // Patch fs.close/closeSync to shared queue version, because we need + // to retry() whenever a close happens *anywhere* in the program. + // This is essential when multiple graceful-fs instances are + // in play at the same time. + const {close, closeSync} = fs + fs.close = (fd, cb) => { + cb = normalizeArgs([cb])[1] + + close(fd, err => { + // This function uses the graceful-fs shared queue + if (!err) { + retry() + } + + cb(err) + }) + } + fs.close[previous] = close + + fs.closeSync = fd => { + // This function uses the graceful-fs shared queue + closeSync(fd) + retry() + } + fs.closeSync[previous] = closeSync + + /* istanbul ignore next */ + if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { + process.on('exit', () => { + debug(global[gracefulQueue]) + require('assert').strictEqual(global[gracefulQueue].length, 0) + }) + } + } +} + +function enqueue (elem) { + debug('ENQUEUE', elem[0].name, elem[1]) + global[gracefulQueue].push(elem) +} + +function retry () { + const elem = global[gracefulQueue].shift() + if (elem) { + debug('RETRY', elem[0].name, elem[1]) + elem[0](...elem[1]) + } +} + +module.exports = {initQueue, enqueue, retry} diff --git a/test/avoid-memory-leak.js b/test/avoid-memory-leak.js index 10fb6b4..6a18982 100644 --- a/test/avoid-memory-leak.js +++ b/test/avoid-memory-leak.js @@ -4,7 +4,9 @@ const path = require('path') const v8 = require('v8') const importFresh = require('import-fresh') const t = require('tap') +const {promisify} = require('util') +const delay = promisify(setTimeout) let previousHeapStats function checkHeap (t) { @@ -22,32 +24,37 @@ function checkHeap (t) { ) } -t.test('no memory leak when loading multiple times', t => { +async function gfsinit() { const gfsPath = path.resolve(__dirname, '../graceful-fs.js') const gfsHelper = path.join(__dirname, './helpers/graceful-fs.js') - importFresh(gfsHelper) + delete require.cache[gfsPath] + let fs = importFresh(gfsHelper) + // Force initialization of `fs.promises` if available + if (fs.promises) { + // fs.promises.open has an async initialization, this ensures it's fully loaded + const handle = await fs.promises.open(__filename, 'r') + await handle.close() + } else { + // For node.js 8 + await delay(1) + } +} + +t.test('no memory leak when loading multiple times', async t => { + t.ok(true, 'tap measures the time between first and last test') + await gfsinit() global.gc() + previousHeapStats = process.memoryUsage() + // simulate project with 4000 tests - let i = 0 - function importFreshGracefulFs () { - delete require.cache[gfsPath] - // We have to use absolute path because importFresh cannot find - // relative paths when run from the callback of `process.nextTick`. - importFresh(gfsHelper) - - if (i < 4000) { - i++ - process.nextTick(() => importFreshGracefulFs()) - } else { - global.gc() - checkHeap(t) - t.end() - } + for (let i = 0; i < 4000; i++) { + await gfsinit() } - importFreshGracefulFs() + global.gc() + checkHeap(t) }) t.test('process is not repeatedly patched', t => { @@ -57,7 +64,7 @@ t.test('process is not repeatedly patched', t => { let {cwd, chdir} = process importFresh(polyfills) - t.is(cwd, process.cwd) - t.is(chdir, process.chdir) + t.is(cwd, process.cwd, 'process.cwd not repeatedly patched') + t.is(chdir, process.chdir, 'process.chdir not repeatedly patched') t.end() }) diff --git a/test/chown-er-ok.js b/test/chown-er-ok.js index a377178..79671d8 100644 --- a/test/chown-er-ok.js +++ b/test/chown-er-ok.js @@ -1,6 +1,7 @@ 'use strict' const realFs = require('fs') +const {promisify} = require('util') // For the fchown / fchmod do not accept `path` as the first parameter but // gfs doesn't duplicate this check so we can still verify that the errors @@ -16,6 +17,12 @@ methods.forEach(method => { const err = makeErr(path, method) process.nextTick(() => cb(err)) } + + if (realFs.promises && !method.startsWith('f')) { + realFs.promises[method] = async path => { + throw makeErr(path, method) + } + } }) function makeErr (path, method) { @@ -26,22 +33,57 @@ function makeErr (path, method) { } const fs = require('./helpers/graceful-fs.js') -const t = require('tap') +const filehandlePromisesFileHandle = require('./helpers/promises.js') +const {test} = require('tap') const errs = ['ENOSYS', 'EINVAL', 'EPERM'] -t.plan(errs.length * methods.length * 2) + +async function helper (t, err, method) { + const args = [err] + if (/chmod/.test(method)) { + args.push('some mode') + } else { + args.push('some uid', 'some gid') + } + + t.doesNotThrow(() => fs[`${method}Sync`](...args), `${method}Sync does not throw ${err}`) + await promisify(fs[method])(...args) + if (fs.promises && !method.startsWith('f')) { + await fs.promises[method](...args) + } +} errs.forEach(err => { methods.forEach(method => { - const args = [err] - if (/chmod/.test(method)) { - args.push('some mode') - } else { - args.push('some uid', 'some gid') + test(`${method} ${err}`, t => helper(t, err, method)) + }) +}) + +if (fs.promises) { + test('FileHandle.chown / FileHandle.chmod', async t => { + const filehandle = await fs.promises.open(__filename, 'r') + const PromisesFileHandle = filehandlePromisesFileHandle(filehandle) + ;['chmod', 'chown'].forEach(method => { + PromisesFileHandle[method] = async path => { + throw makeErr(path, method) + } + }) + + for (const err of errs) { + await filehandle.chmod(err, 'some mode') + await filehandle.chown(err, 'some uid', 'some gid') } - t.doesNotThrow(() => fs[`${method}Sync`](...args), `${method}Sync does not throw ${err}`) - args.push(e => t.notOk(e, `${method} does not throw ${err}`)) - fs[method](...args) + await filehandle.close() + + await t.rejects( + filehandle.chmod('EBADF', 'some mode'), + {code: 'EBADF'} + ) + + await t.rejects( + filehandle.chown('EBADF', 'some uid', 'some gid'), + {code: 'EBADF'} + ) }) -}) +} diff --git a/test/eagain.js b/test/eagain.js index 456e838..e7c58e6 100644 --- a/test/eagain.js +++ b/test/eagain.js @@ -8,6 +8,8 @@ let readPing let readSyncPing let cbArgs = [] +const filehandlePromisesFileHandle = require('./helpers/promises.js') + // We can't hijack the actual `fs` module so we have to fake it const fs = require('../graceful-fs.js').gracefulify({ ...require('fs'), @@ -82,3 +84,35 @@ test('readSync unresolved EAGAIN', t => { t.is(counter, 5) t.end() }) + +if (fs.promises) { + test('promises read', async t => { + t.ok(true) + const filehandle = await fs.promises.open(__filename, 'r') + let counter = 0 + const PromisesFileHandle = filehandlePromisesFileHandle(filehandle) + PromisesFileHandle.read = async (...args) => { + counter++ + throw eagain() + } + + await t.rejects( + filehandle.read(null, 0, 0, 0), + {code: 'EAGAIN'}, + 'unresolve eagain' + ) + t.is(counter, 11) + + counter = 0 + PromisesFileHandle.read = async (...args) => { + counter++ + if (counter !== 5) { + throw eagain() + } + } + + await filehandle.read(null, 0, 0, 0) + t.is(counter, 5, 'retried 5 times') + await filehandle.close() + }) +} diff --git a/test/enoent.js b/test/enoent.js index a3122ed..8f2502a 100644 --- a/test/enoent.js +++ b/test/enoent.js @@ -3,6 +3,7 @@ // this test makes sure that various things get enoent, instead of // some other kind of throw. +const {promisify} = require('util') const g = require('./helpers/graceful-fs.js') const t = require('tap') @@ -19,20 +20,17 @@ const methods = [ t.plan(methods.length) methods.forEach(([method, ...args]) => { - t.test(method, t => { + t.test(method, async t => { const methodSync = `${method}Sync` t.isa(g[method], 'function') t.isa(g[methodSync], 'function') args.unshift(file) t.throws(() => g[methodSync](...args), {code: 'ENOENT'}) + if (g.promises) { + await t.rejects(g.promises[method](...args), {code: 'ENOENT'}) + } - // add the callback - args.push(er => { - t.isa(er, Error) - t.is(er.code, 'ENOENT') - t.end() - }) - t.doesNotThrow(() => g[method](...args)) + await t.rejects(promisify(g[method])(...args), {code: 'ENOENT'}) }) }) diff --git a/test/helpers/promises.js b/test/helpers/promises.js new file mode 100644 index 0000000..63104e3 --- /dev/null +++ b/test/helpers/promises.js @@ -0,0 +1,9 @@ +'use strict' + +function filehandlePromisesFileHandle (filehandle) { + return Object.getPrototypeOf( + Object.getPrototypeOf(filehandle) + ) +} + +module.exports = filehandlePromisesFileHandle diff --git a/test/open.js b/test/open.js index be41a84..06bfc88 100644 --- a/test/open.js +++ b/test/open.js @@ -22,15 +22,13 @@ test('open an existing file works', t => { }) }) -test('open a non-existing file throws', t => { - t.throws( - () => fs.openSync('this file does not exist', 'r'), - {code: 'ENOENT'} - ) +if (fs.promises) { + test('fs.promises.open an existing file works', async t => { + const filehandle = await fs.promises.open(__filename, 'r') - fs.open('neither does this file', 'r', (er, fd) => { - t.ok(er && er.code === 'ENOENT', 'should throw ENOENT') - t.notOk(fd, 'should not get an fd') - t.end() + t.type(filehandle.getAsyncId, 'function') + t.type(filehandle.read, 'function') + + await filehandle.close() }) -}) +} diff --git a/test/readdir-options.js b/test/readdir-options.js index f457608..13201dd 100644 --- a/test/readdir-options.js +++ b/test/readdir-options.js @@ -2,6 +2,7 @@ const fs = require('fs') const t = require('tap') +const {promisify} = require('util') let currentTest @@ -20,13 +21,14 @@ function getRet (encoding) { } } +const emfile = () => Object.assign(new Error('synthetic emfile'), {code: 'EMFILE'}) let failed = false fs.readdir = (path, options, cb) => { if (!failed) { // simulate an EMFILE and then open and close a thing to retry failed = true process.nextTick(() => { - cb(Object.assign(new Error('synthetic emfile'), {code: 'EMFILE'})) + cb(emfile()) process.nextTick(() => { g.closeSync(fs.openSync(__filename, 'r')) }) @@ -43,16 +45,37 @@ fs.readdir = (path, options, cb) => { }) } +if (fs.promises) { + fs.promises.readdir = async (path, options) => { + if (!failed) { + // simulate an EMFILE and then open and close a thing to retry + failed = true + process.nextTick(() => { + g.closeSync(fs.openSync(__filename, 'r')) + }) + throw emfile() + } + + failed = false + currentTest.isa(options, 'object') + currentTest.ok(options) + return getRet(options.encoding) + } +} + const g = require('./helpers/graceful-fs.js') const encodings = ['buffer', 'hex', 'utf8', null] encodings.forEach(encoding => { - t.test('encoding=' + encoding, t => { + const readdir = promisify(g.readdir) + t.test('encoding=' + encoding, async t => { currentTest = t - g.readdir('whatevers', {encoding}, (er, files) => { - t.error(er) + let files = await readdir('whatevers', {encoding}) + t.same(files, getRet(encoding).sort()) + + if (g.promises) { + files = await g.promises.readdir('whatevers', {encoding}) t.same(files, getRet(encoding).sort()) - t.end() - }) + } }) }) diff --git a/test/readdir-sort.js b/test/readdir-sort.js index 894c23a..7f8471a 100644 --- a/test/readdir-sort.js +++ b/test/readdir-sort.js @@ -1,6 +1,7 @@ 'use strict' const fs = require('fs') +const {promisify} = require('util') fs.readdir = (path, cb) => { process.nextTick(() => { @@ -8,16 +9,21 @@ fs.readdir = (path, cb) => { }) } +if (fs.promises) { + fs.promises.readdir = async (path) => { + return ['b', 'z', 'a'] + } +} + const g = require('./helpers/graceful-fs.js') const {test} = require('tap') -test('readdir reorder', t => { - g.readdir('whatevers', (er, files) => { - if (er) { - throw er - } +test('readdir reorder', async t => { + let files = await promisify(g.readdir)('whatevers') + t.same(files, ['a', 'b', 'z']) + if (g.promises) { + files = await g.promises.readdir('whatevers') t.same(files, ['a', 'b', 'z']) - t.end() - }) + } }) diff --git a/test/readfile.js b/test/readfile.js index bb42793..7babb02 100644 --- a/test/readfile.js +++ b/test/readfile.js @@ -8,11 +8,14 @@ const {test} = require('tap') // Make sure to reserve the stderr fd process.stderr.write('') -const p = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) -const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) +const tmpdir = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) +const dir = path.join(tmpdir, 'tmp') +const paths = new Array(4097).fill().map((_, i) => `${dir}/file-${i}`) test('write files', t => { t.plan(paths.length * 2) + fs.mkdirSync(dir) + // write files for (const i in paths) { fs.writeFile(paths[i], 'content', 'ascii', er => { t.error(er) @@ -22,17 +25,65 @@ test('write files', t => { }) test('read files', t => { - // now read them t.plan(paths.length * 2) + // now read them for (const i in paths) { fs.readFile(paths[i], 'ascii', (er, data) => { t.error(er) t.equal(data, 'content') }) } + + rimraf.sync(dir) }) +if (fs.promises) { + test('promise write then read files', async t => { + t.ok(true) + await fs.promises.mkdir(dir) + // write files + await Promise.all(paths.map(async (p, i) => { + // Alternate between the three methods + if (i % 3 === 0) { + await fs.promises.writeFile(p, 'content', 'ascii') + } else { + const filehandle = await fs.promises.open(p, 'wx') + if (i % 3 === 1) { + await filehandle.writeFile('content', 'ascii') + } else { + await fs.promises.writeFile(filehandle, 'content', 'ascii') + } + + await filehandle.close() + } + })) + + // now read them + const results = await Promise.all(paths.map(async (p, i) => { + // Alternate between the three methods + if (i % 3 === 0) { + return fs.promises.readFile(p, 'ascii') + } + + let result + const filehandle = await fs.promises.open(p, 'r') + + if (i % 3 === 1) { + result = await filehandle.readFile('ascii') + } else { + result = await fs.promises.readFile(filehandle, 'ascii') + } + + await filehandle.close() + return result + })) + + t.is(results.length, paths.length) + t.ok(results.every(r => r === 'content')) + }) +} + test('cleanup', t => { - rimraf.sync(p) + rimraf.sync(tmpdir) t.end() }) diff --git a/test/windows-rename-polyfill.js b/test/windows-rename-polyfill.js index 88b1033..045362d 100644 --- a/test/windows-rename-polyfill.js +++ b/test/windows-rename-polyfill.js @@ -2,7 +2,10 @@ const path = require('path') const fs = require('fs') +const {promisify} = require('util') + const windowsRenamePolyfill = require('../windows-rename-polyfill.js') +const promiseWindowsRenamePolyfill = require('../promise-windows-rename-polyfill.js') function createPolyfilledObject (code) { const pfs = { @@ -14,6 +17,17 @@ function createPolyfilledObject (code) { } windowsRenamePolyfill(pfs) + if (fs.promises) { + pfs.promises = { + stat: fs.promises.stat, + async rename (a, b) { + /* original rename */ + throw Object.assign(new Error(code), {code}) + } + } + + promiseWindowsRenamePolyfill(pfs.promises) + } return pfs } @@ -26,6 +40,9 @@ const b = path.join(__dirname, 'b') t.test('setup', t => { const pfs = createPolyfilledObject('EPERM') t.notMatch(pfs.rename.toString(), /original rename/) + if (pfs.promises) { + t.notMatch(pfs.promises.rename.toString(), /original rename/) + } try { fs.mkdirSync(a) @@ -38,39 +55,36 @@ t.test('setup', t => { t.end() }) -t.test('rename EPERM', {timeout: 100}, t => { - t.plan(2) - +t.test('rename EPERM', {timeout: 100}, async t => { const pfs = createPolyfilledObject('EPERM') - pfs.rename(a, b, er => { - t.ok(er) - t.is(er.code, 'EPERM') - }) -}) + console.log('orig rename') + await t.rejects(promisify(pfs.rename)(a, b), {code: 'EPERM'}) -t.test('rename EACCES', {timeout: 100}, t => { - t.plan(2) + if (pfs.promises) { + console.log('promise rename') + await t.rejects(pfs.promises.rename(a, b), {code: 'EPERM'}) + } +}) +t.test('rename EACCES', {timeout: 100}, async t => { const pfs = createPolyfilledObject('EACCES') - pfs.rename(a, b, er => { - t.ok(er) - t.is(er.code, 'EACCES') - }) -}) + await t.rejects(promisify(pfs.rename)(a, b), {code: 'EACCES'}) -t.test('rename ENOENT', {timeout: 100}, t => { - t.plan(2) + if (pfs.promises) { + await t.rejects(pfs.promises.rename(a, b), {code: 'EACCES'}) + } +}) +t.test('rename ENOENT', {timeout: 100}, async t => { const pfs = createPolyfilledObject('ENOENT') - pfs.rename(a, b, er => { - t.ok(er) - t.is(er.code, 'ENOENT') - }) -}) + await t.rejects(promisify(pfs.rename)(a, b), {code: 'ENOENT'}) -t.test('rename EPERM then stat ENOENT', {timeout: 2000}, t => { - t.plan(3) + if (pfs.promises) { + await t.rejects(pfs.promises.rename(a, b), {code: 'ENOENT'}) + } +}) +t.test('rename EPERM then stat ENOENT', {timeout: 2000}, async t => { const pfs = createPolyfilledObject('EPERM') let enoent = 12 pfs.stat = (p, cb) => { @@ -81,11 +95,37 @@ t.test('rename EPERM then stat ENOENT', {timeout: 2000}, t => { } } - pfs.rename(a, b, er => { - t.notOk(enoent) - t.ok(er) - t.is(er.code, 'EPERM') - }) + await t.rejects(promisify(pfs.rename)(a, b), {code: 'EPERM'}) + + if (pfs.promises) { + enoent = 12 + pfs.promises.stat = async p => { + if (--enoent) { + throw Object.assign(new Error('ENOENT'), {code: 'ENOENT'}) + } + + return fs.promises.stat(p) + } + + await t.rejects(pfs.promises.rename(a, b), {code: 'EPERM'}) + } +}) + +t.test('rename EPERM then stat EACCES', {timeout: 2000}, async t => { + const pfs = createPolyfilledObject('EPERM') + pfs.stat = (p, cb) => { + cb(Object.assign(new Error('EACCES'), {code: 'EACCES'})) + } + + await t.rejects(promisify(pfs.rename)(a, b), {code: 'EPERM'}) + + if (pfs.promises) { + pfs.promises.stat = async p => { + throw Object.assign(new Error('EACCES'), {code: 'EACCES'}) + } + + await t.rejects(pfs.promises.rename(a, b), {code: 'EPERM'}) + } }) t.test('cleanup', t => { diff --git a/test/write-then-read.js b/test/write-then-read.js deleted file mode 100644 index e25fb5e..0000000 --- a/test/write-then-read.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' - -const path = require('path') -const fs = require('./helpers/graceful-fs.js') -const rimraf = require('rimraf') -const {test} = require('tap') - -// Make sure to reserve the stderr fd -process.stderr.write('') - -const p = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) -const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) - -test('make files', t => { - for (const i in paths) { - fs.writeFileSync(paths[i], 'content') - } - - t.end() -}) - -test('read files', function (t) { - // now read them - t.plan(paths.length * 2) - for (const i in paths) { - fs.readFile(paths[i], 'ascii', (err, data) => { - t.error(err) - t.equal(data, 'content') - }) - } -}) - -test('cleanup', t => { - rimraf.sync(p) - t.end() -}) From 23ee88b3dd23bab154230a5a8bb786da2107330f Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Sat, 10 Aug 2019 16:52:14 -0400 Subject: [PATCH 27/34] Add support for withFileTypes to readdir --- graceful-fs.js | 7 ++----- package.json | 1 + promises.js | 3 ++- readdir-sort.js | 19 +++++++++++++++++++ test/readdir-options.js | 37 +++++++++++++++++++++++++++---------- 5 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 readdir-sort.js diff --git a/graceful-fs.js b/graceful-fs.js index 36b0e76..4e144d0 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -6,6 +6,7 @@ const polyfills = require('./polyfills.js') const clone = require('./clone.js') const normalizeArgs = require('./normalize-args.js') const {initQueue, retry, enqueue} = require('./retry-queue.js') +const readdirSort = require('./readdir-sort.js') const gracefulPatched = Symbol.for('graceful-fs.patched') @@ -109,11 +110,7 @@ function patch (fs) { fs.readdir = patchENFILE(fs.readdir, (args, cb) => [ args, (err, files) => { - if (files && files.sort) { - files = files.sort() - } - - cb(err, files) + cb(err, readdirSort(files)) } ]) diff --git a/package.json b/package.json index 704c842..d28f686 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "polyfills.js", "promises.js", "promise-windows-rename-polyfill.js", + "readdir-sort.js", "retry-queue.js", "windows-rename-polyfill.js" ], diff --git a/promises.js b/promises.js index 3ad930a..be1ff16 100644 --- a/promises.js +++ b/promises.js @@ -5,6 +5,7 @@ const {promisify} = require('util') const clone = require('./clone.js') const chownErFilter = require('./chown-er-filter.js') const {retry, enqueue} = require('./retry-queue.js') +const readdirSort = require('./readdir-sort.js') // This is the native C++ FileHandle const {FileHandle} = process.binding('fs') @@ -133,7 +134,7 @@ function patchPromises (fs, orig) { promises.readFile = patchAsyncENFILE(promises.readFile) promises.writeFile = patchAsyncENFILE(promises.writeFile) promises.appendFile = patchAsyncENFILE(promises.appendFile) - promises.readdir = patchAsyncENFILE(promises.readdir, files => files.sort()) + promises.readdir = patchAsyncENFILE(promises.readdir, readdirSort) promises.chmod = patchChown(promises.chmod) promises.lchmod = patchChown(promises.lchmod) diff --git a/readdir-sort.js b/readdir-sort.js new file mode 100644 index 0000000..90a171d --- /dev/null +++ b/readdir-sort.js @@ -0,0 +1,19 @@ +'use strict' + +const {Dirent} = require('fs') + +module.exports = files => { + if (!files || files.length === 0) { + return files + } + + if (typeof files[0] === 'object' && files[0].constructor === Dirent) { + if (typeof files[0].name === 'string') { + return files.sort((a, b) => a.name.localeCompare(b.name)) + } + + return files.sort((a, b) => a.name.toString().localeCompare(b.name.toString())) + } + + return files.sort() +} diff --git a/test/readdir-options.js b/test/readdir-options.js index 13201dd..64221f5 100644 --- a/test/readdir-options.js +++ b/test/readdir-options.js @@ -6,19 +6,29 @@ const {promisify} = require('util') let currentTest -const strings = ['b', 'z', 'a'] -const buffs = strings.map(s => Buffer.from(s)) -const hexes = buffs.map(b => b.toString('hex')) +function getRet (encoding, withFileTypes) { + const strings = ['b', 'z', 'a'] + const buffs = strings.map(s => Buffer.from(s)) + const hexes = buffs.map(b => b.toString('hex')) -function getRet (encoding) { + let results switch (encoding) { case 'hex': - return hexes + results = hexes + break case 'buffer': - return buffs + results = buffs + break default: - return strings + results = strings + break } + + if (withFileTypes) { + return results.map(name => new fs.Dirent(name)) + } + + return results } const emfile = () => Object.assign(new Error('synthetic emfile'), {code: 'EMFILE'}) @@ -41,7 +51,7 @@ fs.readdir = (path, options, cb) => { currentTest.isa(options, 'object') currentTest.ok(options) process.nextTick(() => { - cb(null, getRet(options.encoding)) + cb(null, getRet(options.encoding, options.withFileTypes)) }) } @@ -59,23 +69,30 @@ if (fs.promises) { failed = false currentTest.isa(options, 'object') currentTest.ok(options) - return getRet(options.encoding) + return getRet(options.encoding, options.withFileTypes) } } const g = require('./helpers/graceful-fs.js') +const sortDirEnts = (a, b) => a.name.toString().localeCompare(b.name.toString()) const encodings = ['buffer', 'hex', 'utf8', null] encodings.forEach(encoding => { const readdir = promisify(g.readdir) t.test('encoding=' + encoding, async t => { currentTest = t let files = await readdir('whatevers', {encoding}) - t.same(files, getRet(encoding).sort()) + t.same(files, getRet(encoding, false).sort()) + + files = await readdir('whatevers', {encoding, withFileTypes: true}) + t.same(files, getRet(encoding, true).sort(sortDirEnts)) if (g.promises) { files = await g.promises.readdir('whatevers', {encoding}) t.same(files, getRet(encoding).sort()) + + files = await g.promises.readdir('whatevers', {encoding, withFileTypes: true}) + t.same(files, getRet(encoding, true).sort(sortDirEnts)) } }) }) From 3e4abeea82a3911e46c1426f00bda13873c8395a Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Sun, 11 Aug 2019 01:34:06 -0400 Subject: [PATCH 28/34] Fix code style issues that slipped in * Prefer const * Space before function parentheses * Remove unused args --- test/avoid-memory-leak.js | 6 +++--- test/chdir.js | 2 +- test/eagain.js | 4 ++-- test/readdir-sort.js | 2 +- test/windows-rename-polyfill.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/avoid-memory-leak.js b/test/avoid-memory-leak.js index 6a18982..99a3ed1 100644 --- a/test/avoid-memory-leak.js +++ b/test/avoid-memory-leak.js @@ -24,12 +24,12 @@ function checkHeap (t) { ) } -async function gfsinit() { +async function gfsinit () { const gfsPath = path.resolve(__dirname, '../graceful-fs.js') const gfsHelper = path.join(__dirname, './helpers/graceful-fs.js') delete require.cache[gfsPath] - let fs = importFresh(gfsHelper) + const fs = importFresh(gfsHelper) // Force initialization of `fs.promises` if available if (fs.promises) { // fs.promises.open has an async initialization, this ensures it's fully loaded @@ -61,7 +61,7 @@ t.test('process is not repeatedly patched', t => { const polyfills = path.resolve(__dirname, '../polyfills.js') importFresh(polyfills) - let {cwd, chdir} = process + const {cwd, chdir} = process importFresh(polyfills) t.is(cwd, process.cwd, 'process.cwd not repeatedly patched') diff --git a/test/chdir.js b/test/chdir.js index 781f50f..a1e97d9 100644 --- a/test/chdir.js +++ b/test/chdir.js @@ -4,7 +4,7 @@ const path = require('path') const t = require('tap') const {chdir, cwd} = process -let hits = { +const hits = { chdir: 0, cwd: 0 } diff --git a/test/eagain.js b/test/eagain.js index e7c58e6..07c8a38 100644 --- a/test/eagain.js +++ b/test/eagain.js @@ -91,7 +91,7 @@ if (fs.promises) { const filehandle = await fs.promises.open(__filename, 'r') let counter = 0 const PromisesFileHandle = filehandlePromisesFileHandle(filehandle) - PromisesFileHandle.read = async (...args) => { + PromisesFileHandle.read = async () => { counter++ throw eagain() } @@ -104,7 +104,7 @@ if (fs.promises) { t.is(counter, 11) counter = 0 - PromisesFileHandle.read = async (...args) => { + PromisesFileHandle.read = async () => { counter++ if (counter !== 5) { throw eagain() diff --git a/test/readdir-sort.js b/test/readdir-sort.js index 7f8471a..168d03b 100644 --- a/test/readdir-sort.js +++ b/test/readdir-sort.js @@ -10,7 +10,7 @@ fs.readdir = (path, cb) => { } if (fs.promises) { - fs.promises.readdir = async (path) => { + fs.promises.readdir = async () => { return ['b', 'z', 'a'] } } diff --git a/test/windows-rename-polyfill.js b/test/windows-rename-polyfill.js index 045362d..a1efc24 100644 --- a/test/windows-rename-polyfill.js +++ b/test/windows-rename-polyfill.js @@ -20,7 +20,7 @@ function createPolyfilledObject (code) { if (fs.promises) { pfs.promises = { stat: fs.promises.stat, - async rename (a, b) { + async rename () { /* original rename */ throw Object.assign(new Error(code), {code}) } @@ -120,7 +120,7 @@ t.test('rename EPERM then stat EACCES', {timeout: 2000}, async t => { await t.rejects(promisify(pfs.rename)(a, b), {code: 'EPERM'}) if (pfs.promises) { - pfs.promises.stat = async p => { + pfs.promises.stat = async () => { throw Object.assign(new Error('EACCES'), {code: 'EACCES'}) } From 295c97220966aceb12ecf168d9e4efa361725828 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Sun, 11 Aug 2019 02:12:45 -0400 Subject: [PATCH 29/34] Emit ready event on stream open --- graceful-fs.js | 1 + test/read-write-stream.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/graceful-fs.js b/graceful-fs.js index 4e144d0..f73ada8 100644 --- a/graceful-fs.js +++ b/graceful-fs.js @@ -60,6 +60,7 @@ function patchStream (fs, isRead) { } else { this.fd = fd this.emit('open', fd) + this.emit('ready') if (isRead) { this.read() } diff --git a/test/read-write-stream.js b/test/read-write-stream.js index 227d4b7..ce1d87a 100644 --- a/test/read-write-stream.js +++ b/test/read-write-stream.js @@ -12,7 +12,7 @@ const p = fs.mkdtempSync(path.join(__dirname, 'temp-files-')) const paths = new Array(4097).fill().map((_, i) => `${p}/file-${i}`) test('write files', t => { - t.plan(paths.length * 2) + t.plan(paths.length * 4) for (const i in paths) { let stream switch (i % 3) { @@ -28,6 +28,8 @@ test('write files', t => { } t.type(stream, fs.WriteStream) + stream.on('open', fd => t.type(fd, 'number')) + stream.on('ready', () => t.pass('ready')) stream.on('finish', () => t.pass('success')) stream.write('content') stream.end() @@ -36,7 +38,7 @@ test('write files', t => { test('read files', t => { // now read them - t.plan(paths.length * 2) + t.plan(paths.length * 4) for (const i in paths) { let stream switch (i % 3) { @@ -52,6 +54,8 @@ test('read files', t => { } t.type(stream, fs.ReadStream) + stream.on('open', fd => t.type(fd, 'number')) + stream.on('ready', () => t.pass('ready')) let data = '' stream.on('data', c => { data += c @@ -70,6 +74,8 @@ function streamErrors (t, read, autoClose) { const matchDestroy = autoClose ? ['destroy'] : ['error', 'destroy'] const matchError = autoClose ? ['destroy', 'error'] : ['error'] const {destroy} = stream + stream.on('open', () => t.fail('unexpected open')) + stream.on('ready', () => t.fail('unexpected ready')) stream.destroy = () => { events.push('destroy') t.deepEqual(events, matchDestroy, 'got destroy') From d108be741bbb9be4502cef6b65cd01bc59f045c3 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Sun, 11 Aug 2019 02:47:38 -0400 Subject: [PATCH 30/34] Fix node.js 8 test issues --- nyc.config.js | 5 +++++ test/avoid-memory-leak.js | 3 +++ test/readdir-options.js | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nyc.config.js b/nyc.config.js index 728d777..9333085 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -9,6 +9,11 @@ if (!('O_SYMLINK' in fs.constants)) { ignore.push('lutimes-polyfill.js') } +if (!fs.Dirent) { + // Unavailable in node.js 8 + ignore.push('readdir-sort.js') +} + if (!Object.getOwnPropertyDescriptor(fs, 'promises')) { ignore.push('promises.js', 'promise-windows-rename-polyfill.js') } diff --git a/test/avoid-memory-leak.js b/test/avoid-memory-leak.js index 99a3ed1..c1bd789 100644 --- a/test/avoid-memory-leak.js +++ b/test/avoid-memory-leak.js @@ -53,7 +53,10 @@ t.test('no memory leak when loading multiple times', async t => { await gfsinit() } + // Two gc cycles seems to help with node.js 8 global.gc() + global.gc() + checkHeap(t) }) diff --git a/test/readdir-options.js b/test/readdir-options.js index 64221f5..f2c91d0 100644 --- a/test/readdir-options.js +++ b/test/readdir-options.js @@ -84,8 +84,10 @@ encodings.forEach(encoding => { let files = await readdir('whatevers', {encoding}) t.same(files, getRet(encoding, false).sort()) - files = await readdir('whatevers', {encoding, withFileTypes: true}) - t.same(files, getRet(encoding, true).sort(sortDirEnts)) + if (fs.Dirent) { + files = await readdir('whatevers', {encoding, withFileTypes: true}) + t.same(files, getRet(encoding, true).sort(sortDirEnts)) + } if (g.promises) { files = await g.promises.readdir('whatevers', {encoding}) From 32f2a75c2cdecfc4482f6cd690b29b90ec4cd4bf Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Sun, 11 Aug 2019 02:59:25 -0400 Subject: [PATCH 31/34] Fix testing for node.js 10, set enumerable for gfs.promises --- promises.js | 2 ++ test/eagain.js | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/promises.js b/promises.js index be1ff16..a9afbf9 100644 --- a/promises.js +++ b/promises.js @@ -115,6 +115,8 @@ async function setupOpen (fs, promises) { function patchPromises (fs, orig) { let promises Object.defineProperty(fs, 'promises', { + // enumerable is true in node.js 11+ where fs.promises is stable + enumerable: orig.enumerable, configurable: true, get () { if (!promises) { diff --git a/test/eagain.js b/test/eagain.js index 07c8a38..356032a 100644 --- a/test/eagain.js +++ b/test/eagain.js @@ -10,8 +10,9 @@ let cbArgs = [] const filehandlePromisesFileHandle = require('./helpers/promises.js') +const gfs = require('../graceful-fs.js') // We can't hijack the actual `fs` module so we have to fake it -const fs = require('../graceful-fs.js').gracefulify({ +const fs = gfs.gracefulify({ ...require('fs'), read (...args) { const cb = args.slice(-1)[0] @@ -85,10 +86,10 @@ test('readSync unresolved EAGAIN', t => { t.end() }) -if (fs.promises) { +if (gfs.promises) { test('promises read', async t => { t.ok(true) - const filehandle = await fs.promises.open(__filename, 'r') + const filehandle = await gfs.promises.open(__filename, 'r') let counter = 0 const PromisesFileHandle = filehandlePromisesFileHandle(filehandle) PromisesFileHandle.read = async () => { From 4d8ddeb6c5a07656842dad924f8af0b7c4db2807 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Mon, 12 Aug 2019 06:23:12 -0400 Subject: [PATCH 32/34] Fix win32 testing --- test/chown-er-ok.js | 3 +++ test/enoent.js | 6 +++++- test/readfile.js | 9 ++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/test/chown-er-ok.js b/test/chown-er-ok.js index 79671d8..33c4be8 100644 --- a/test/chown-er-ok.js +++ b/test/chown-er-ok.js @@ -3,6 +3,9 @@ const realFs = require('fs') const {promisify} = require('util') +// This test depends on chown-er-filter.js not seeing us as root. +process.getuid = () => 1000; + // For the fchown / fchmod do not accept `path` as the first parameter but // gfs doesn't duplicate this check so we can still verify that the errors // are ignored without added complexity in this test diff --git a/test/enoent.js b/test/enoent.js index 8f2502a..9358dbd 100644 --- a/test/enoent.js +++ b/test/enoent.js @@ -14,10 +14,14 @@ const methods = [ ['utimes', new Date(), new Date()], ['readdir'], ['readdir', {}], - ['chown', 0, 0], ['chmod', 0] ] +if (process.platform !== 'win32') { + // fs.chown does nothing on win32 (doesn't even check if the file exists) + methods.push(['chown', 0, 0]) +} + t.plan(methods.length) methods.forEach(([method, ...args]) => { t.test(method, async t => { diff --git a/test/readfile.js b/test/readfile.js index 7babb02..81ad334 100644 --- a/test/readfile.js +++ b/test/readfile.js @@ -33,8 +33,11 @@ test('read files', t => { t.equal(data, 'content') }) } +}) +test('cleanup', t => { rimraf.sync(dir) + t.end() }) if (fs.promises) { @@ -80,10 +83,6 @@ if (fs.promises) { t.is(results.length, paths.length) t.ok(results.every(r => r === 'content')) + rimraf.sync(tmpdir) }) } - -test('cleanup', t => { - rimraf.sync(tmpdir) - t.end() -}) From 7a7822ac29a8a6386f63031d5f37dae378520568 Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 14 Aug 2019 00:00:30 -0400 Subject: [PATCH 33/34] Hide fs.close and fs.closeSync previous symbols --- retry-queue.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/retry-queue.js b/retry-queue.js index c55dffc..3bb8e1d 100644 --- a/retry-queue.js +++ b/retry-queue.js @@ -53,14 +53,18 @@ function initQueue () { cb(err) }) } - fs.close[previous] = close + Object.defineProperty(fs.close, previous, { + value: close + }); fs.closeSync = fd => { // This function uses the graceful-fs shared queue closeSync(fd) retry() } - fs.closeSync[previous] = closeSync + Object.defineProperty(fs.closeSync, previous, { + value: closeSync + }); /* istanbul ignore next */ if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { From 14834841b2598d8de57ad3513067329a864dea8d Mon Sep 17 00:00:00 2001 From: Corey Farrell Date: Wed, 14 Aug 2019 20:01:07 -0400 Subject: [PATCH 34/34] Improve promises functionality * Avoid process.binding * Monkey-patch promises FileHandle.close to call retry * Do not use promisify * Eliminate delay on first open --- promises.js | 191 ++++++++++++++++++++------------------- retry-queue.js | 4 +- test/chown-er-ok.js | 15 ++- test/eagain.js | 10 +- test/helpers/promises.js | 9 -- test/open.js | 6 ++ test/promise-close.js | 70 ++++++++++++++ 7 files changed, 194 insertions(+), 111 deletions(-) delete mode 100644 test/helpers/promises.js create mode 100644 test/promise-close.js diff --git a/promises.js b/promises.js index a9afbf9..96e4fe9 100644 --- a/promises.js +++ b/promises.js @@ -1,26 +1,10 @@ 'use strict' -const {promisify} = require('util') - const clone = require('./clone.js') const chownErFilter = require('./chown-er-filter.js') const {retry, enqueue} = require('./retry-queue.js') const readdirSort = require('./readdir-sort.js') -// This is the native C++ FileHandle -const {FileHandle} = process.binding('fs') - -// We need the constructor of the object returned by -// fs.promises.open so we can extend it -async function getPromisesFileHandle (open) { - const handle = await open(__filename, 'r') - const PromisesFileHandle = handle.constructor - - await handle.close() - - return PromisesFileHandle -} - function patchChown (orig) { return (...args) => orig(...args).catch(er => { if (chownErFilter(er)) { @@ -49,104 +33,125 @@ function patchAsyncENFILE (origImpl, next = res => res) { return (...args) => new Promise((resolve, reject) => attempt(args, resolve, reject)) } -async function setupOpen (fs, promises) { - const PromisesFileHandle = await getPromisesFileHandle(promises.open) - class GracefulFileHandle extends PromisesFileHandle { - // constructor (filehandle) - // getAsyncId () - // get fd () - // datasync () - // sync () - // stat (options) - // truncate (len = 0) - // utimes (atime, mtime) - // write (buffer, offset, length, position) - - // fd is already open so no need to use `patchAsyncENFILE` functions from here - // appendFile (data, options) - // readFile (options) - // writeFile (data, options) - - async chmod (mode) { - return super.chmod(mode).catch(er => { - if (chownErFilter(er)) { - throw er - } - }) - } +async function patchFileHandleClose (promises) { + const filehandle = await promises.open(__filename, 'r') + const Klass = Object.getPrototypeOf(filehandle) + await filehandle.close() - async chown (uid, gid) { - return super.chown(uid, gid).catch(er => { - if (chownErFilter(er)) { - throw er - } - }) - } + const {close} = Klass + if (/graceful-fs replacement/.test(close.toString())) { + return + } + + Klass.close = async function (...args) { + /* graceful-fs replacement */ + await close.apply(this, args) + retry() + } + + // Just in case a promises FileHandle closed before we could monkey-patch it + retry() +} - async read (buffer, offset, length, position) { - let eagCounter = 0 - while (true) { +function initPromises (orig) { + // Checking `orig.value` handles situations where a user does + // something like require('graceful-fs).gracefulify({...require('fs')}) + const origPromises = orig.value || orig.get() + patchFileHandleClose(origPromises).catch( + /* istanbul ignore next: this should never happen */ + console.error + ) + + const promises = clone(origPromises) + promises.open = patchAsyncENFILE(promises.open, filehandle => { + /* It's too bad node.js makes it impossible to extend the + * actual filehandle class. */ + const replacementFns = { + async chmod (...args) { try { - return await super.read(buffer, offset, length, position) + await filehandle.chmod(...args) } catch (er) { - if (er.code === 'EAGAIN' && eagCounter < 10) { - eagCounter ++ - continue + if (chownErFilter(er)) { + throw er + } + } + }, + async chown (...args) { + try { + await filehandle.chown(...args) + } catch (er) { + if (chownErFilter(er)) { + throw er + } + } + }, + async read (...args) { + let eagCounter = 0 + while (true) { + try { + return await filehandle.read(...args) + } catch (er) { + if (er.code === 'EAGAIN' && eagCounter < 10) { + eagCounter ++ + continue + } + + throw er } - - throw er } } } - async close () { - await super.close() - retry() - } - } + return new Proxy(filehandle, { + get (filehandle, prop) { + if (!(prop in replacementFns)) { + const original = filehandle[prop] + if (typeof original !== 'function') { + return original + } + + replacementFns[prop] = original.bind(filehandle) + } + + return replacementFns[prop] + } + }) + }) - // fs.open is already patched, do not use patchAsyncENFILE here - const open = promisify(fs.open) - promises.open = async (...args) => { - return new GracefulFileHandle(new FileHandle(await open(...args))) + promises.readFile = patchAsyncENFILE(promises.readFile) + promises.writeFile = patchAsyncENFILE(promises.writeFile) + promises.appendFile = patchAsyncENFILE(promises.appendFile) + promises.readdir = patchAsyncENFILE(promises.readdir, readdirSort) + + promises.chmod = patchChown(promises.chmod) + promises.lchmod = patchChown(promises.lchmod) + promises.chown = patchChown(promises.chown) + promises.lchown = patchChown(promises.lchown) + + /* istanbul ignore next */ + if (process.platform === 'win32') { + require('./promise-windows-rename-polyfill.js')(promises) } + + return promises } function patchPromises (fs, orig) { let promises + /* istanbul ignore next: ignoring the version specific branch, initPromises is covered */ + if (orig.enumerable) { + // If enumerable is enabled fs.promises is not experimental, no warning + promises = initPromises(orig) + } + Object.defineProperty(fs, 'promises', { // enumerable is true in node.js 11+ where fs.promises is stable enumerable: orig.enumerable, configurable: true, get () { + /* istanbul ignore next: ignoring the version specific branch, initPromises is covered */ if (!promises) { - // Checking `orig.value` handles situations where a user does - // something like require('graceful-fs).gracefulify({...require('fs')}) - promises = clone(orig.value || orig.get()) - const initOpen = setupOpen(fs, promises) - - // This is temporary because node.js doesn't directly expose - // everything we need, some async operations are required to - // construct the real replacement for open - promises.open = async (...args) => { - await initOpen - return promises.open(...args) - } - - promises.readFile = patchAsyncENFILE(promises.readFile) - promises.writeFile = patchAsyncENFILE(promises.writeFile) - promises.appendFile = patchAsyncENFILE(promises.appendFile) - promises.readdir = patchAsyncENFILE(promises.readdir, readdirSort) - - promises.chmod = patchChown(promises.chmod) - promises.lchmod = patchChown(promises.lchmod) - promises.chown = patchChown(promises.chown) - promises.lchown = patchChown(promises.lchown) - - /* istanbul ignore next */ - if (process.platform === 'win32') { - require('./promise-windows-rename-polyfill.js')(promises) - } + promises = initPromises(orig) } return promises diff --git a/retry-queue.js b/retry-queue.js index 3bb8e1d..c495e5d 100644 --- a/retry-queue.js +++ b/retry-queue.js @@ -55,7 +55,7 @@ function initQueue () { } Object.defineProperty(fs.close, previous, { value: close - }); + }) fs.closeSync = fd => { // This function uses the graceful-fs shared queue @@ -64,7 +64,7 @@ function initQueue () { } Object.defineProperty(fs.closeSync, previous, { value: closeSync - }); + }) /* istanbul ignore next */ if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { diff --git a/test/chown-er-ok.js b/test/chown-er-ok.js index 33c4be8..a89363c 100644 --- a/test/chown-er-ok.js +++ b/test/chown-er-ok.js @@ -3,8 +3,9 @@ const realFs = require('fs') const {promisify} = require('util') +const realPromises = realFs.promises // This test depends on chown-er-filter.js not seeing us as root. -process.getuid = () => 1000; +process.getuid = () => 1000 // For the fchown / fchmod do not accept `path` as the first parameter but // gfs doesn't duplicate this check so we can still verify that the errors @@ -36,7 +37,6 @@ function makeErr (path, method) { } const fs = require('./helpers/graceful-fs.js') -const filehandlePromisesFileHandle = require('./helpers/promises.js') const {test} = require('tap') const errs = ['ENOSYS', 'EINVAL', 'EPERM'] @@ -63,14 +63,19 @@ errs.forEach(err => { }) if (fs.promises) { - test('FileHandle.chown / FileHandle.chmod', async t => { - const filehandle = await fs.promises.open(__filename, 'r') - const PromisesFileHandle = filehandlePromisesFileHandle(filehandle) + test('setup for promises', async () => { + const filehandle = await realPromises.open(__filename, 'r') + const PromisesFileHandle = Object.getPrototypeOf(filehandle) ;['chmod', 'chown'].forEach(method => { PromisesFileHandle[method] = async path => { throw makeErr(path, method) } }) + await filehandle.close() + }) + + test('FileHandle.chown / FileHandle.chmod', async t => { + const filehandle = await fs.promises.open(__filename, 'r') for (const err of errs) { await filehandle.chmod(err, 'some mode') diff --git a/test/eagain.js b/test/eagain.js index 356032a..95005b0 100644 --- a/test/eagain.js +++ b/test/eagain.js @@ -8,7 +8,7 @@ let readPing let readSyncPing let cbArgs = [] -const filehandlePromisesFileHandle = require('./helpers/promises.js') +const realPromises = require('fs').promises const gfs = require('../graceful-fs.js') // We can't hijack the actual `fs` module so we have to fake it @@ -87,11 +87,17 @@ test('readSync unresolved EAGAIN', t => { }) if (gfs.promises) { + let PromisesFileHandle + test('find PromisesFileHandle', async () => { + const filehandle = await realPromises.open(__filename, 'r') + PromisesFileHandle = Object.getPrototypeOf(filehandle) + await filehandle.close() + }) + test('promises read', async t => { t.ok(true) const filehandle = await gfs.promises.open(__filename, 'r') let counter = 0 - const PromisesFileHandle = filehandlePromisesFileHandle(filehandle) PromisesFileHandle.read = async () => { counter++ throw eagain() diff --git a/test/helpers/promises.js b/test/helpers/promises.js deleted file mode 100644 index 63104e3..0000000 --- a/test/helpers/promises.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -function filehandlePromisesFileHandle (filehandle) { - return Object.getPrototypeOf( - Object.getPrototypeOf(filehandle) - ) -} - -module.exports = filehandlePromisesFileHandle diff --git a/test/open.js b/test/open.js index 06bfc88..df817ad 100644 --- a/test/open.js +++ b/test/open.js @@ -24,10 +24,16 @@ test('open an existing file works', t => { if (fs.promises) { test('fs.promises.open an existing file works', async t => { + const stats = await fs.promises.stat(__filename) const filehandle = await fs.promises.open(__filename, 'r') t.type(filehandle.getAsyncId, 'function') t.type(filehandle.read, 'function') + t.type(filehandle.fd, 'number') + + const result = await filehandle.readFile('utf8') + t.type(result, 'string') + t.is(result.length, stats.size); await filehandle.close() }) diff --git a/test/promise-close.js b/test/promise-close.js new file mode 100644 index 0000000..350df76 --- /dev/null +++ b/test/promise-close.js @@ -0,0 +1,70 @@ +'use strict' + +const fs = require('fs') +const {promisify} = require('util') +const t = require('tap') +const {enqueue} = require('../retry-queue.js') + +const promises = Object.getOwnPropertyDescriptor(fs, 'promises') +const delay = promisify(setTimeout) + +if (!promises) { + t.pass('nothing to do') + process.exit(0) +} + +const gfs = require('./helpers/graceful-fs.js') + +let hit = false +const retryHit = () => { + hit = true +} + +const enqueueHit = () => enqueue([retryHit, []]) + +function testHits (msg) { + t.ok(hit, msg) + hit = false +} + +async function testFunction () { + await delay(0) + enqueueHit() + + if (!promises.enumerable) { + hit = false + // Force initialization of promises + gfs.promises + } + + await delay(50) + testHits('gfs.promises close initialization did retry') + + enqueueHit() + let filehandle = await gfs.promises.open(__filename) + t.notOk(hit, 'gfs.promises.open delays retry') + await delay(0) + testHits('gfs.promises.open retry on nextTick') + + enqueueHit() + await filehandle.close() + testHits('filehandle.close caused immediate retry') + + if (!process.env.TEST_GFS_GLOBAL_PATCH) { + enqueueHit() + filehandle = await fs.promises.open(__filename) + t.notOk(hit, 'no retry from fs.promises.open') + await delay(0) + t.notOk(hit, 'fs.promises.open no retry on nextTick') + + await filehandle.close() + testHits('filehandle.close caused immediate retry') + } +} + +t.resolves(testFunction(), 'test function resolves') + .then(() => t.end()) + .catch(error => { + console.error(error) + t.fail() + })