Skip to content

Commit 653bc3d

Browse files
committed
feat(reporter): emit pnpm:lifecycle and pnpm:ignored-scripts
Match upstream's NDJSON wire format for the build phase so @pnpm/cli.default-reporter parses pacquet's lifecycle output the same way it parses pnpm's. - Add `LogEvent::Lifecycle(LifecycleLog)` with the three upstream message shapes (Script, Stdio, Exit) as a `#[serde(untagged)]` union, mirroring `core/core-loggers/src/lifecycleLogger.ts`. - Add `LogEvent::IgnoredScripts(IgnoredScriptsLog)` carrying `packageNames` (camelCase), mirroring `ignoredScriptsLogger.ts`. - `pacquet-executor` depends on `pacquet-reporter`. `run_postinstall_hooks` / `run_lifecycle_hook` are now generic over `R: Reporter`. The hook switches from `Stdio::inherit()` to `Stdio::piped()` and reads each stream on its own thread, emitting one `Stdio` event per line. A `Script` event fires before spawn, `Exit` fires after wait — same ordering as `runLifecycleHook.ts:102/165`. - `BuildModules::run::<R>` collects sorted (peer-stripped) keys of packages that hit the "not in allowBuilds" branch and returns them as `Vec<String>`. Explicit `false` is silently skipped, matching upstream's switch in `building/during-install/src/index.ts:88-101`. - `InstallFrozenLockfile::run` emits one `pnpm:ignored-scripts` event with the returned list (always, even when empty) — mirrors the unconditional emit at `installing/deps-installer/src/install/index.ts:414`. Tests: - Wire-shape tests for both new variants pin the JSON output. - Recording-fake tests for `run_postinstall_hooks` cover Script→Stdio→Exit ordering and non-zero exit propagation. - Recording-fake tests for `BuildModules::run` cover the ignored-builds return value (sorted, peer-stripped, excludes explicit-deny). - Updated `install_emits_pnpm_event_sequence` to expect the new `IgnoredScripts` event between `Stats` and `ImportingDone`. Addresses items pnpm#6 and pnpm#8 from pnpm#397. Upstream refs at `pnpm/pnpm@80037699fb`: - core/core-loggers/src/lifecycleLogger.ts - core/core-loggers/src/ignoredScriptsLogger.ts - exec/lifecycle/src/runLifecycleHook.ts - building/during-install/src/index.ts - installing/deps-installer/src/install/index.ts
1 parent 5f56842 commit 653bc3d

11 files changed

Lines changed: 747 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/executor/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ license.workspace = true
1111
repository.workspace = true
1212

1313
[dependencies]
14+
pacquet-reporter = { workspace = true }
15+
1416
derive_more = { workspace = true }
1517
miette = { workspace = true }
1618
serde_json = { workspace = true }
1719
tracing = { workspace = true }
20+
21+
[dev-dependencies]
22+
pretty_assertions = { workspace = true }
23+
tempfile = { workspace = true }

crates/executor/src/lifecycle.rs

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
use derive_more::{Display, Error};
22
use miette::Diagnostic;
3+
use pacquet_reporter::{
4+
LifecycleLog, LifecycleMessage, LifecycleStdio, LogEvent, LogLevel, Reporter,
5+
};
36
use std::{
47
collections::HashMap,
58
env,
69
ffi::OsString,
10+
io::{BufRead, BufReader, Read},
711
path::{Path, PathBuf},
812
process::{Command, ExitStatus, Stdio},
13+
thread,
914
};
1015

1116
/// Error from running lifecycle scripts.
@@ -71,10 +76,12 @@ pub struct RunPostinstallHooks<'a> {
7176
/// a single dependency.
7277
///
7378
/// Ports `runPostinstallHooks` from
74-
/// `https://github.com/pnpm/pnpm/blob/7e91e4b35f/exec/lifecycle/src/index.ts`.
79+
/// `https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/index.ts`.
7580
///
7681
/// Returns `true` if any script was present and executed.
77-
pub fn run_postinstall_hooks(opts: RunPostinstallHooks<'_>) -> Result<bool, LifecycleScriptError> {
82+
pub fn run_postinstall_hooks<R: Reporter>(
83+
opts: RunPostinstallHooks<'_>,
84+
) -> Result<bool, LifecycleScriptError> {
7885
let manifest_path = opts.pkg_root.join("package.json");
7986
let manifest_text = match std::fs::read_to_string(&manifest_path) {
8087
Ok(text) => text,
@@ -97,7 +104,7 @@ pub fn run_postinstall_hooks(opts: RunPostinstallHooks<'_>) -> Result<bool, Life
97104
let mut ran_any = false;
98105

99106
if let Some(script) = get_script("preinstall") {
100-
run_lifecycle_hook("preinstall", script, &opts)?;
107+
run_lifecycle_hook::<R>("preinstall", script, &opts)?;
101108
ran_any = true;
102109
}
103110

@@ -111,23 +118,27 @@ pub fn run_postinstall_hooks(opts: RunPostinstallHooks<'_>) -> Result<bool, Life
111118
if let Some(script) = &install_script
112119
&& script != "npx only-allow pnpm"
113120
{
114-
run_lifecycle_hook("install", script, &opts)?;
121+
run_lifecycle_hook::<R>("install", script, &opts)?;
115122
ran_any = true;
116123
}
117124

118125
if let Some(script) = get_script("postinstall") {
119-
run_lifecycle_hook("postinstall", script, &opts)?;
126+
run_lifecycle_hook::<R>("postinstall", script, &opts)?;
120127
ran_any = true;
121128
}
122129

123130
Ok(ran_any)
124131
}
125132

126-
/// Run a single lifecycle hook.
133+
/// Run a single lifecycle hook and emit `pnpm:lifecycle` events.
127134
///
128135
/// Ports the core of `runLifecycleHook` from
129-
/// `https://github.com/pnpm/pnpm/blob/7e91e4b35f/exec/lifecycle/src/runLifecycleHook.ts`.
130-
fn run_lifecycle_hook(
136+
/// `https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/runLifecycleHook.ts`.
137+
///
138+
/// Mirrors the upstream emit ordering: a `Script` event before the spawn,
139+
/// `Stdio` events for each stdout/stderr line, then an `Exit` event with
140+
/// the resolved exit code.
141+
fn run_lifecycle_hook<R: Reporter>(
131142
stage: &str,
132143
script: &str,
133144
opts: &RunPostinstallHooks<'_>,
@@ -140,6 +151,21 @@ fn run_lifecycle_hook(
140151
pkg_root = %opts.pkg_root.display(),
141152
);
142153

154+
let pkg_root_str = opts.pkg_root.to_string_lossy().into_owned();
155+
156+
// Mirrors `lifecycleLogger.debug({ depPath, optional, script, stage, wd })`
157+
// at <https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/runLifecycleHook.ts#L102>.
158+
R::emit(&LogEvent::Lifecycle(LifecycleLog {
159+
level: LogLevel::Debug,
160+
message: LifecycleMessage::Script {
161+
dep_path: opts.dep_path.to_string(),
162+
optional: false,
163+
script: script.to_string(),
164+
stage: stage.to_string(),
165+
wd: pkg_root_str.clone(),
166+
},
167+
}));
168+
143169
let path_env = build_path_env(opts.pkg_root, opts.extra_bin_paths);
144170

145171
let mut cmd = Command::new("sh");
@@ -149,26 +175,56 @@ fn run_lifecycle_hook(
149175
.env("PATH", &path_env)
150176
.env("INIT_CWD", opts.init_cwd)
151177
.env("PNPM_SCRIPT_SRC_DIR", opts.pkg_root)
152-
.stdout(Stdio::inherit())
153-
.stderr(Stdio::inherit());
178+
.stdout(Stdio::piped())
179+
.stderr(Stdio::piped());
154180

155181
for (key, value) in opts.extra_env {
156182
cmd.env(key, value);
157183
}
158184

159-
let status = cmd
160-
.spawn()
161-
.map_err(|e| LifecycleScriptError::Spawn {
162-
dep_path: opts.dep_path.to_string(),
163-
stage: stage.to_string(),
164-
source: e,
165-
})?
166-
.wait()
167-
.map_err(|e| LifecycleScriptError::Wait {
185+
let mut child = cmd.spawn().map_err(|e| LifecycleScriptError::Spawn {
186+
dep_path: opts.dep_path.to_string(),
187+
stage: stage.to_string(),
188+
source: e,
189+
})?;
190+
191+
let stdout = child.stdout.take();
192+
let stderr = child.stderr.take();
193+
194+
let stdout_handle = stdout.map(|s| {
195+
spawn_line_pump::<R>(s, LifecycleStdio::Stdout, opts.dep_path, stage, &pkg_root_str)
196+
});
197+
let stderr_handle = stderr.map(|s| {
198+
spawn_line_pump::<R>(s, LifecycleStdio::Stderr, opts.dep_path, stage, &pkg_root_str)
199+
});
200+
201+
let status = child.wait().map_err(|e| LifecycleScriptError::Wait {
202+
dep_path: opts.dep_path.to_string(),
203+
stage: stage.to_string(),
204+
source: e,
205+
})?;
206+
207+
// Joining the pumps after `wait` ensures every line they read is
208+
// emitted before the `Exit` event below, matching pnpm's ordering.
209+
if let Some(h) = stdout_handle {
210+
let _ = h.join();
211+
}
212+
if let Some(h) = stderr_handle {
213+
let _ = h.join();
214+
}
215+
216+
// Mirrors `lifecycleLogger.debug({ depPath, exitCode, optional, stage, wd })`
217+
// at <https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/runLifecycleHook.ts#L165>.
218+
R::emit(&LogEvent::Lifecycle(LifecycleLog {
219+
level: LogLevel::Debug,
220+
message: LifecycleMessage::Exit {
168221
dep_path: opts.dep_path.to_string(),
222+
exit_code: status.code().unwrap_or(-1),
223+
optional: false,
169224
stage: stage.to_string(),
170-
source: e,
171-
})?;
225+
wd: pkg_root_str,
226+
},
227+
}));
172228

173229
if !status.success() {
174230
return Err(LifecycleScriptError::ScriptFailed {
@@ -182,6 +238,44 @@ fn run_lifecycle_hook(
182238
Ok(())
183239
}
184240

241+
/// Spawn a thread that reads `reader` line-by-line and emits a
242+
/// `LifecycleMessage::Stdio` event per line. Mirrors the per-chunk
243+
/// logging callback at
244+
/// <https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/runLifecycleHook.ts#L147>.
245+
fn spawn_line_pump<R: Reporter>(
246+
reader: impl Read + Send + 'static,
247+
stdio: LifecycleStdio,
248+
dep_path: &str,
249+
stage: &str,
250+
wd: &str,
251+
) -> thread::JoinHandle<()> {
252+
let dep_path = dep_path.to_string();
253+
let stage = stage.to_string();
254+
let wd = wd.to_string();
255+
thread::spawn(move || {
256+
let buf = BufReader::new(reader);
257+
for line in buf.lines() {
258+
let Ok(line) = line else {
259+
// Stop pumping on read error — an EBADF or EPIPE means
260+
// the child closed the stream. Errors are not fatal to
261+
// the install; the wait below will surface a non-zero
262+
// exit code if the child failed because of them.
263+
break;
264+
};
265+
R::emit(&LogEvent::Lifecycle(LifecycleLog {
266+
level: LogLevel::Debug,
267+
message: LifecycleMessage::Stdio {
268+
dep_path: dep_path.clone(),
269+
line,
270+
stage: stage.clone(),
271+
stdio,
272+
wd: wd.clone(),
273+
},
274+
}));
275+
}
276+
})
277+
}
278+
185279
/// Build the `PATH` environment variable for lifecycle scripts.
186280
///
187281
/// Prepends the package's own `node_modules/.bin`, any extra bin paths
@@ -199,3 +293,6 @@ fn build_path_env(pkg_root: &Path, extra_bin_paths: &[PathBuf]) -> OsString {
199293

200294
env::join_paths(paths).unwrap_or(system_path)
201295
}
296+
297+
#[cfg(test)]
298+
mod tests;

0 commit comments

Comments
 (0)