From 0a6ab6cad2593292dd3f98edefa898938282465b Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 24 Oct 2025 11:04:46 -0700 Subject: [PATCH] feat: add deduping to notices unless in verbose+ mode --- lib/utils/display.js | 11 ++++++ test/lib/utils/display.js | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/lib/utils/display.js b/lib/utils/display.js index 01ad55d4ce30c..bdaac6efceafe 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -177,6 +177,8 @@ class Display { #stdout #stderr + #seenNotices = new Set() + constructor ({ stdout, stderr }) { this.#stdout = setBlocking(stdout) this.#stderr = setBlocking(stderr) @@ -195,6 +197,7 @@ class Display { this.#outputState.buffer.length = 0 process.off('input', this.#inputHandler) this.#progress.off() + this.#seenNotices.clear() } get chalk () { @@ -404,6 +407,14 @@ class Display { // notice logs typically come from `npm-notice` headers in responses. Some of them have 2fa login links so we skip redaction. if (level === 'notice') { writeOpts.redact = false + // Deduplicate notices within a single command execution, unless in verbose mode + if (this.#levelIndex < LEVEL_OPTIONS.verbose.index) { + const noticeKey = JSON.stringify([title, ...args]) + if (this.#seenNotices.has(noticeKey)) { + return + } + this.#seenNotices.add(noticeKey) + } } this.#write(this.#stderr, writeOpts, ...args) } diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index bc4e23485fa3e..c29347fc283de 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -132,6 +132,77 @@ t.test('incorrect levels', async t => { t.strictSame(outputs, [], 'output is ignored') }) +t.test('notice deduplication', async t => { + const { log, logs, display } = await mockDisplay(t, { + load: { loglevel: 'notice' }, + }) + + // Log the same notice multiple times - should be deduplicated + log.notice('', 'This is a duplicate notice') + log.notice('', 'This is a duplicate notice') + log.notice('', 'This is a duplicate notice') + + // Should only appear once in logs + t.equal(logs.notice.length, 1, 'notice appears only once') + t.strictSame(logs.notice, ['This is a duplicate notice']) + + // Different notice should appear + log.notice('', 'This is a different notice') + t.equal(logs.notice.length, 2, 'different notice is shown') + t.strictSame(logs.notice, [ + 'This is a duplicate notice', + 'This is a different notice', + ]) + + // Same notice with different title should appear + log.notice('title', 'This is a duplicate notice') + t.equal(logs.notice.length, 3, 'notice with different title is shown') + t.match(logs.notice[2], /title.*This is a duplicate notice/) + + // Log the first notice again - should still be deduplicated + log.notice('', 'This is a duplicate notice') + t.equal(logs.notice.length, 3, 'original notice still deduplicated') + + // Call off() to simulate end of command and clear deduplication + display.off() + + // Create a new display instance to simulate a new command + const logsResult = mockLogs() + const Display = tmock(t, '{LIB}/utils/display') + const display2 = new Display(logsResult.streams) + await display2.load({ + loglevel: 'silly', + stderrColor: false, + stdoutColor: false, + heading: 'npm', + }) + t.teardown(() => display2.off()) + + // Log the same notice again - should appear since deduplication was cleared + log.notice('', 'This is a duplicate notice') + t.equal(logsResult.logs.logs.notice.length, 1, 'notice appears after display.off() clears deduplication') + t.strictSame(logsResult.logs.logs.notice, ['This is a duplicate notice']) +}) + +t.test('notice deduplication does not apply in verbose mode', async t => { + const { log, logs } = await mockDisplay(t, { + load: { loglevel: 'verbose' }, + }) + + // Log the same notice multiple times in verbose mode + log.notice('', 'Repeated notice') + log.notice('', 'Repeated notice') + log.notice('', 'Repeated notice') + + // Should appear all three times in verbose mode + t.equal(logs.notice.length, 3, 'all notices appear in verbose mode') + t.strictSame(logs.notice, [ + 'Repeated notice', + 'Repeated notice', + 'Repeated notice', + ]) +}) + t.test('Display.clean', async (t) => { const { output, outputs, clearOutput } = await mockDisplay(t)