Skip to content

Commit

Permalink
feat(run): allow multiple script targets to be triggered at once (#3527)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesHenry committed Feb 5, 2023
1 parent aea79df commit 937b02a
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 5 deletions.
84 changes: 84 additions & 0 deletions e2e/run/task-runner/src/multiple-targets/assertions.spec.ts
@@ -0,0 +1,84 @@
import { Fixture, normalizeCommandOutput, normalizeEnvironment } from "@lerna/e2e-utils";

expect.addSnapshotSerializer({
serialize(str: string) {
return (
normalizeCommandOutput(normalizeEnvironment(str))
// Normalize the script names in the output otherwise it will be non-deterministic
.replaceAll("print-name-again", "XXXXXXXXXX")
.replaceAll("print-name", "XXXXXXXXXX")
);
},
test(val: string) {
return val != null && typeof val === "string";
},
});

describe("lerna-run-nx-multiple-targets", () => {
const fixtureRootPath = process.env.FIXTURE_ROOT_PATH;
let fixture: Fixture;

beforeAll(() => {
if (!fixtureRootPath) {
throw new Error("FIXTURE_ROOT_PATH environment variable is not set");
}
fixture = Fixture.fromExisting(process.env.E2E_ROOT, fixtureRootPath);
});

afterAll(() => fixture.destroy());

it("should run multiple comma-delimited targets concurrently", async () => {
const output = await fixture.readOutput("multiple-targets");

expect(output).toMatchInlineSnapshot(`
lerna notice cli v999.9.9-e2e.0
> Lerna (powered by Nx) Running targets XXXXXXXXXX, XXXXXXXXXX for 3 projects:
- package-X
- package-X
- package-X
> package-X:XXXXXXXXXX
> package-X@0.0.0 XXXXXXXXXX
> echo test-package-X
test-package-X
> package-X:XXXXXXXXXX
> package-X@0.0.0 XXXXXXXXXX
> echo test-package-X
test-package-X
> package-X:XXXXXXXXXX
> package-X@0.0.0 XXXXXXXXXX
> echo test-package-X
test-package-X
> package-X:XXXXXXXXXX
> package-X@0.0.0 XXXXXXXXXX
> echo test-package-X
test-package-X
> Lerna (powered by Nx) Successfully ran targets XXXXXXXXXX, XXXXXXXXXX for 3 projects
`);
});
});
16 changes: 16 additions & 0 deletions e2e/run/task-runner/src/multiple-targets/exec.sh
@@ -0,0 +1,16 @@
DIR=$(dirname "$0")
UPDATE_SNAPSHOTS=$1

# Inline the bash util functions
source "$DIR/../utils.sh"

# Initialize the Fixture and modify the generated fixture root path
declare FIXTURE_ROOT_PATH
declare E2E_ROOT
initializeFixture $DIR

# Run the relevant task runner commands and write stdout and stderr to a named file in each case (for later assertions)
npx lerna run print-name,print-name-again > $OUTPUTS/multiple-targets.txt 2>&1

# Run the assertions
runAssertions $DIR $E2E_ROOT $FIXTURE_ROOT_PATH $UPDATE_SNAPSHOTS
41 changes: 41 additions & 0 deletions e2e/run/task-runner/src/multiple-targets/init.ts
@@ -0,0 +1,41 @@
import { Fixture } from "@lerna/e2e-utils";

(async () => {
const fixture = await Fixture.create({
e2eRoot: process.env.E2E_ROOT,
name: "lerna-run-nx-multiple-targets",
packageManager: "npm",
initializeGit: true,
runLernaInit: true,
installDependencies: true,
});

await fixture.lerna("create package-1 -y");
await fixture.addScriptsToPackage({
packagePath: "packages/package-1",
scripts: {
// Just print-name
"print-name": "echo test-package-1",
},
});
await fixture.lerna("create package-2 -y");
await fixture.addScriptsToPackage({
packagePath: "packages/package-2",
scripts: {
// Just print-name-again
"print-name-again": "echo test-package-2",
},
});
await fixture.lerna("create package-3 -y");
await fixture.addScriptsToPackage({
packagePath: "packages/package-3",
scripts: {
// Both print-name and print-name-again
"print-name": "echo test-package-3",
"print-name-again": "echo test-package-3",
},
});

// Log the fixture's root path so that it can be captured in exec.sh
console.log((fixture as any).fixtureRootPath);
})();
10 changes: 10 additions & 0 deletions libs/commands/run/src/command.ts
Expand Up @@ -16,6 +16,16 @@ const command: CommandModule = {
.positional("script", {
describe: "The npm script to run. Pass flags to send to the npm client after --",
type: "string",
coerce: (script) => {
// Allow passing multiple scripts to run concurrently by comma-separating them
if (script.includes(",")) {
return script
.split(",")
.filter(Boolean)
.map((s: string) => s.trim());
}
return script;
},
})
.options({
"npm-client": {
Expand Down
21 changes: 17 additions & 4 deletions libs/commands/run/src/index.ts
Expand Up @@ -24,7 +24,7 @@ module.exports = function factory(argv: NodeJS.Process["argv"]) {
};

class RunCommand extends Command {
script: string;
script: string | string[];
args: string[];
npmClient: string;
bail: boolean;
Expand All @@ -41,7 +41,7 @@ class RunCommand extends Command {
this.args = this.options["--"] || [];
this.npmClient = npmClient;

if (!script) {
if (!this.script) {
throw new ValidationError("ENOSCRIPT", "You must specify a lifecycle script to run");
}

Expand All @@ -53,6 +53,14 @@ class RunCommand extends Command {
);
}

// Only the modern task runner supports multiple targets concurrently
if (Array.isArray(this.script) && this.options.useNx === false) {
throw new ValidationError(
"run",
"The legacy task runner does not support running multiple scripts concurrently. Please update to the latest version of lerna and ensure you do not have useNx set to false in your lerna.json."
);
}

// inverted boolean options
this.bail = this.options.bail !== false;
this.prefix = this.options.prefix !== false;
Expand All @@ -64,7 +72,12 @@ class RunCommand extends Command {
this.packagesWithScript =
script === "env"
? filteredPackages
: filteredPackages.filter((pkg) => pkg.scripts && pkg.scripts[script]);
: filteredPackages.filter((pkg) => {
if (Array.isArray(this.script)) {
return this.script.some((scriptName) => pkg.scripts && pkg.scripts[scriptName]);
}
return pkg.scripts && pkg.scripts[script];
});
});

return chain.then(() => {
Expand Down Expand Up @@ -232,7 +245,7 @@ class RunCommand extends Command {
return runMany(
{
projects,
target: this.script,
targets: Array.isArray(this.script) ? this.script : [this.script],
...options,
},
targetDependencies,
Expand Down
8 changes: 8 additions & 0 deletions libs/commands/run/src/lib/run-command.spec.ts
Expand Up @@ -53,6 +53,14 @@ describe("RunCommand", () => {
await expect(command).rejects.toThrow("You must specify a lifecycle script to run");
});

it("should error if invoked with multiple targets as that is only supported with the modern task-runner", async () => {
const command = lernaRun(testDir)("foo,bar");

await expect(command).rejects.toThrow(
"The legacy task runner does not support running multiple scripts concurrently. Please update to the latest version of lerna and ensure you do not have useNx set to false in your lerna.json."
);
});

it("runs a script in packages", async () => {
await lernaRun(testDir)("my-script");

Expand Down
12 changes: 11 additions & 1 deletion website/docs/features/run-tasks.md
Expand Up @@ -46,7 +46,17 @@ This will build the projects in the right order: `footer` and `header` and then
Note that Lerna doesn't care what each of the build scripts does. The name `build` is also **not** special: it's simply
the name of the npm script.

## Run a Single Task
## Run a Multiple Tasks concurrently

You can pass a comma-delimited list of targets you wish to trigger to run concurrently.

```bash
npx lerna run test,build,lint
```

If, for example, there are dependencies between your tasks, such as `build` needing to run before `test` for particular packages, the task-runner will coordinate that for you as long as you have configured an appropriate [Task Pipeline Configuration](../concepts/task-pipeline-configuration).

## Run a Task for a single Package

While developing you rarely run all the builds or all the tests. Instead, you often run things only against the projects
you are changing. For instance, you can run the `header` tests like this:
Expand Down

0 comments on commit 937b02a

Please sign in to comment.