diff --git a/capture/lib/frames.spec.ts b/capture/lib/frames.spec.ts index 5a662e7cf..b90bcb565 100644 --- a/capture/lib/frames.spec.ts +++ b/capture/lib/frames.spec.ts @@ -2,6 +2,7 @@ import type { Chunk } from './pty.ts' import { describe, expect, it } from 'vitest' import { buildFingerprint } from './frames.ts' +import { resolveRules, scrubLine } from './scrub.ts' /** * How each progress display we record repaints. They disagree, and reading only @@ -57,3 +58,63 @@ describe('capture fingerprint', () => { }) } }) + +describe('scrubbing a right-aligned tag', () => { + const TAG = 'nitro' + const WIDTH = 96 + + /** One consola line as it would be rendered for a given real duration. */ + function rendered(duration: string): string { + const message = `✔ Nuxt Nitro server built in ${duration}` + return message + ' '.repeat(WIDTH - message.length - TAG.length) + TAG + } + + function scrub(duration: string): string { + const line = rendered(duration) + const styles = Array.from({ length: line.length }).fill(undefined) as never + return scrubLine(line, styles, resolveRules(['timings'])).line + } + + it('should put the tag in the same column however long the duration was', () => { + const durations = ['1085 ms', '986 ms', '9 ms', '42 ms', '1.2 s'] + const scrubbed = durations.map(scrub) + + for (const line of scrubbed) { + expect(line).toBe(scrubbed[0]) + expect(line).toHaveLength(WIDTH) + } + }) + + it('should leave indentation and gaps inside a message alone', () => { + const untagged = [ + ' config 1085 ms · modules 42 ms', + '● Nuxt 1085 ms and more', + ' ➜ DevTools: 1085 ms', + ' Ready in 1085 ms → http://localhost:3000/', + ] + + for (const content of untagged) { + const line = content.padEnd(WIDTH) + const styles = Array.from({ length: line.length }).fill(undefined) as never + + expect(scrubLine(line, styles, resolveRules(['timings'])).line.trimEnd()) + .toBe(content.replaceAll('1085 ms', '42 ms')) + } + }) + + it('should keep the styles aligned with the re-padded line', () => { + const line = rendered('1085 ms') + const styles = Array.from({ length: line.length }, (_, index) => index) as never + const result = scrubLine(line, styles, resolveRules(['timings'])) + + expect(result.styles).toHaveLength(result.line.length) + }) + + it('should leave a line without padding alone', () => { + expect(scrub('42 ms').trimEnd()).not.toBe('') + const plain = '✔ Vite client built in 1085 ms' + const styles = Array.from({ length: plain.length }).fill(undefined) as never + + expect(scrubLine(plain, styles, resolveRules(['timings'])).line).toBe('✔ Vite client built in 42 ms') + }) +}) diff --git a/capture/lib/scrub.ts b/capture/lib/scrub.ts index c871dae09..6c4038b85 100644 --- a/capture/lib/scrub.ts +++ b/capture/lib/scrub.ts @@ -121,7 +121,41 @@ export function scrubLine(line: string, styles: Style[], rules: ScrubRule[]): { currentStyles = next.styles } } - return { line: currentLine, styles: currentStyles as Style[] } + const realigned = realign(currentLine, currentStyles, line.length) + return { line: realigned.line, styles: realigned.styles as Style[] } +} + +/** + * Spaces holding a right-aligned tag against the end of the line. Anchoring to + * the end is what tells tag padding apart from indentation and from ordinary + * gaps inside a message, neither of which may be resized. + */ +const TAG_PADDING_RE = / {2,}(?=\S+$)/ + +/** + * Restore a line to the width it was rendered at, by resizing the padding that + * holds a trailing tag against the right edge. Consola sizes that padding for + * the unscrubbed message, so without this the tag moves whenever a substitution + * changes the length of what precedes it. + */ +function realign(line: string, styles: (Style | undefined)[], width: number): { line: string, styles: (Style | undefined)[] } { + const delta = width - line.length + if (delta === 0) { + return { line, styles } + } + const padding = TAG_PADDING_RE.exec(line) + if (!padding || padding[0].length + delta < 2) { + return { line, styles } + } + const at = padding.index + return { + line: line.slice(0, at) + ' '.repeat(padding[0].length + delta) + line.slice(at + padding[0].length), + styles: [ + ...styles.slice(0, at), + ...Array.from