Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --only-updated option to exec and run subcommands #726

Merged
merged 12 commits into from
May 18, 2017
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 @@ -33,8 +39,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
Original file line number Diff line number Diff line change
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 @@ -36,10 +41,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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