diff --git a/lib/shameimaru.js b/lib/shameimaru.js index 98cc960..9ed5a8a 100644 --- a/lib/shameimaru.js +++ b/lib/shameimaru.js @@ -1,5 +1,6 @@ "use strict"; +const { CancellationError } = require("bluebird"); const fs = require("fs"); const path = require("path"); @@ -11,14 +12,32 @@ class Shameimaru { this.projDir = projDir; this.nodeModuleDir = path.resolve(process.cwd(), projDir, "node_modules"); this.package = JSON.parse(fs.readFileSync(path.join(projDir, "package.json"), "utf8")); + this.traversePromise = null; } async traverse() { - const ret = await traverse( + this.traversePromise = traverse( utils.extraDependenciesFromPackage(this.package), this.nodeModuleDir); + + const ret = await new Promise((resolve, reject) => { + this.traversePromise.then(resolve, reject).finally(() => { + // onFulfilled & onFailure won't be called if the promise was cancelled, so we need to handle cancellation mannually + if (this.traversePromise.isCancelled()) { + reject(new CancellationError()); + } + }); + }); return ret; } + + cancel() { + if (!this.traversePromise) { + return; + } + + this.traversePromise.cancel(); + } } module.exports = Shameimaru; diff --git a/lib/traverse.js b/lib/traverse.js index e672239..e06b963 100644 --- a/lib/traverse.js +++ b/lib/traverse.js @@ -6,6 +6,9 @@ const fs = require("mz/fs"); const Linklist = require("algorithmjs").ds.Linklist; const npa = require("npm-package-arg"); const uuid = require("uuid/v4"); +const Bluebird = require("bluebird"); + +Bluebird.config({ cancellation: true }); const utils = require("./utils"); @@ -192,7 +195,7 @@ async function scanDir(q, node, ancestors, refs) { } } -async function traverse(rootDependencies, rootDir) { +function traverse(rootDependencies, rootDir) { const q = new Linklist(); const rootTree = {}; const ancestors = {}; @@ -205,12 +208,25 @@ async function traverse(rootDependencies, rootDir) { dummyFolder: "/" }); - while(q.length) { + let isCanceled = false; + const run = async () => { + // when cancelled or the queue is empty, stop and return + if (isCanceled || !q.length) { + return rootTree; + } + const node = q.popFront(); await scanDir(q, node, ancestors, refs); + return run(); } - return rootTree; + return new Bluebird((resolve, reject, onCancel) => { + onCancel(() => { + isCanceled = true; + }); + + run().then(resolve, reject); + }) ; } module.exports = traverse; diff --git a/package.json b/package.json index e63ce4c..fd36304 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Shameimaru Aya likes to traverse node_modules and capture the tree", "dependencies": { "algorithmjs": "^1.0.0", + "bluebird": "^3.7.2", "mz": "^2.7.0", "npm-package-arg": "^6.1.0", "uuid": "^3.2.1" diff --git a/test/app1.test.js b/test/app1.test.js index 81d9b71..60b8e87 100644 --- a/test/app1.test.js +++ b/test/app1.test.js @@ -2,6 +2,7 @@ require("should"); +const { CancellationError } = require("bluebird"); const Shameimaru = require("../"); const utils = require("./utils"); @@ -20,6 +21,17 @@ describe("app1.test.js", () => { // require("fs").writeFileSync("./test/fixtures/apps/app1/target.npm.json", JSON.stringify(tree, 0, 2)); }); + + it("#traverse canncel", async function() { + const shameimaru = new Shameimaru("./test/fixtures/apps/app1"); + console.time("app1#npm"); + const promise = shameimaru.traverse(); + setTimeout(() => { + shameimaru.cancel(); + }, 0); + console.timeEnd("app1#npm"); + promise.should.rejectedWith(CancellationError); + }) }); describe("cnpm 6", () => {