Merge pull request #2936 from node-red/npm-install-hooks

Add pre/postInstall hooks to npm install handling
This commit is contained in:
Nick O'Leary
2021-04-27 10:57:14 +01:00
committed by GitHub
13 changed files with 410 additions and 85 deletions

View File

@@ -14,6 +14,7 @@ const os = require("os");
const NR_TEST_UTILS = require("nr-test-utils");
const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec");
const hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
let homeDir;
@@ -40,19 +41,20 @@ describe("externalModules api", function() {
await createUserDir()
})
afterEach(async function() {
hooks.clear();
await fs.remove(homeDir);
})
describe("checkFlowDependencies", function() {
beforeEach(function() {
sinon.stub(exec,"run").callsFake(async function(cmd, args, options) {
let error;
if (args[1] === "moduleNotFound") {
if (args[2] === "moduleNotFound") {
error = new Error();
error.stderr = "E404";
} else if (args[1] === "moduleVersionNotFound") {
} else if (args[2] === "moduleVersionNotFound") {
error = new Error();
error.stderr = "ETARGET";
} else if (args[1] === "moduleFail") {
} else if (args[2] === "moduleFail") {
error = new Error();
error.stderr = "Some unexpected install error";
}
@@ -102,6 +104,45 @@ describe("externalModules api", function() {
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
})
it("calls pre/postInstall hooks", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.true();
// exec.run.lastCall.args[1].should.eql([ 'install', 'a', 'foo' ]);
receivedPreEvent.should.have.property("module","foo")
receivedPreEvent.should.have.property("version")
receivedPreEvent.should.have.property("dir")
receivedPreEvent.should.eql(receivedPostEvent)
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
})
it("skips npm install if preInstall returns false", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { receivedPreEvent = event; return false })
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.false();
receivedPreEvent.should.have.property("module","foo")
receivedPreEvent.should.have.property("version")
receivedPreEvent.should.have.property("dir")
receivedPreEvent.should.eql(receivedPostEvent)
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
})
it("installs missing modules from inside subflow module", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
@@ -299,4 +340,4 @@ describe("externalModules api", function() {
}
})
})
});
});

View File

@@ -25,7 +25,7 @@ var NR_TEST_UTILS = require("nr-test-utils");
var installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer");
var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry/lib/registry");
const { events, exec, log } = NR_TEST_UTILS.require("@node-red/util");
const { events, exec, log, hooks } = NR_TEST_UTILS.require("@node-red/util");
describe('nodes/registry/installer', function() {
@@ -68,6 +68,7 @@ describe('nodes/registry/installer', function() {
fs.statSync.restore();
}
exec.run.restore();
hooks.clear();
});
describe("installs module", function() {
@@ -251,6 +252,70 @@ describe('nodes/registry/installer', function() {
}).catch(done);
});
it("triggers preInstall and postInstall hooks", function(done) {
let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
var res = {code: 0,stdout:"",stderr:""}
var p = Promise.resolve(res);
p.catch((err)=>{});
execResponse = p;
var addModule = sinon.stub(registry,"addModule").callsFake(function(md) {
return Promise.resolve(nodeInfo);
});
installer.installModule("this_wont_exist","1.2.3").then(function(info) {
exec.run.called.should.be.true();
exec.run.lastCall.args[1].should.eql([ 'install', 'a', 'this_wont_exist@1.2.3' ]);
info.should.eql(nodeInfo);
should.exist(receivedPreEvent)
receivedPreEvent.should.have.property("module","this_wont_exist")
receivedPreEvent.should.have.property("version","1.2.3")
receivedPreEvent.should.have.property("dir")
receivedPreEvent.should.have.property("url")
receivedPreEvent.should.have.property("isExisting")
receivedPreEvent.should.have.property("isUpgrade")
receivedPreEvent.should.eql(receivedPostEvent)
done();
}).catch(done);
});
it("fails install if preInstall hook fails", function(done) {
let receivedEvent;
hooks.add("preInstall", function(event) { throw new Error("preInstall-error"); })
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
installer.installModule("this_wont_exist","1.2.3").catch(function(err) {
exec.run.called.should.be.false();
done();
}).catch(done);
});
it("skips invoking npm if preInstall returns false", function(done) {
let receivedEvent;
hooks.add("preInstall", function(event) { return false })
hooks.add("postInstall", function(event) { receivedEvent = event; })
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
installer.installModule("this_wont_exist","1.2.3").catch(function(err) {
exec.run.called.should.be.false();
should.exist(receivedEvent);
done();
}).catch(done);
});
it("rollsback install if postInstall hook fails", function(done) {
hooks.add("postInstall", function(event) { throw new Error("fail"); })
installer.installModule("this_wont_exist","1.2.3").catch(function(err) {
exec.run.calledTwice.should.be.true();
exec.run.firstCall.args[1].includes("install").should.be.true();
exec.run.secondCall.args[1].includes("remove").should.be.true();
done();
}).catch(done);
});
});
describe("uninstalls module", function() {
it("rejects invalid module names", function(done) {

View File

@@ -26,7 +26,7 @@ var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");

View File

@@ -19,7 +19,7 @@ var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils");
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var Log = NR_TEST_UTILS.require("@node-red/util").log;
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
describe('Node', function() {

View File

@@ -1,9 +1,9 @@
const should = require("should");
const NR_TEST_UTILS = require("nr-test-utils");
const hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
const hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
describe("runtime/hooks", function() {
describe("util/hooks", function() {
afterEach(function() {
hooks.clear();
})
@@ -81,7 +81,7 @@ describe("runtime/hooks", function() {
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.false();
done(err);
} catch(err2) {
done(err2);
@@ -121,7 +121,46 @@ describe("runtime/hooks", function() {
})
})
})
it("allows a hook to remove itself whilst being called", function(done) {
let data = { order: [] }
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
hooks.add("onSend.B", function(payload) {
hooks.remove("*.B");
})
hooks.add("onSend.C", function(payload) { payload.order.push("C") } )
hooks.add("onSend.D", function(payload) { payload.order.push("D") } )
hooks.trigger("onSend", data, err => {
try {
should.not.exist(err);
data.order.should.eql(["A","C","D"])
done();
} catch(e) {
done(e);
}
})
});
it("allows a hook to remove itself and others whilst being called", function(done) {
let data = { order: [] }
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
hooks.add("onSend.B", function(payload) {
hooks.remove("*.B");
hooks.remove("*.C");
})
hooks.add("onSend.C", function(payload) { payload.order.push("C") } )
hooks.add("onSend.D", function(payload) { payload.order.push("D") } )
hooks.trigger("onSend", data, err => {
try {
should.not.exist(err);
data.order.should.eql(["A","D"])
done();
} catch(e) {
done(e);
}
})
});
it("halts execution on return false", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A"); return false } )
@@ -249,4 +288,51 @@ describe("runtime/hooks", function() {
done();
})
})
it("handler can use callback function - promise API", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
payload.order.push("A")
done()
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data).then(() => {
data.order.should.eql(["A","B"])
done()
}).catch(done)
})
it("handler can halt execution - promise API", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
payload.order.push("A")
done(false)
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data).then(() => {
data.order.should.eql(["A"])
done()
}).catch(done)
})
it("handler can halt execution on error - promise API", function(done) {
hooks.add("onSend.A", function(payload, done) {
throw new Error("error");
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data).then(() => {
done("hooks.trigger resolved unexpectedly")
}).catch(err => {
done();
})
})
});