Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/utils/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ class Display {
#stdout
#stderr

#seenNotices = new Set()

constructor ({ stdout, stderr }) {
this.#stdout = setBlocking(stdout)
this.#stderr = setBlocking(stderr)
Expand All @@ -195,6 +197,7 @@ class Display {
this.#outputState.buffer.length = 0
process.off('input', this.#inputHandler)
this.#progress.off()
this.#seenNotices.clear()
}

get chalk () {
Expand Down Expand Up @@ -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)
}
Expand Down
71 changes: 71 additions & 0 deletions test/lib/utils/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading