mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 04:02:32 -04:00
feat(reporter): emit pnpm:summary at install end (#355)
* feat(reporter): emit pnpm:summary at install end Adds the `Summary` variant to `LogEvent` and emits it from `Install::run` immediately after `pnpm:stage importing_done`. Mirrors pnpm's emit at https://github.com/pnpm/pnpm/blob/086c5e91e8/installing/deps-installer/src/install/index.ts#L1663. The payload carries only `prefix`; the reporter combines this with the accumulated `pnpm:root` events (still TODO under #347) to render the final "+N -M" diff block. Test: extends the install integration test (renamed to `install_emits_pnpm_event_sequence` to match the growing scope) to assert the four-event success sequence (`Context`, `ImportingStarted`, `ImportingDone`, `Summary`) and spot-checks summary's `prefix`. Verified the test catches the regression by temporarily commenting out the emit and confirming it failed with a Summary-shaped gap. Refs #347. Engine: #345. Previous channel: #354. * docs(reporter): clarify SummaryLog payload comment Per review feedback on #355: the original wording said the `pnpm:summary` payload "is just `prefix`", which omitted the `level` field that's also part of the serialized record. `level` is the bunyan-envelope severity carried by every channel, not channel-specific to summary; say so explicitly so a reader doesn't try to "fix" the struct based on the comment. Same fix applied to the wire-shape test's doc comment.
This commit is contained in:
@@ -10,7 +10,7 @@ use pacquet_lockfile::Lockfile;
|
||||
use pacquet_network::ThrottledClient;
|
||||
use pacquet_npmrc::Npmrc;
|
||||
use pacquet_package_manifest::{DependencyGroup, PackageManifest};
|
||||
use pacquet_reporter::{ContextLog, LogEvent, LogLevel, Reporter, Stage, StageLog};
|
||||
use pacquet_reporter::{ContextLog, LogEvent, LogLevel, Reporter, Stage, StageLog, SummaryLog};
|
||||
use pacquet_tarball::MemCache;
|
||||
|
||||
/// This subroutine does everything `pacquet install` is supposed to do.
|
||||
@@ -160,10 +160,16 @@ where
|
||||
|
||||
R::emit(&LogEvent::Stage(StageLog {
|
||||
level: LogLevel::Debug,
|
||||
prefix,
|
||||
prefix: prefix.clone(),
|
||||
stage: Stage::ImportingDone,
|
||||
}));
|
||||
|
||||
// `pnpm:summary` closes the install and lets the reporter render
|
||||
// the accumulated `pnpm:root` events as a "+N -M" block. Must
|
||||
// come after `importing_done`, matching pnpm's ordering at
|
||||
// <https://github.com/pnpm/pnpm/blob/086c5e91e8/installing/deps-installer/src/install/index.ts#L1663>.
|
||||
R::emit(&LogEvent::Summary(SummaryLog { level: LogLevel::Debug, prefix }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -175,7 +181,9 @@ mod tests {
|
||||
use pacquet_npmrc::Npmrc;
|
||||
use pacquet_package_manifest::{DependencyGroup, PackageManifest};
|
||||
use pacquet_registry_mock::AutoMockInstance;
|
||||
use pacquet_reporter::{ContextLog, LogEvent, Reporter, SilentReporter, Stage, StageLog};
|
||||
use pacquet_reporter::{
|
||||
ContextLog, LogEvent, Reporter, SilentReporter, Stage, StageLog, SummaryLog,
|
||||
};
|
||||
use pacquet_testing_utils::fs::{get_all_folders, is_symlink_or_junction};
|
||||
use std::sync::Mutex;
|
||||
use tempfile::tempdir;
|
||||
@@ -566,14 +574,14 @@ mod tests {
|
||||
drop(dir);
|
||||
}
|
||||
|
||||
/// [`Install::run`] emits `pnpm:context` followed by the
|
||||
/// `pnpm:stage` `importing_started` / `importing_done` pair that
|
||||
/// brackets the install. On the success path all three fire in
|
||||
/// order; on an early-error path such as
|
||||
/// [`Install::run`] emits `pnpm:context`, then `pnpm:stage`
|
||||
/// `importing_started`, then on the success path `importing_done`
|
||||
/// followed by `pnpm:summary`. On an early-error path such as
|
||||
/// [`InstallError::NoLockfile`] only the leading events fire. This
|
||||
/// matches pnpm: context is emitted once alongside the install
|
||||
/// header, and the stage pairing drives the JS reporter's progress
|
||||
/// UI.
|
||||
/// header, the stage pairing drives the JS reporter's progress
|
||||
/// UI, and summary closes the run so the reporter can render its
|
||||
/// "+N -M" block.
|
||||
///
|
||||
/// `pnpm:context` carries `currentLockfileExists`, `storeDir`,
|
||||
/// `virtualStoreDir`. `currentLockfileExists` is hard-coded
|
||||
@@ -581,7 +589,7 @@ mod tests {
|
||||
/// `node_modules/.pnpm/lock.yaml`), matching the TODO in
|
||||
/// [`Install::run`].
|
||||
#[tokio::test]
|
||||
async fn install_emits_context_and_stage_events() {
|
||||
async fn install_emits_pnpm_event_sequence() {
|
||||
static EVENTS: Mutex<Vec<LogEvent>> = Mutex::new(Vec::new());
|
||||
|
||||
struct RecordingReporter;
|
||||
@@ -635,8 +643,8 @@ mod tests {
|
||||
|
||||
let captured = EVENTS.lock().unwrap();
|
||||
|
||||
// Event ordering matches pnpm: context first, then the stage
|
||||
// bracketing pair.
|
||||
// Event ordering matches pnpm: context, then the stage
|
||||
// bracketing pair, then summary closing the run.
|
||||
assert!(
|
||||
matches!(
|
||||
captured.as_slice(),
|
||||
@@ -644,6 +652,7 @@ mod tests {
|
||||
LogEvent::Context(_),
|
||||
LogEvent::Stage(StageLog { stage: Stage::ImportingStarted, .. }),
|
||||
LogEvent::Stage(StageLog { stage: Stage::ImportingDone, .. }),
|
||||
LogEvent::Summary(_),
|
||||
]
|
||||
),
|
||||
"unexpected event sequence: {captured:?}",
|
||||
@@ -665,6 +674,17 @@ mod tests {
|
||||
assert_eq!(emitted_store_dir, &store_dir.display().to_string());
|
||||
assert_eq!(emitted_virtual_store_dir, &virtual_store_dir.to_string_lossy().into_owned());
|
||||
|
||||
// Summary's `prefix` must equal the manifest-parent value
|
||||
// `Install::run` derives, since pnpm's reporter keys its
|
||||
// accumulated root-events by prefix to render the diff.
|
||||
let LogEvent::Summary(SummaryLog { prefix: summary_prefix, .. }) = &captured[3] else {
|
||||
unreachable!("fourth event is summary, asserted above");
|
||||
};
|
||||
assert_eq!(
|
||||
summary_prefix,
|
||||
&manifest.path().parent().unwrap().to_string_lossy().into_owned(),
|
||||
);
|
||||
|
||||
drop(dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,15 @@ pub enum LogEvent {
|
||||
/// Upstream: <https://github.com/pnpm/pnpm/blob/3b12eb27de/core/core-loggers/src/stageLogger.ts>.
|
||||
#[serde(rename = "pnpm:stage")]
|
||||
Stage(StageLog),
|
||||
|
||||
/// End-of-install marker (`pnpm:summary`). pnpm's reporter combines
|
||||
/// this with the accumulated `pnpm:root` events to render the final
|
||||
/// "+N -M" block.
|
||||
///
|
||||
/// Upstream: <https://github.com/pnpm/pnpm/blob/086c5e91e8/core/core-loggers/src/summaryLogger.ts>.
|
||||
/// Emit site: <https://github.com/pnpm/pnpm/blob/086c5e91e8/installing/deps-installer/src/install/index.ts#L1663>.
|
||||
#[serde(rename = "pnpm:summary")]
|
||||
Summary(SummaryLog),
|
||||
}
|
||||
|
||||
/// `pnpm:context` payload.
|
||||
@@ -77,6 +86,18 @@ pub enum Stage {
|
||||
ImportingDone,
|
||||
}
|
||||
|
||||
/// `pnpm:summary` payload. `prefix` identifies the importer; pnpm's
|
||||
/// reporter uses it to look up the matching `pnpm:root` history and
|
||||
/// render its "+N -M" diff. `level` is the [bunyan]-envelope severity,
|
||||
/// common to every channel.
|
||||
///
|
||||
/// [bunyan]: https://github.com/trentm/node-bunyan
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SummaryLog {
|
||||
pub level: LogLevel,
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
/// Severity level on the [bunyan]-shaped envelope.
|
||||
///
|
||||
/// pnpm's logger uses the [bole] library, which writes one of these strings
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ContextLog, Envelope, GetHostName, LogEvent, LogLevel, RealApi, Reporter, SilentReporter,
|
||||
Stage, StageLog,
|
||||
Stage, StageLog, SummaryLog,
|
||||
};
|
||||
|
||||
/// Context log serializes with the camelCase field names
|
||||
@@ -64,6 +64,31 @@ fn stage_event_matches_pnpm_wire_shape() {
|
||||
assert_eq!(json["pid"], 4242);
|
||||
}
|
||||
|
||||
/// Summary log serializes with the channel name flattened into the
|
||||
/// envelope alongside `prefix` and the [bunyan]-envelope `level`.
|
||||
/// `prefix` is what pnpm's reporter uses to find the matching
|
||||
/// `pnpm:root` history and render its "+N -M" block.
|
||||
///
|
||||
/// [bunyan]: https://github.com/trentm/node-bunyan
|
||||
#[test]
|
||||
fn summary_event_matches_pnpm_wire_shape() {
|
||||
let event = LogEvent::Summary(SummaryLog {
|
||||
level: LogLevel::Debug,
|
||||
prefix: "/some/project".to_string(),
|
||||
});
|
||||
let envelope = Envelope { time: 1_700_000_000_000, hostname: "host", pid: 4242, event: &event };
|
||||
|
||||
let json: Value = envelope
|
||||
.pipe_ref(serde_json::to_string)
|
||||
.expect("serialize envelope")
|
||||
.pipe_as_ref(serde_json::from_str)
|
||||
.expect("parse JSON");
|
||||
|
||||
assert_eq!(json["name"], "pnpm:summary");
|
||||
assert_eq!(json["level"], "debug");
|
||||
assert_eq!(json["prefix"], "/some/project");
|
||||
}
|
||||
|
||||
/// Phase markers serialize as the snake_case strings pnpm uses.
|
||||
#[test]
|
||||
fn stage_phases_serialize_in_pnpm_form() {
|
||||
|
||||
Reference in New Issue
Block a user