Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/FileSystemUtilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export default class FileSystemUtilities {
fs.writeFile(filePath, ensureEndsWithNewLine(fileContents), callback);
}

@logger.logifyAsync
static rename(from, to, callback) {
fs.rename(from, to, callback);
}

@logger.logifySync
static renameSync(from, to) {
fs.renameSync(from, to);
}

@logger.logifySync
static writeFileSync(filePath, fileContents) {
fs.writeFileSync(filePath, ensureEndsWithNewLine(fileContents));
Expand Down
60 changes: 55 additions & 5 deletions src/NpmUtilities.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import ChildProcessUtilities from "./ChildProcessUtilities";
import FileSystemUtilities from "./FileSystemUtilities";
import onExit from "signal-exit";
import logger from "./logger";
import escapeArgs from "command-join";
import path from "path";
Expand All @@ -7,18 +9,66 @@ import semver from "semver";
export default class NpmUtilities {
@logger.logifyAsync
static installInDir(directory, dependencies, callback) {
let args = ["install"];

if (dependencies) {
args = args.concat(dependencies);
}
// Nothing to do if we weren't given any deps.
if (!(dependencies && dependencies.length)) return callback();

const args = ["install"];

const opts = {
cwd: directory,
stdio: ["ignore", "ignore", "pipe"],
};

ChildProcessUtilities.spawn("npm", args, opts, callback);
const packageJson = path.join(directory, "package.json");
const packageJsonBkp = packageJson + ".asini_backup";

FileSystemUtilities.rename(packageJson, packageJsonBkp, (err) => {
if (err) return callback(err);

const cleanup = () => {

// Need to do this one synchronously because we might be doing it on exit.
FileSystemUtilities.renameSync(packageJsonBkp, packageJson);
};

// If we die we need to be sure to put things back the way we found them.
const unregister = onExit(cleanup);

// Construct a basic fake package.json with just the deps we need to install.
const tempJson = JSON.stringify({
dependencies: dependencies.reduce((deps, dep) => {
const [pkg, version] = NpmUtilities.splitVersion(dep);
deps[pkg] = version || "*";
return deps;
}, {})
});

// Write out our temporary cooked up package.json and then install.
FileSystemUtilities.writeFile(packageJson, tempJson, (err) => {

// We have a few housekeeping tasks to take care of whether we succeed or fail.
const done = (err) => {
cleanup();
unregister();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is cleanup() not guaranteed to run in unregister()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unregister() unschedules the onExit() cleanup, which we only need if we wind up exiting before this done() function is called.

callback(err);
};

if (err) {
return done(err);
} else {
ChildProcessUtilities.spawn("npm", args, opts, done);
}
});
});
}

// Take a dep like "foo@^1.0.0".
// Return a tuple like ["foo", "^1.0.0"].
// Handles scoped packages.
// Returns undefined for version if none specified.
static splitVersion(dep) {
return dep.match(/^(@?[^@]+)(?:@(.+))?/).slice(1, 3);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just love regexs 😖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I originally used a simple .split("@"), but then I remembered scoped packages. 😖

}

@logger.logifySync
Expand Down
167 changes: 130 additions & 37 deletions test/BootstrapCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,57 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

const want = {
[path.join(testDir, "package.json")]: {
dependencies: { "foo": "^1.0.0", "@test/package-1": "^0.0.0" }
},
[path.join(testDir, "packages" ,"package-3", "package.json")]: {
dependencies: { "foo": "0.1.12" }
},
};
const got = {};
stub(FileSystemUtilities, "writeFile", (fn, json, callback) => {
got[fn] = JSON.parse(json);
callback();
});

assertStubbedCalls([
[FileSystemUtilities, "rename", { nodeCallback: true }, [
{ args: [
path.join(testDir, "package.json"),
path.join(testDir, "package.json.asini_backup"),
] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0", "@test/package-1@^0.0.0"], { cwd: testDir, stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: testDir, stdio: STDIO_OPT }] }
]],
[FileSystemUtilities, "renameSync", { }, [
{ args: [
path.join(testDir, "package.json.asini_backup"),
path.join(testDir, "package.json"),
] }
]],
[FileSystemUtilities, "rename", { nodeCallback: true }, [
{ args: [
path.join(testDir, "packages" ,"package-3", "package.json"),
path.join(testDir, "packages" ,"package-3", "package.json.asini_backup"),
] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "[email protected]"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
]],
[FileSystemUtilities, "renameSync", { }, [
{ args: [
path.join(testDir, "packages" ,"package-3", "package.json.asini_backup"),
path.join(testDir, "packages" ,"package-3", "package.json"),
] }
]],
]);

bootstrapCommand.runCommand(exitWithCode(0, done));
bootstrapCommand.runCommand(exitWithCode(0, (err) => {
assert.deepEqual(got, want, "Installed the right deps");
done(err);
}));
});

it("should not hoist when disallowed", (done) => {
Expand All @@ -77,19 +118,39 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

const want = {
[path.join(testDir, "package.json")]: {
dependencies: { "foo": "^1.0.0" }
},
[path.join(testDir, "packages" ,"package-3", "package.json")]: {
dependencies: { "foo": "0.1.12" }
},
[path.join(testDir, "packages" ,"package-4", "package.json")]: {
dependencies: { "@test/package-1": "^0.0.0" }
},
};
const got = {};
stub(FileSystemUtilities, "writeFile", (fn, json, callback) => {
got[fn] = JSON.parse(json);
callback();
});

assertStubbedCalls([
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: testDir, stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: testDir, stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "[email protected]"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "@test/package-1@^0.0.0"], { cwd: path.join(testDir, "packages", "package-4"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages", "package-4"), stdio: STDIO_OPT }] }
]],
]);

bootstrapCommand.runCommand(exitWithCode(0, done));
bootstrapCommand.runCommand(exitWithCode(0, (err) => {
assert.deepEqual(got, want, "Installed the right deps");
done(err);
}));
});
});

