Skip to content

Commit d3a8128

Browse files
committed
fix(child-process): Prevent duplicate logs when any package-oriented execution fails
Previously, avoiding duplicate logging required special handling per use-case and only covered non-streaming variants.
1 parent ee0c7be commit d3a8128

File tree

11 files changed

+106
-63
lines changed

11 files changed

+106
-63
lines changed

commands/exec/__tests__/exec-command.test.js

+9
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,14 @@ describe("ExecCommand", () => {
8989
expect(ChildProcessUtilities.spawn).toHaveBeenCalledTimes(1);
9090
expect(ChildProcessUtilities.spawn).lastCalledWith("ls", [], {
9191
cwd: path.join(testDir, "packages/package-2"),
92+
pkg: expect.objectContaining({
93+
name: "package-2",
94+
}),
9295
env: expect.objectContaining({
9396
LERNA_PACKAGE_NAME: "package-2",
97+
LERNA_ROOT_PATH: testDir,
9498
}),
99+
extendEnv: false,
95100
reject: true,
96101
shell: true,
97102
});
@@ -120,9 +125,13 @@ describe("ExecCommand", () => {
120125
expect(ChildProcessUtilities.spawn).toHaveBeenCalledTimes(1);
121126
expect(ChildProcessUtilities.spawn).lastCalledWith("ls", [], {
122127
cwd: path.join(testDir, "packages/package-1"),
128+
pkg: expect.objectContaining({
129+
name: "package-1",
130+
}),
123131
env: expect.objectContaining({
124132
LERNA_PACKAGE_NAME: "package-1",
125133
}),
134+
extendEnv: false,
126135
reject: true,
127136
shell: true,
128137
});

commands/exec/index.js

+24-37
Original file line numberDiff line numberDiff line change
@@ -27,50 +27,46 @@ class ExecCommand extends Command {
2727

2828
initialize() {
2929
const dashedArgs = this.options["--"] || [];
30-
const { cmd, args } = this.options;
3130

32-
this.command = cmd || dashedArgs.shift();
33-
this.args = (args || []).concat(dashedArgs);
31+
this.command = this.options.cmd || dashedArgs.shift();
32+
this.args = (this.options.args || []).concat(dashedArgs);
3433

3534
if (!this.command) {
3635
throw new ValidationError("ENOCOMMAND", "A command to execute is required");
3736
}
3837

39-
const { filteredPackages } = this;
38+
// accessing properties of process.env can be expensive,
39+
// so cache it here to reduce churn during tighter loops
40+
this.env = Object.assign({}, process.env);
4041

4142
this.batchedPackages = this.toposort
42-
? batchPackages(filteredPackages, this.options.rejectCycles)
43-
: [filteredPackages];
43+
? batchPackages(this.filteredPackages, this.options.rejectCycles)
44+
: [this.filteredPackages];
4445
}
4546

4647
execute() {
4748
if (this.options.parallel) {
4849
return this.runCommandInPackagesParallel();
4950
}
5051

51-
return runParallelBatches(this.batchedPackages, this.concurrency, pkg =>
52-
this.runCommandInPackage(pkg).catch(err => {
53-
this.logger.error("exec", `'${err.cmd}' errored in '${pkg.name}'`);
52+
const runner = this.options.stream
53+
? pkg => this.runCommandInPackageStreaming(pkg)
54+
: pkg => this.runCommandInPackageCapturing(pkg);
5455

55-
if (err.code) {
56-
// log non-lerna error cleanly
57-
err.pkg = pkg;
58-
}
59-
60-
throw err;
61-
})
62-
);
56+
return runParallelBatches(this.batchedPackages, this.concurrency, runner);
6357
}
6458

6559
getOpts(pkg) {
6660
return {
6761
cwd: pkg.location,
6862
shell: true,
69-
env: Object.assign({}, process.env, {
63+
extendEnv: false,
64+
env: Object.assign({}, this.env, {
7065
LERNA_PACKAGE_NAME: pkg.name,
7166
LERNA_ROOT_PATH: this.project.rootPath,
7267
}),
7368
reject: this.options.bail,
69+
pkg,
7470
};
7571
}
7672

@@ -82,28 +78,19 @@ class ExecCommand extends Command {
8278
[this.command].concat(this.args).join(" ")
8379
);
8480

85-
return Promise.all(
86-
this.filteredPackages.map(pkg =>
87-
ChildProcessUtilities.spawnStreaming(
88-
this.command,
89-
this.args,
90-
this.getOpts(pkg),
91-
this.options.prefix && pkg.name
92-
)
93-
)
94-
);
81+
return Promise.all(this.filteredPackages.map(pkg => this.runCommandInPackageStreaming(pkg)));
9582
}
9683

97-
runCommandInPackage(pkg) {
98-
if (this.options.stream) {
99-
return ChildProcessUtilities.spawnStreaming(
100-
this.command,
101-
this.args,
102-
this.getOpts(pkg),
103-
this.options.prefix && pkg.name
104-
);
105-
}
84+
runCommandInPackageStreaming(pkg) {
85+
return ChildProcessUtilities.spawnStreaming(
86+
this.command,
87+
this.args,
88+
this.getOpts(pkg),
89+
this.options.prefix && pkg.name
90+
);
91+
}
10692

93+
runCommandInPackageCapturing(pkg) {
10794
return ChildProcessUtilities.spawn(this.command, this.args, this.getOpts(pkg));
10895
}
10996
}

commands/run/index.js

+4-15
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class RunCommand extends Command {
8888
? pkg => this.runScriptInPackageStreaming(pkg)
8989
: pkg => this.runScriptInPackageCapturing(pkg);
9090

91-
return runParallelBatches(this.batchedPackages, this.concurrency, pkg => runner(pkg));
91+
return runParallelBatches(this.batchedPackages, this.concurrency, runner);
9292
}
9393

9494
runScriptInPackagesParallel() {
@@ -107,20 +107,9 @@ class RunCommand extends Command {
107107
}
108108

109109
runScriptInPackageCapturing(pkg) {
110-
return npmRunScript(this.script, this.getOpts(pkg))
111-
.then(result => {
112-
output(result.stdout);
113-
})
114-
.catch(err => {
115-
this.logger.error("run", `'${this.script}' errored in '${pkg.name}'`);
116-
117-
if (err.code) {
118-
// log non-lerna error cleanly
119-
err.pkg = pkg;
120-
}
121-
122-
throw err;
123-
});
110+
return npmRunScript(this.script, this.getOpts(pkg)).then(result => {
111+
output(result.stdout);
112+
});
124113
}
125114
}
126115

core/child-process/__tests__/child-process.test.js

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ describe("ChildProcessUtilities", () => {
4242
expect(one.stdout).toBe("one");
4343
expect(two.stdout).toBe("two");
4444
});
45+
46+
it("decorates opts.pkg on error if caught", async () => {
47+
try {
48+
await ChildProcessUtilities.exec(
49+
"theVeneratedVirginianVeteranWhoseMenAreAll",
50+
["liningUpToPutMeUpOnAPedestal"],
51+
{ pkg: { name: "hamilton" } }
52+
);
53+
} catch (err) {
54+
expect(err.code).toBe("ENOENT");
55+
expect(err.pkg).toEqual({ name: "hamilton" });
56+
}
57+
});
4558
});
4659

4760
describe(".spawn()", () => {

core/child-process/index.js

+28-8
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ const NUM_COLORS = colorWheel.length;
1313

1414
function exec(command, args, opts) {
1515
const options = Object.assign({ stdio: "pipe" }, opts);
16+
const spawned = spawnProcess(command, args, options);
1617

17-
return _spawn(command, args, options);
18+
return wrapError(spawned);
1819
}
1920

2021
function execSync(command, args, opts) {
2122
return execa.sync(command, args, opts).stdout;
2223
}
2324

2425
function spawn(command, args, opts) {
25-
const options = Object.assign({}, opts);
26-
options.stdio = "inherit";
26+
const options = Object.assign({}, opts, { stdio: "inherit" });
27+
const spawned = spawnProcess(command, args, options);
2728

28-
return _spawn(command, args, options);
29+
return wrapError(spawned);
2930
}
3031

3132
// istanbul ignore next
@@ -35,7 +36,7 @@ function spawnStreaming(command, args, opts, prefix) {
3536

3637
const colorName = colorWheel[children % NUM_COLORS];
3738
const color = chalk[colorName];
38-
const spawned = _spawn(command, args, options);
39+
const spawned = spawnProcess(command, args, options);
3940

4041
const stdoutOpts = {};
4142
const stderrOpts = {}; // mergeMultiline causes escaped newlines :P
@@ -54,15 +55,14 @@ function spawnStreaming(command, args, opts, prefix) {
5455
spawned.stdout.pipe(logTransformer(stdoutOpts)).pipe(process.stdout);
5556
spawned.stderr.pipe(logTransformer(stderrOpts)).pipe(process.stderr);
5657

57-
return spawned;
58+
return wrapError(spawned);
5859
}
5960

6061
function getChildProcessCount() {
6162
return children;
6263
}
6364

64-
// eslint-disable-next-line no-underscore-dangle
65-
function _spawn(command, args, opts) {
65+
function spawnProcess(command, args, opts) {
6666
children += 1;
6767

6868
const child = execa(command, args, opts);
@@ -78,9 +78,29 @@ function _spawn(command, args, opts) {
7878
child.once("exit", drain);
7979
child.once("error", drain);
8080

81+
if (opts.pkg) {
82+
child.pkg = opts.pkg;
83+
}
84+
8185
return child;
8286
}
8387

88+
function wrapError(spawned) {
89+
if (spawned.pkg) {
90+
return spawned.catch(err => {
91+
// istanbul ignore else
92+
if (err.code) {
93+
// log non-lerna error cleanly
94+
err.pkg = spawned.pkg;
95+
}
96+
97+
throw err;
98+
});
99+
}
100+
101+
return spawned;
102+
}
103+
84104
exports.exec = exec;
85105
exports.execSync = execSync;
86106
exports.spawn = spawn;

integration/lerna-run.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("lerna run", () => {
1515
try {
1616
await cliRunner(cwd)(...args);
1717
} catch (err) {
18-
expect(err.message).toMatch("run 'fail' errored in 'package-3'");
18+
expect(err.message).toMatch("npm run fail --silent exited 1 in 'package-3'");
1919
}
2020
});
2121

utils/get-npm-exec-opts/get-npm-exec-opts.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = getExecOpts;
77
function getExecOpts(pkg, registry) {
88
const opts = {
99
cwd: pkg.location,
10+
pkg,
1011
};
1112

1213
if (registry) {

utils/npm-dist-tag/__tests__/npm-dist-tag.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe("dist-tag", () => {
2727

2828
expect(ChildProcessUtilities.exec).lastCalledWith("npm", ["dist-tag", "add", "[email protected]", tag], {
2929
cwd: pkg.location,
30+
pkg,
3031
});
3132
});
3233

@@ -39,6 +40,7 @@ describe("dist-tag", () => {
3940
npm_config_registry: registry,
4041
}),
4142
extendEnv: false,
43+
pkg,
4244
});
4345
});
4446
});
@@ -56,6 +58,7 @@ describe("dist-tag", () => {
5658

5759
expect(ChildProcessUtilities.exec).lastCalledWith("npm", ["dist-tag", "rm", pkg.name, tag], {
5860
cwd: pkg.location,
61+
pkg,
5962
});
6063
});
6164

@@ -68,6 +71,7 @@ describe("dist-tag", () => {
6871
npm_config_registry: registry,
6972
}),
7073
extendEnv: false,
74+
pkg,
7175
});
7276
});
7377
});
@@ -88,6 +92,7 @@ describe("dist-tag", () => {
8892

8993
expect(ChildProcessUtilities.execSync).lastCalledWith("npm", ["dist-tag", "ls", pkg.name], {
9094
cwd: pkg.location,
95+
pkg,
9196
});
9297
});
9398

@@ -102,6 +107,7 @@ describe("dist-tag", () => {
102107
npm_config_registry: registry,
103108
}),
104109
extendEnv: false,
110+
pkg,
105111
});
106112
});
107113
});

0 commit comments

Comments
 (0)