Skip to content

Commit

Permalink
Add --only-updated option to exec and run subcommands (#726)
Browse files Browse the repository at this point in the history
  • Loading branch information
jameslnewell authored and evocateur committed May 18, 2017
1 parent 64a01ac commit 22bad8b
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 3 deletions.
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -674,6 +674,20 @@ $ lerna bootstrap --scope "package-*" --ignore "package-util-*" --include-filter
# package matched by "package-*"
```

#### --only-updated

When executing a script or command, only run the script or command on packages that have been updated since the last release.

A package is considered "updated" using the same rules as `lerna updated`.

```sh
$ lerna exec --only-updated -- ls -la
```

```
$ lerna run --only-updated test
```

#### --loglevel [silent|error|warn|success|info|verbose|silly]

What level of logs to report. On failure, all logs are written to lerna-debug.log in the current working directory.
Expand Down
7 changes: 7 additions & 0 deletions src/PackageUtilities.js
Expand Up @@ -157,6 +157,13 @@ export default class PackageUtilities {
return packages;
}

static filterPackagesThatAreNotUpdated(packagesToFilter, packageUpdates) {
return packageUpdates
.map((update) => update.package)
.filter((pkg) => packagesToFilter.some((p) => p.name === pkg.name))
;
}

static topologicallyBatchPackages(packagesToBatch, { depsOnly } = {}) {
// We're going to be chopping stuff out of this array, so copy it.
const packages = packagesToBatch.slice();
Expand Down
18 changes: 17 additions & 1 deletion src/commands/ExecCommand.js
@@ -1,8 +1,10 @@
import async from "async";

import ChildProcessUtilities from "../ChildProcessUtilities";

import Command from "../Command";
import PackageUtilities from "../PackageUtilities";
import UpdatedPackagesCollector from "../UpdatedPackagesCollector";

export function handler(argv) {
return new ExecCommand([argv.command, ...argv.args], argv).run();
Expand All @@ -13,6 +15,10 @@ export const command = "exec <command> [args..]";
export const describe = "Run an arbitrary command in each package.";

export const builder = {
"only-updated": {
"describe": "When executing scripts/commands, only run the script/command on packages which "
+ "have been updated since the last release"
},
"parallel": {
group: "Command Options:",
describe: "Run command in all packages with unlimited concurrency, streaming prefixed output",
Expand All @@ -37,8 +43,18 @@ export default class ExecCommand extends Command {
// don't interrupt spawned or streaming stdio
this.logger.disableProgress();

let filteredPackages = this.filteredPackages;
if (this.flags.onlyUpdated) {
const updatedPackagesCollector = new UpdatedPackagesCollector(this);
const packageUpdates = updatedPackagesCollector.getUpdates();
filteredPackages = PackageUtilities.filterPackagesThatAreNotUpdated(
filteredPackages,
packageUpdates
);
}

this.batchedPackages = this.toposort
? PackageUtilities.topologicallyBatchPackages(this.filteredPackages)
? PackageUtilities.topologicallyBatchPackages(filteredPackages)
: [this.filteredPackages];

callback(null, true);
Expand Down
19 changes: 17 additions & 2 deletions src/commands/RunCommand.js
Expand Up @@ -4,6 +4,7 @@ import Command from "../Command";
import NpmUtilities from "../NpmUtilities";
import output from "../utils/output";
import PackageUtilities from "../PackageUtilities";
import UpdatedPackagesCollector from "../UpdatedPackagesCollector";

export function handler(argv) {
return new RunCommand([argv.script, ...argv.args], argv).run();
Expand All @@ -19,6 +20,10 @@ export const builder = {
describe: "Stream output with lines prefixed by package.",
type: "boolean",
},
"only-updated": {
"describe": "When executing scripts/commands, only run the script/command on packages which "
+ "have been updated since the last release"
},
"parallel": {
group: "Command Options:",
describe: "Run script in all packages with unlimited concurrency, streaming prefixed output",
Expand All @@ -40,10 +45,20 @@ export default class RunCommand extends Command {
return;
}

let filteredPackages = this.filteredPackages;
if (this.flags.onlyUpdated) {
const updatedPackagesCollector = new UpdatedPackagesCollector(this);
const packageUpdates = updatedPackagesCollector.getUpdates();
filteredPackages = PackageUtilities.filterPackagesThatAreNotUpdated(
filteredPackages,
packageUpdates
);
}

if (this.script === "test" || this.script === "env") {
this.packagesWithScript = this.filteredPackages;
this.packagesWithScript = filteredPackages;
} else {
this.packagesWithScript = this.filteredPackages
this.packagesWithScript = filteredPackages
.filter((pkg) => pkg.scripts && pkg.scripts[this.script]);
}

Expand Down
36 changes: 36 additions & 0 deletions test/ExecCommand.js
Expand Up @@ -3,6 +3,7 @@ import path from "path";

// mocked modules
import ChildProcessUtilities from "../src/ChildProcessUtilities";
import UpdatedPackagesCollector from "../src/UpdatedPackagesCollector";

// helpers
import callsBack from "./helpers/callsBack";
Expand All @@ -13,6 +14,7 @@ import initFixture from "./helpers/initFixture";
import ExecCommand from "../src/commands/ExecCommand";

jest.mock("../src/ChildProcessUtilities");
jest.mock("../src/UpdatedPackagesCollector");

// silence logs
log.level = "silent";
Expand Down Expand Up @@ -110,6 +112,40 @@ describe("ExecCommand", () => {
}));
});

it("should filter packages that are not updated when onlyUpdate", (done) => {

UpdatedPackagesCollector.prototype.getUpdates = jest.fn(() => [{ package: {
name: "package-2",
location: path.join(testDir, "packages/package-2")
} }]);

const execCommand = new ExecCommand(["ls"], {
onlyUpdated: true,
}, testDir);

execCommand.runValidations();
execCommand.runPreparations();

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

try {
expect(ChildProcessUtilities.spawn).toHaveBeenCalledTimes(1);
expect(ChildProcessUtilities.spawn).lastCalledWith("ls", [], {
cwd: path.join(testDir, "packages/package-2"),
env: expect.objectContaining({
LERNA_PACKAGE_NAME: "package-2",
}),
shell: true,
}, expect.any(Function));

done();
} catch (ex) {
done.fail(ex);
}
}));
});

it("should run a command", (done) => {
const execCommand = new ExecCommand(["ls"], {}, testDir);

Expand Down
51 changes: 51 additions & 0 deletions test/PackageUtilities.js
Expand Up @@ -182,6 +182,57 @@ describe("PackageUtilities", () => {
});
});

describe(".filterPackagesThatAreNotUpdated()", () => {

it("should return an empty array when there are no packages to filter", () => {
const packagesToFilter = [];
const packageUpdates = [
{ package: { name: "ghi" } },
{ package: { name: "xyz" } }
];
const updatedPackages = PackageUtilities.filterPackagesThatAreNotUpdated(
packagesToFilter,
packageUpdates
);
expect(updatedPackages).toHaveLength(0);
});

it("should return an empty array when there are no packages that have been updated", () => {
const packagesToFilter = [
{ name: "abc" },
{ name: "def" },
{ name: "ghi" },
{ name: "jkl" },
];
const packageUpdates = [];
const updatedPackages = PackageUtilities.filterPackagesThatAreNotUpdated(
packagesToFilter,
packageUpdates
);
expect(updatedPackages).toHaveLength(0);
});

it("should return only packages that are to be filtered and have been updated", () => {
const packagesToFilter = [
{ name: "abc" },
{ name: "def" },
{ name: "ghi" },
{ name: "jkl" },
];
const packageUpdates = [
{ package: { name: "ghi" } },
{ package: { name: "xyz" } },
];
const updatedPackages = PackageUtilities.filterPackagesThatAreNotUpdated(
packagesToFilter,
packageUpdates
);
expect(updatedPackages).toHaveLength(1);
expect(updatedPackages).toEqual([{ name: "ghi" }]);
});

});

describe(".topologicallyBatchPackages()", () => {
let packages;

Expand Down
32 changes: 32 additions & 0 deletions test/RunCommand.js
@@ -1,8 +1,10 @@
import path from "path";
import log from "npmlog";

// mocked modules
import NpmUtilities from "../src/NpmUtilities";
import output from "../src/utils/output";
import UpdatedPackagesCollector from "../src/UpdatedPackagesCollector";

// helpers
import callsBack from "./helpers/callsBack";
Expand Down Expand Up @@ -147,6 +149,34 @@ describe("RunCommand", () => {
});
});

it("should filter packages that are not updated when onlyUpdate", (done) => {
UpdatedPackagesCollector.prototype.getUpdates = jest.fn(() => [{ package: {
name: "package-3",
location: path.join(testDir, "packages/package-3"),
scripts: { "my-script": "echo package-3" }
} }]);

const runCommand = new RunCommand(["my-script"], {
onlyUpdated: true,
}, testDir);

runCommand.runValidations();
runCommand.runPreparations();

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

try {
expect(ranInPackages(testDir))
.toMatchSnapshot("run <script> --only-updated");

done();
} catch (ex) {
done.fail(ex);
}
}));
});

it("runs a script in all packages with --parallel", (done) => {
const runCommand = new RunCommand(["env"], {
parallel: true,
Expand All @@ -168,6 +198,7 @@ describe("RunCommand", () => {
}
}));
});

});

describe("with --include-filtered-dependencies", () => {
Expand Down Expand Up @@ -200,4 +231,5 @@ describe("RunCommand", () => {
}));
});
});

});
6 changes: 6 additions & 0 deletions test/__snapshots__/RunCommand.js.snap
Expand Up @@ -6,6 +6,12 @@ Array [
]
`;
exports[`run <script> --only-updated 1`] = `
Array [
"packages/package-3 my-script",
]
`;
exports[`run <script> --parallel 1`] = `
Array [
"packages/package-1 env",
Expand Down

0 comments on commit 22bad8b

Please sign in to comment.