Expand All @@ -107,20 +168,9 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

assertStubbedCalls([
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-1"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-2"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "[email protected]"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "@test/package-1@^0.0.0"], { cwd: path.join(testDir, "packages", "package-4"), stdio: STDIO_OPT }] }
]]
]);
stub(ChildProcessUtilities, "spawn", (command, args, options, callback) => {
callback();
});

bootstrapCommand.runCommand(exitWithCode(0, (err) => {
if (err) return done(err);
Expand Down Expand Up @@ -190,12 +240,18 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

const installed = [0,0,0];
stub(ChildProcessUtilities, "spawn", (command, args, options, callback) => {
installed[+options.cwd.match(/package-(\d)$/)[1]]++;
callback();
});

assertStubbedCalls([
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-1"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages", "package-1"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-2"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages", "package-2"), stdio: STDIO_OPT }] }
]]
]);

Expand All @@ -204,6 +260,7 @@ describe("BootstrapCommand", () => {

try {
assert.ok(!pathExists.sync(path.join(testDir, "asini-debug.log")), "asini-debug.log should not exist");
assert.deepEqual(installed, [0,1,1], "Did all our installs");

done();
} catch (err) {
Expand All @@ -220,12 +277,18 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

const installed = [0,0,0,0,0];
stub(ChildProcessUtilities, "spawn", (command, args, options, callback) => {
installed[+options.cwd.match(/package-(\d)$/)[1]]++;
callback();
});

assertStubbedCalls([
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "[email protected]"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages" ,"package-3"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "@test/package-1@^0.0.0"], { cwd: path.join(testDir, "packages", "package-4"), stdio: STDIO_OPT }] }
{ args: ["npm", ["install"], { cwd: path.join(testDir, "packages", "package-4"), stdio: STDIO_OPT }] }
]]
]);

Expand All @@ -234,6 +297,8 @@ describe("BootstrapCommand", () => {

try {
assert.ok(!pathExists.sync(path.join(testDir, "asini-debug.log")), "asini-debug.log should not exist");
assert.deepEqual(installed, [0,0,0,1,1], "Did all our installs");

// package-3 package dependencies are symlinked
assert.equal(
normalize(fs.readlinkSync(path.join(testDir, "packages", "package-3", "node_modules", "@test", "package-1"))),
Expand Down Expand Up @@ -291,6 +356,18 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

const want = {
[path.join(testDir, "packages", "package-1")]: true,
[path.join(testDir, "packages", "package-2")]: true,
[path.join(testDir, "package-3")]: true,
[path.join(testDir, "packages", "package-4")]: true,
};
const got = {};
stub(ChildProcessUtilities, "spawn", (command, args, options, callback) => {
got[options.cwd] = true;
callback();
});

assertStubbedCalls([
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-1"), stdio: STDIO_OPT }] }
Expand All @@ -311,6 +388,9 @@ describe("BootstrapCommand", () => {

try {
assert.ok(!pathExists.sync(path.join(testDir, "asini-debug.log")), "asini-debug.log should not exist");

assert.deepEqual(want, got, "Installed everywhere");

// Make sure the `prepublish` script got run (index.js got created).
assert.ok(pathExists.sync(path.join(testDir, "packages", "package-1", "index.js")));
// package-1 should not have any packages symlinked
Expand Down Expand Up @@ -376,21 +456,20 @@ describe("BootstrapCommand", () => {
bootstrapCommand.runValidations();
bootstrapCommand.runPreparations();

assertStubbedCalls([
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-1"), stdio: STDIO_OPT }] }
]],
[ChildProcessUtilities, "spawn", { nodeCallback: true }, [
{ args: ["npm", ["install", "foo@^1.0.0"], { cwd: path.join(testDir, "packages", "package-2"), stdio: STDIO_OPT }] }
]]
]);
const installed = [0,0,0];
stub(ChildProcessUtilities, "spawn", (command, args, options, callback) => {
installed[+options.cwd.match(/package-(\d)$/)[1]]++;
callback();
});

bootstrapCommand.runCommand(exitWithCode(0, (err) => {
if (err) return done(err);

try {
assert.ok(!pathExists.sync(path.join(testDir, "asini-debug.log")), "asini-debug.log should not exist");

assert.deepEqual(installed, [0,1,1], "Did all our installs");

done();
} catch (err) {
done(err);
Expand All @@ -415,6 +494,14 @@ describe("BootstrapCommand", () => {
let installed = 0;
let spawnArgs = [];
let spawnOptions = [];
const writeFns = [];
const writeJsons = [];

stub(FileSystemUtilities, "writeFile", (fn, json, callback) => {
writeFns.push(fn);
writeJsons.push(JSON.parse(json));
callback();
});

stub(ChildProcessUtilities, "spawn", (command, args, options, callback) => {
spawnArgs.push(args);
Expand All @@ -428,14 +515,20 @@ describe("BootstrapCommand", () => {

try {
assert.ok(!pathExists.sync(path.join(testDir, "asini-debug.log")), "asini-debug.log should not exist");
assert.deepEqual(spawnArgs, [
["install", "external@^1.0.0"],
["install", "external@^2.0.0"]
]);
assert.deepEqual(spawnArgs, [ ["install"], ["install"] ]);
assert.deepEqual(spawnOptions, [
{ cwd: path.join(testDir, "packages", "package-1"), stdio: STDIO_OPT },
{ cwd: path.join(testDir, "packages", "package-2"), stdio: STDIO_OPT }
]);
assert.deepEqual(writeFns, [
path.join(testDir, "packages", "package-1", "package.json"),
path.join(testDir, "packages", "package-2", "package.json"),
]);
assert.deepEqual(writeJsons, [
{ dependencies: {external: "^1.0.0"}},
{ dependencies: {external: "^2.0.0"}},
]);

assert.equal(installed, 2, "The external dependencies were installed");

done();
Expand Down