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 --parallel flag to lerna run #796

Merged
merged 1 commit into from May 1, 2017
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 11 additions & 3 deletions README.md
Expand Up @@ -442,19 +442,27 @@ List all of the public packages in the current Lerna repo.
### run

```sh
$ lerna run [script] # runs npm run my-script in all packages that have it
$ lerna run <script> -- [..args] # runs npm run my-script in all packages that have it
$ lerna run test
$ lerna run build

# watch all packages and transpile on change, streaming prefixed output
$ lerna run --parallel watch
```

Run an [npm script](https://docs.npmjs.com/misc/scripts) in each package that contains that script.
Run an [npm script](https://docs.npmjs.com/misc/scripts) in each package that contains that script. A double-dash (`--`) is necessary to pass dashed arguments to the script execution.

`lerna run` respects the `--concurrency`, `--scope` and `ignore` flags (see [Flags](#flags)).
`lerna run` respects the `--concurrency`, `--scope`, `--ignore`, `--stream`, and `--parallel` flags (see [Flags](#flags)).

```sh
$ lerna run --scope my-component test
```

> Note: It is advised to constrain the scope of this command (and `lerna exec`,
> below) when using the `--parallel` flag, as spawning dozens of subprocesses
> may be harmful to your shell's equanimity (or maximum file descriptor limit,
> for example). YMMV

### exec

```sh
Expand Down
42 changes: 37 additions & 5 deletions src/commands/RunCommand.js
@@ -1,3 +1,5 @@
import async from "async";

import Command from "../Command";
import NpmUtilities from "../NpmUtilities";
import output from "../utils/output";
Expand All @@ -14,8 +16,14 @@ export const describe = "Run an npm script in each package that contains that sc
export const builder = {
"stream": {
group: "Command Options:",
describe: "Stream output with lines prefixed by package."
}
describe: "Stream output with lines prefixed by package.",
type: "boolean",
Copy link
Member Author

Choose a reason for hiding this comment

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

Discovered during the writing of new integration tests, without this explicit type one could cause a mangling of the arguments passed in by innocently re-ordering them. Obviously not very ideal.

},
"parallel": {
group: "Command Options:",
describe: "Run script in all packages with unlimited concurrency, streaming prefixed output",
type: "boolean",
},
};

export default class RunCommand extends Command {
Expand All @@ -40,6 +48,11 @@ export default class RunCommand extends Command {
return;
}

if (this.options.parallel || this.options.stream) {
// don't interrupt streaming stdio
this.logger.disableProgress();
}

this.batchedPackages = this.toposort
? PackageUtilities.topologicallyBatchPackages(this.packagesWithScript)
: [this.packagesWithScript];
Expand All @@ -48,18 +61,24 @@ export default class RunCommand extends Command {
}

execute(callback) {
this.runScriptInPackages((err) => {
const finish = (err) => {
if (err) {
callback(err);
} else {
this.logger.success("run", `Ran npm script '${this.script}' in packages:`);
this.logger.success("", this.packagesWithScript.map((pkg) => `- ${pkg.name}`).join("\n"));
callback(null, true);
}
});
};

if (this.options.parallel) {
this.runScriptInPackagesParallel(finish);
} else {
this.runScriptInPackagesBatched(finish);
}
}

runScriptInPackages(callback) {
runScriptInPackagesBatched(callback) {
PackageUtilities.runParallelBatches(this.batchedPackages, (pkg) => (done) => {
this.runScriptInPackage(pkg, done);
}, this.concurrency, callback);
Expand All @@ -73,6 +92,19 @@ export default class RunCommand extends Command {
}
}

runScriptInPackagesParallel(callback) {
this.logger.info(
"run",
"in %d package(s): npm run %s",
this.packagesWithScript.length,
[this.script].concat(this.args).join(" ")
);

async.parallel(this.packagesWithScript.map((pkg) => (done) => {
this.runScriptInPackageStreaming(pkg, done);
}), callback);
}

runScriptInPackageStreaming(pkg, callback) {
NpmUtilities.runScriptInPackageStreaming(this.script, this.args, pkg, callback);
}
Expand Down
22 changes: 22 additions & 0 deletions test/RunCommand.js
Expand Up @@ -146,6 +146,28 @@ describe("RunCommand", () => {
}));
});
});

it("runs a script in all packages with --parallel", (done) => {
const runCommand = new RunCommand(["env"], {
parallel: true,
}, testDir);

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

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

try {
expect(ranInPackagesStreaming(testDir))
.toMatchSnapshot("run <script> --parallel");

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

describe("with --include-filtered-dependencies", () => {
Expand Down
9 changes: 9 additions & 0 deletions test/__snapshots__/RunCommand.js.snap
Expand Up @@ -6,6 +6,15 @@ Array [
]
`;

exports[`run <script> --parallel 1`] = `
Array [
"packages/package-1 env",
"packages/package-2 env",
"packages/package-3 env",
"packages/package-4 env",
]
`;

exports[`run <script> --scope @test/package-2 --include-filtered-dependencies 1`] = `
Array [
"packages/package-1 my-script",
Expand Down
@@ -1,6 +1,9 @@
{
"name": "package-1",
"version": "1.0.0",
"devDependencies": {
"package-3": "^1.0.0"
},
"scripts": {
"test": "echo package-1"
}
Expand Down
Expand Up @@ -2,6 +2,9 @@
"name": "package-2",
"version": "1.0.0",
"dependencies": {
"package-1": "^1.0.0"
"package-3": "^1.0.0"
},
"scripts": {
"test": "echo package-2"
}
}
@@ -1,7 +1,7 @@
{
"name": "package-3",
"version": "1.0.0",
"devDependencies": {
"package-2": "^1.0.0"
"scripts": {
"test": "echo package-3"
}
}
34 changes: 34 additions & 0 deletions test/integration/__snapshots__/lerna-run.test.js.snap
@@ -1,5 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`stderr: my-script --parallel 1`] = `
"lerna info version __TEST_VERSION__
lerna info run in 2 package(s): npm run my-script --silent
lerna success run Ran npm script 'my-script' in packages:
lerna success - package-1
lerna success - package-3"
`;

exports[`stderr: my-script --scope 1`] = `
"lerna info version __TEST_VERSION__
lerna info scope package-1
Expand All @@ -14,6 +22,32 @@ lerna success run Ran npm script 'test' in packages:
lerna success - package-4"
`;

exports[`stderr: test --parallel 1`] = `
"lerna info version __TEST_VERSION__
lerna info run in 4 package(s): npm run test --silent
lerna success run Ran npm script 'test' in packages:
lerna success - package-1
lerna success - package-2
lerna success - package-3
lerna success - package-4"
`;

exports[`stderr: test --stream 1`] = `
"lerna info version __TEST_VERSION__
lerna success run Ran npm script 'test' in packages:
lerna success - package-1
lerna success - package-2
lerna success - package-3
lerna success - package-4"
`;

exports[`stdout: my-script --scope 1`] = `"package-1"`;

exports[`stdout: test --ignore 1`] = `"package-4"`;

exports[`stdout: test --stream 1`] = `
"package-3: package-3
package-4: package-4
package-1: package-1
package-2: package-2"
`;
58 changes: 58 additions & 0 deletions test/integration/lerna-run.test.js
Expand Up @@ -3,6 +3,11 @@ import execa from "execa";
import { LERNA_BIN } from "../helpers/constants";
import initFixture from "../helpers/initFixture";

/**
* NOTE: We do not test the "missing test script" case here
* because Windows makes the snapshots impossible to stabilize.
**/

describe("lerna run", () => {
test.concurrent("my-script --scope", async () => {
const cwd = await initFixture("RunCommand/basic");
Expand Down Expand Up @@ -34,4 +39,57 @@ describe("lerna run", () => {
expect(stdout).toMatchSnapshot("stdout: test --ignore");
expect(stderr).toMatchSnapshot("stderr: test --ignore");
});

test.concurrent("test --stream", async () => {
const cwd = await initFixture("RunCommand/integration-lifecycle");
const args = [
"run",
"--stream",
"test",
"--concurrency=1",
// args below tell npm to be quiet
"--", "--silent",
];
const { stdout, stderr } = await execa(LERNA_BIN, args, { cwd });
expect(stdout).toMatchSnapshot("stdout: test --stream");
expect(stderr).toMatchSnapshot("stderr: test --stream");
});

test.concurrent("test --parallel", async () => {
const cwd = await initFixture("RunCommand/integration-lifecycle");
const args = [
"run",
"test",
"--parallel",
// args below tell npm to be quiet
"--", "--silent",
];
const { stdout, stderr } = await execa(LERNA_BIN, args, { cwd });
expect(stderr).toMatchSnapshot("stderr: test --parallel");

// order is non-deterministic, so assert each item seperately
expect(stdout).toMatch("package-1: package-1");
expect(stdout).toMatch("package-2: package-2");
expect(stdout).toMatch("package-3: package-3");
expect(stdout).toMatch("package-4: package-4");
});

test.concurrent("my-script --parallel", async () => {
const cwd = await initFixture("RunCommand/basic");
const args = [
"run",
"--parallel",
"my-script",
// args below tell npm to be quiet
"--", "--silent",
];
const { stdout, stderr } = await execa(LERNA_BIN, args, { cwd });
expect(stderr).toMatchSnapshot("stderr: my-script --parallel");

// order is non-deterministic, so assert each item seperately
expect(stdout).toMatch("package-1: package-1");
expect(stdout).not.toMatch("package-2");
expect(stdout).toMatch("package-3: package-3");
expect(stdout).not.toMatch("package-4");
});
});