From 5ae697660dbb3595d2a5b35ed34b6c772991c60d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 13:09:52 +0900 Subject: [PATCH 01/14] fix(pretty-format): prevent string length limit on large object --- packages/pretty-format/src/index.ts | 73 +++++++++++++++++++---------- packages/pretty-format/src/types.ts | 2 + 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index d5f0d886b3c5..c760816178fd 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -212,9 +212,11 @@ function printComplexValue( return printer(val.toJSON(), config, indentation, depth, refs, true) } + let result: string + const toStringed = toString.call(val) if (toStringed === '[object Arguments]') { - return hitMaxDepth + result = hitMaxDepth ? '[Arguments]' : `${min ? '' : 'Arguments '}[${printListItems( val, @@ -225,8 +227,8 @@ function printComplexValue( printer, )}]` } - if (isToStringedArrayType(toStringed)) { - return hitMaxDepth + else if (isToStringedArrayType(toStringed)) { + result = hitMaxDepth ? `[${val.constructor.name}]` : `${ min @@ -236,8 +238,8 @@ function printComplexValue( : `${val.constructor.name} ` }[${printListItems(val, config, indentation, depth, refs, printer)}]` } - if (toStringed === '[object Map]') { - return hitMaxDepth + else if (toStringed === '[object Map]') { + result = hitMaxDepth ? '[Map]' : `Map {${printIteratorEntries( val.entries(), @@ -249,8 +251,8 @@ function printComplexValue( ' => ', )}}` } - if (toStringed === '[object Set]') { - return hitMaxDepth + else if (toStringed === '[object Set]') { + result = hitMaxDepth ? '[Set]' : `Set {${printIteratorValues( val.values(), @@ -261,25 +263,36 @@ function printComplexValue( printer, )}}` } - // Avoid failure to serialize global window object in jsdom test environment. // For example, not even relevant if window is prop of React element. - return hitMaxDepth || isWindow(val) - ? `[${getConstructorName(val)}]` - : `${ - min - ? '' - : !config.printBasicPrototype && getConstructorName(val) === 'Object' - ? '' - : `${getConstructorName(val)} ` - }{${printObjectProperties( - val, - config, - indentation, - depth, - refs, - printer, - )}}` + else { + result = hitMaxDepth || isWindow(val) + ? `[${getConstructorName(val)}]` + : `${ + min + ? '' + : !config.printBasicPrototype && getConstructorName(val) === 'Object' + ? '' + : `${getConstructorName(val)} ` + }{${printObjectProperties( + val, + config, + indentation, + depth, + refs, + printer, + )}}` + } + + // Post-hoc budget check + // Accumulate output length and if exceeded, force no recursion by patching maxDepth. + // Inspired by node's util.inspect bail out heuristics. + config.budget.used += result.length + if (config.budget.used > config.budget.max) { + config.maxDepth = -1 + } + + return result } const ErrorPlugin: NewPlugin = { @@ -298,7 +311,7 @@ const ErrorPlugin: NewPlugin = { ...rest, } const name = val.name !== 'Error' ? val.name : getConstructorName(val as any) - return hitMaxDepth + const result = hitMaxDepth ? `[${name}]` : `${name} {${printIteratorEntries( Object.entries(entries).values(), @@ -308,6 +321,11 @@ const ErrorPlugin: NewPlugin = { refs, printer, )}}` + config.budget.used += result.length + if (config.budget.used > config.budget.max) { + config.maxDepth = -1 + } + return result }, } @@ -425,6 +443,10 @@ export const DEFAULT_OPTIONS: Options = { highlight: false, indent: 2, maxDepth: Number.POSITIVE_INFINITY, + // Prevent hitting Node's string length limit (~512MB) on pathological object + // graphs with shared references that fan out exponentially. 100K is generous + // enough for normal formatting but well below the ~2**27 danger zone. + maxOutputLength: 100_000, maxWidth: Number.POSITIVE_INFINITY, min: false, plugins: [], @@ -509,6 +531,7 @@ function getConfig(options?: OptionsReceived): Config { printShadowRoot: options?.printShadowRoot ?? true, spacingInner: options?.min ? ' ' : '\n', spacingOuter: options?.min ? '' : '\n', + budget: { used: 0, max: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength }, } } diff --git a/packages/pretty-format/src/types.ts b/packages/pretty-format/src/types.ts index eac12b7946e6..49c2b2ceea66 100644 --- a/packages/pretty-format/src/types.ts +++ b/packages/pretty-format/src/types.ts @@ -45,6 +45,7 @@ export interface PrettyFormatOptions { indent?: number maxDepth?: number maxWidth?: number + maxOutputLength?: number min?: boolean printBasicPrototype?: boolean printFunctionName?: boolean @@ -71,6 +72,7 @@ export interface Config { printShadowRoot: boolean spacingInner: string spacingOuter: string + budget: { used: number; max: number } } export type Printer = ( From c45d93c289c32d11f169a7ed3596dad6e5185c72 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 14:26:42 +0900 Subject: [PATCH 02/14] fix(pretty-format): use maxDepth = 0 and add budget kill-switch tests Change kill-switch from maxDepth = -1 to maxDepth = 0 (equivalent behavior, clearer intent). Add test suite visualizing the budget behavior: early elements expanded, later elements folded. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/pretty-format/src/index.ts | 4 +- test/core/test/pretty-format.test.ts | 95 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 test/core/test/pretty-format.test.ts diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index c760816178fd..4bacea380fd0 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -289,7 +289,7 @@ function printComplexValue( // Inspired by node's util.inspect bail out heuristics. config.budget.used += result.length if (config.budget.used > config.budget.max) { - config.maxDepth = -1 + config.maxDepth = 0 } return result @@ -323,7 +323,7 @@ const ErrorPlugin: NewPlugin = { )}}` config.budget.used += result.length if (config.budget.used > config.budget.max) { - config.maxDepth = -1 + config.maxDepth = 0 } return result }, diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts new file mode 100644 index 000000000000..c7ec6b1f61b4 --- /dev/null +++ b/test/core/test/pretty-format.test.ts @@ -0,0 +1,95 @@ +import { format } from '@vitest/pretty-format' +import { describe, expect, test } from 'vitest' + +describe('maxOutputLength budget', () => { + // https://github.com/vitest-dev/vitest/issues/9329 + // Object graphs with shared references that fan out can cause exponential + // output growth, hitting Node's string length limit. The budget (maxOutputLength) + // guards against this amplification by setting maxDepth = 0 once exceeded. + + function createSharedRefGraph(n: number) { + // N columns each referencing the same shared model/events objects. + // When formatting `groups` (not the root model), columnModel is NOT + // an ancestor — so [Circular] doesn't apply and each column fully + // re-expands the shared model, causing exponential blowup. + const model = { columns: [] as any[], groups: [] as any[] } + const events = { model, listeners: [] } + for (let i = 0; i < n; i++) { + const col = { id: `col${i}`, model, events } + model.columns.push(col) + if (i % 3 === 0) { + model.groups.push(col) + } + } + return model + } + + test('terminates on exponential shared-reference graph', () => { + const model = createSharedRefGraph(100) + // Without budget this would hit Node's string length limit. + // With default budget (100_000) it completes quickly. + const result = format(model.groups) + expect(result.length).toBeLessThan(100_000) + }) + + test('custom maxOutputLength limits output', () => { + const model = createSharedRefGraph(50) + const small = format(model.groups, { maxOutputLength: 5_000 }) + const large = format(model.groups, { maxOutputLength: 50_000 }) + expect(small.length).toBeLessThan(large.length) + }) + + test('abbreviated objects use [ClassName] form after budget exceeded', () => { + const model = createSharedRefGraph(20) + const result = format(model.groups, { maxOutputLength: 1_000 }) + // Once budget trips, remaining objects render as [Object] / [Array] + expect(result).toContain('[Object]') + }) + + test('early elements expanded, later elements folded after budget trips', () => { + // Visualizes the kill-switch: once budget is exceeded, maxDepth is set to 0 + // and all subsequent objects render as [ClassName] while earlier ones are full. + const arr = Array.from({ length: 5 }, (_, i) => ({ id: i, nested: { x: i } })) + expect(format(arr, { maxOutputLength: 100 })).toMatchInlineSnapshot(` + "Array [ + Object { + "id": 0, + "nested": Object { + "x": 0, + }, + }, + Object { + "id": 1, + "nested": Object { + "x": 1, + }, + }, + [Object], + [Object], + [Object], + ]" + `) + }) + + test('does not affect simple values', () => { + // Flat arrays of primitives should format normally — no amplification. + expect(format([1, 2, 3], { maxOutputLength: 100 })).toMatchInlineSnapshot(` + "Array [ + 1, + 2, + 3, + ]" + `) + }) + + test('does not affect normal circular references', () => { + const obj: any = { a: 1 } + obj.self = obj + expect(format(obj, { maxOutputLength: 100 })).toMatchInlineSnapshot(` + "Object { + "a": 1, + "self": [Circular], + }" + `) + }) +}) From 6a65bf6ec31a9c2a02d5f94f6d87a347dc4addf2 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 14:54:29 +0900 Subject: [PATCH 03/14] test: wip --- test/core/test/pretty-format.test.ts | 108 +++++++++++++++++++-------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index c7ec6b1f61b4..976736ffc252 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -7,44 +7,90 @@ describe('maxOutputLength budget', () => { // output growth, hitting Node's string length limit. The budget (maxOutputLength) // guards against this amplification by setting maxDepth = 0 once exceeded. - function createSharedRefGraph(n: number) { - // N columns each referencing the same shared model/events objects. - // When formatting `groups` (not the root model), columnModel is NOT - // an ancestor — so [Circular] doesn't apply and each column fully - // re-expands the shared model, causing exponential blowup. - const model = { columns: [] as any[], groups: [] as any[] } - const events = { model, listeners: [] } + // Recursive types show why pretty-format can't terminate: + // expanding a Child means expanding its Parent, which contains all Children again. + type Parent = { children: Child[] } + type Child = { id: number; parent: Parent } + + function createGraph(n: number) { + // format(children) --> child(0) --> parent --> child(0) [Circular] + // | | + // | +--> child(1) --> parent (not ancestor, re-expand!) + // | | +--> child(0) --> parent ... + // | | +--> child(1) [Circular] + // | | +--> child(2) --> parent ... + // | +--> child(2) --> parent (same explosion) + // ... + // child(1) --> parent (same explosion again from sibling) + // + // `parent` is never an ancestor when visiting via a sibling child, + // so [Circular] doesn't apply and each child fully re-expands it. + const parent: Parent = { children: [] } for (let i = 0; i < n; i++) { - const col = { id: `col${i}`, model, events } - model.columns.push(col) - if (i % 3 === 0) { - model.groups.push(col) - } + parent.children.push({ id: i, parent }) } - return model + return parent } - test('terminates on exponential shared-reference graph', () => { - const model = createSharedRefGraph(100) - // Without budget this would hit Node's string length limit. - // With default budget (100_000) it completes quickly. - const result = format(model.groups) - expect(result.length).toBeLessThan(100_000) + test('print graph', () => { + const parent = createGraph(3) + expect(format(parent)).toMatchInlineSnapshot(` + "Object { + "children": Array [ + Object { + "id": 0, + "parent": [Circular], + }, + Object { + "id": 1, + "parent": [Circular], + }, + Object { + "id": 2, + "parent": [Circular], + }, + ], + }" + `) + expect(format(parent.children)).toMatchInlineSnapshot(` + "Array [ + Object { + "id": 0, + "parent": Object { + "children": [Circular], + }, + }, + Object { + "id": 1, + "parent": Object { + "children": [Circular], + }, + }, + Object { + "id": 2, + "parent": Object { + "children": [Circular], + }, + }, + ]" + `) + // const result = format(parent) + // expect(result.length).toBeLessThan(100_000) }) - test('custom maxOutputLength limits output', () => { - const model = createSharedRefGraph(50) - const small = format(model.groups, { maxOutputLength: 5_000 }) - const large = format(model.groups, { maxOutputLength: 50_000 }) - expect(small.length).toBeLessThan(large.length) - }) + // test('custom maxOutputLength limits output', () => { + // const children = createGraph(50) + // const small = format(children, { maxOutputLength: 5_000 }) + // const large = format(children, { maxOutputLength: 50_000 }) + // expect(small.length).toBeLessThan(large.length) + // }) - test('abbreviated objects use [ClassName] form after budget exceeded', () => { - const model = createSharedRefGraph(20) - const result = format(model.groups, { maxOutputLength: 1_000 }) - // Once budget trips, remaining objects render as [Object] / [Array] - expect(result).toContain('[Object]') - }) + // test('abbreviated objects use [ClassName] form after budget exceeded', () => { + // const children = createGraph(20) + // const result = format(children, { maxOutputLength: 1_000 }) + // // Once budget trips, remaining objects render as [Object] / [Array] + // expect(result).toContain('[Object]') + // }) test('early elements expanded, later elements folded after budget trips', () => { // Visualizes the kill-switch: once budget is exceeded, maxDepth is set to 0 From 636a7dc942d12646b1a40f57f81accbf2d480a01 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:11:45 +0900 Subject: [PATCH 04/14] test: cute tests --- test/core/test/pretty-format.test.ts | 217 +++++++++++++++------------ 1 file changed, 123 insertions(+), 94 deletions(-) diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index 976736ffc252..b8eb725cb450 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -2,140 +2,169 @@ import { format } from '@vitest/pretty-format' import { describe, expect, test } from 'vitest' describe('maxOutputLength budget', () => { - // https://github.com/vitest-dev/vitest/issues/9329 - // Object graphs with shared references that fan out can cause exponential - // output growth, hitting Node's string length limit. The budget (maxOutputLength) - // guards against this amplification by setting maxDepth = 0 once exceeded. - - // Recursive types show why pretty-format can't terminate: - // expanding a Child means expanding its Parent, which contains all Children again. - type Parent = { children: Child[] } - type Child = { id: number; parent: Parent } - - function createGraph(n: number) { - // format(children) --> child(0) --> parent --> child(0) [Circular] - // | | - // | +--> child(1) --> parent (not ancestor, re-expand!) - // | | +--> child(0) --> parent ... - // | | +--> child(1) [Circular] - // | | +--> child(2) --> parent ... - // | +--> child(2) --> parent (same explosion) - // ... - // child(1) --> parent (same explosion again from sibling) - // - // `parent` is never an ancestor when visiting via a sibling child, - // so [Circular] doesn't apply and each child fully re-expands it. - const parent: Parent = { children: [] } + function createObjectGraph(n: number) { + // owner + // |-> cats + // |-> cat0 -> owner + // |-> cat1 -> owner + // |-> cat2 + // |-> ... + // |-> dogs + // |-> dog0 + // |-> dog1 + // |-> dog2 + // |-> ... + interface Owner { dogs: Pet[]; cats: Pet[] } + interface Pet { name: string; owner: Owner } + const owner: Owner = { dogs: [], cats: [] } for (let i = 0; i < n; i++) { - parent.children.push({ id: i, parent }) + owner.dogs.push({ name: `dog${i}`, owner }) } - return parent + for (let i = 0; i < n; i++) { + owner.cats.push({ name: `cat${i}`, owner }) + } + return owner } - test('print graph', () => { - const parent = createGraph(3) - expect(format(parent)).toMatchInlineSnapshot(` + test('quadratic growth example depending on format root', () => { + const owner = createObjectGraph(3) + + // when starting from owner, each pet is expanded once, so no amplification, just linear growth. + expect(format(owner)).toMatchInlineSnapshot(` "Object { - "children": Array [ + "cats": Array [ + Object { + "name": "cat0", + "owner": [Circular], + }, + Object { + "name": "cat1", + "owner": [Circular], + }, Object { - "id": 0, - "parent": [Circular], + "name": "cat2", + "owner": [Circular], + }, + ], + "dogs": Array [ + Object { + "name": "dog0", + "owner": [Circular], }, Object { - "id": 1, - "parent": [Circular], + "name": "dog1", + "owner": [Circular], }, Object { - "id": 2, - "parent": [Circular], + "name": "dog2", + "owner": [Circular], }, ], }" `) - expect(format(parent.children)).toMatchInlineSnapshot(` + + // when starting from owner.cats, each cat re-expands the full dogs list via owner. + // this exhibits quadratic growth, which is what the budget is designed to prevent. + expect(format(owner.cats)).toMatchInlineSnapshot(` "Array [ Object { - "id": 0, - "parent": Object { - "children": [Circular], + "name": "cat0", + "owner": Object { + "cats": [Circular], + "dogs": Array [ + Object { + "name": "dog0", + "owner": [Circular], + }, + Object { + "name": "dog1", + "owner": [Circular], + }, + Object { + "name": "dog2", + "owner": [Circular], + }, + ], }, }, Object { - "id": 1, - "parent": Object { - "children": [Circular], + "name": "cat1", + "owner": Object { + "cats": [Circular], + "dogs": Array [ + Object { + "name": "dog0", + "owner": [Circular], + }, + Object { + "name": "dog1", + "owner": [Circular], + }, + Object { + "name": "dog2", + "owner": [Circular], + }, + ], }, }, Object { - "id": 2, - "parent": Object { - "children": [Circular], + "name": "cat2", + "owner": Object { + "cats": [Circular], + "dogs": Array [ + Object { + "name": "dog0", + "owner": [Circular], + }, + Object { + "name": "dog1", + "owner": [Circular], + }, + Object { + "name": "dog2", + "owner": [Circular], + }, + ], }, }, ]" `) - // const result = format(parent) - // expect(result.length).toBeLessThan(100_000) }) - // test('custom maxOutputLength limits output', () => { - // const children = createGraph(50) - // const small = format(children, { maxOutputLength: 5_000 }) - // const large = format(children, { maxOutputLength: 50_000 }) - // expect(small.length).toBeLessThan(large.length) - // }) - - // test('abbreviated objects use [ClassName] form after budget exceeded', () => { - // const children = createGraph(20) - // const result = format(children, { maxOutputLength: 1_000 }) - // // Once budget trips, remaining objects render as [Object] / [Array] - // expect(result).toContain('[Object]') - // }) + test('budget prevents blowup on large graphs', () => { + // TODO: use number that actually breaks on main + const owner = createObjectGraph(100) + const result = format(owner.cats) + expect(result.length).toBeLessThan(100_000) + }) test('early elements expanded, later elements folded after budget trips', () => { - // Visualizes the kill-switch: once budget is exceeded, maxDepth is set to 0 - // and all subsequent objects render as [ClassName] while earlier ones are full. - const arr = Array.from({ length: 5 }, (_, i) => ({ id: i, nested: { x: i } })) + // First few objects are fully expanded, but once budget is exceeded, + // maxDepth = 0 means no more expansion. + const arr = Array.from({ length: 10 }, (_, i) => ({ i })) expect(format(arr, { maxOutputLength: 100 })).toMatchInlineSnapshot(` "Array [ Object { - "id": 0, - "nested": Object { - "x": 0, - }, + "i": 0, }, Object { - "id": 1, - "nested": Object { - "x": 1, - }, + "i": 1, + }, + Object { + "i": 2, + }, + Object { + "i": 3, + }, + Object { + "i": 4, }, [Object], [Object], [Object], + [Object], + [Object], ]" `) }) - - test('does not affect simple values', () => { - // Flat arrays of primitives should format normally — no amplification. - expect(format([1, 2, 3], { maxOutputLength: 100 })).toMatchInlineSnapshot(` - "Array [ - 1, - 2, - 3, - ]" - `) - }) - - test('does not affect normal circular references', () => { - const obj: any = { a: 1 } - obj.self = obj - expect(format(obj, { maxOutputLength: 100 })).toMatchInlineSnapshot(` - "Object { - "a": 1, - "self": [Circular], - }" - `) - }) }) From f61c7987ab66fe93462c86287f6fc14cd602deb0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:34:10 +0900 Subject: [PATCH 05/14] test: polish --- test/core/test/pretty-format.test.ts | 32 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index b8eb725cb450..d0573a53d896 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -14,8 +14,14 @@ describe('maxOutputLength budget', () => { // |-> dog1 // |-> dog2 // |-> ... - interface Owner { dogs: Pet[]; cats: Pet[] } - interface Pet { name: string; owner: Owner } + interface Owner { + dogs: Pet[] + cats: Pet[] + } + interface Pet { + name: string + owner: Owner + } const owner: Owner = { dogs: [], cats: [] } for (let i = 0; i < n; i++) { owner.dogs.push({ name: `dog${i}`, owner }) @@ -132,10 +138,24 @@ describe('maxOutputLength budget', () => { }) test('budget prevents blowup on large graphs', () => { - // TODO: use number that actually breaks on main - const owner = createObjectGraph(100) - const result = format(owner.cats) - expect(result.length).toBeLessThan(100_000) + // quickly hit the kill switch due to quadratic growth + expect([1, 5, 10, 15, 20, 30, 100].map(n => format(createObjectGraph(n).cats).length)) + .toMatchInlineSnapshot(` + [ + 216, + 2744, + 9729, + 21044, + 27554, + 27169, + 27309, + ] + `) + + // depending on object/array shape, output can exceed the limit 100_000, + // but the size should be proportional to the amount of objects and the size of array. + expect(format(createObjectGraph(1000).cats).length).toMatchInlineSnapshot(`99009`) + expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`389799`) }) test('early elements expanded, later elements folded after budget trips', () => { From 4dfb0fbd3628a42e3b201f40a3ad866b6ac02087 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:37:41 +0900 Subject: [PATCH 06/14] test: fix deps --- pnpm-lock.yaml | 3 +++ test/core/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d44e98c9965..bd29eac78a63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1464,6 +1464,9 @@ importers: '@vitest/mocker': specifier: workspace:* version: link:../../packages/mocker + '@vitest/pretty-format': + specifier: workspace:* + version: link:../../packages/pretty-format '@vitest/runner': specifier: workspace:* version: link:../../packages/runner diff --git a/test/core/package.json b/test/core/package.json index db42398579f4..ff850bb52af2 100644 --- a/test/core/package.json +++ b/test/core/package.json @@ -22,6 +22,7 @@ "@test/vite-external": "link:./projects/vite-external", "@vitest/expect": "workspace:*", "@vitest/mocker": "workspace:*", + "@vitest/pretty-format": "workspace:*", "@vitest/runner": "workspace:*", "@vitest/test-dep-cjs": "file:./deps/dep-cjs", "@vitest/test-dep-nested-cjs": "file:./deps/dep-nested-cjs", From b7db37e01d2afc74fb1ea74be2440a57ff4a026b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:38:15 +0900 Subject: [PATCH 07/14] chore: comment --- test/core/test/pretty-format.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index d0573a53d896..e6b56e75dbaf 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -153,7 +153,7 @@ describe('maxOutputLength budget', () => { `) // depending on object/array shape, output can exceed the limit 100_000, - // but the size should be proportional to the amount of objects and the size of array. + // but the output size is proportional to the amount of objects and the size of array. expect(format(createObjectGraph(1000).cats).length).toMatchInlineSnapshot(`99009`) expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`389799`) }) From f1f981e48310055d88d0e6ce7feab5da9cfc805e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:38:34 +0900 Subject: [PATCH 08/14] chore: cleanup --- test/core/test/pretty-format.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index e6b56e75dbaf..19c25e9f7d0d 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -1,7 +1,7 @@ import { format } from '@vitest/pretty-format' import { describe, expect, test } from 'vitest' -describe('maxOutputLength budget', () => { +describe('maxOutputLength', () => { function createObjectGraph(n: number) { // owner // |-> cats From 5aca821f081bb8f5ced66f5544762e41604f12d6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:43:45 +0900 Subject: [PATCH 09/14] fix: tweak default limit --- packages/pretty-format/src/index.ts | 6 ++---- test/core/test/pretty-format.test.ts | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 4bacea380fd0..bd911185cb4a 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -443,10 +443,8 @@ export const DEFAULT_OPTIONS: Options = { highlight: false, indent: 2, maxDepth: Number.POSITIVE_INFINITY, - // Prevent hitting Node's string length limit (~512MB) on pathological object - // graphs with shared references that fan out exponentially. 100K is generous - // enough for normal formatting but well below the ~2**27 danger zone. - maxOutputLength: 100_000, + // Prevent hitting Node's string length limit (~512MB) + maxOutputLength: 1_000_000, maxWidth: Number.POSITIVE_INFINITY, min: false, plugins: [], diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index 19c25e9f7d0d..e03f9c0bc9b1 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -139,23 +139,22 @@ describe('maxOutputLength', () => { test('budget prevents blowup on large graphs', () => { // quickly hit the kill switch due to quadratic growth - expect([1, 5, 10, 15, 20, 30, 100].map(n => format(createObjectGraph(n).cats).length)) + expect([10, 20, 30, 1000, 2000, 3000].map(n => format(createObjectGraph(n).cats).length)) .toMatchInlineSnapshot(` - [ - 216, - 2744, - 9729, - 21044, - 27554, - 27169, - 27309, - ] - `) + [ + 9729, + 36659, + 80789, + 273009, + 374009, + 299009, + ] + `) - // depending on object/array shape, output can exceed the limit 100_000, + // depending on object/array shape, output can exceed the limit 1mb // but the output size is proportional to the amount of objects and the size of array. - expect(format(createObjectGraph(1000).cats).length).toMatchInlineSnapshot(`99009`) - expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`389799`) + expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`999009`) + expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1497738`) }) test('early elements expanded, later elements folded after budget trips', () => { From 43c53adb27427260d1ca0202a803bef3f8d39b4f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:45:08 +0900 Subject: [PATCH 10/14] chore: comment --- packages/pretty-format/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index bd911185cb4a..64e8d83e4c13 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -443,7 +443,8 @@ export const DEFAULT_OPTIONS: Options = { highlight: false, indent: 2, maxDepth: Number.POSITIVE_INFINITY, - // Prevent hitting Node's string length limit (~512MB) + // Practical default hard-limit to avoid too long string being generated + // (Node's limit is 512MB) maxOutputLength: 1_000_000, maxWidth: Number.POSITIVE_INFINITY, min: false, From 9e0e40101cb85c303305a340c362c1648f178b3f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:52:14 +0900 Subject: [PATCH 11/14] refactor: slop --- packages/pretty-format/src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 64e8d83e4c13..63b024aeff06 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -311,7 +311,7 @@ const ErrorPlugin: NewPlugin = { ...rest, } const name = val.name !== 'Error' ? val.name : getConstructorName(val as any) - const result = hitMaxDepth + return hitMaxDepth ? `[${name}]` : `${name} {${printIteratorEntries( Object.entries(entries).values(), @@ -321,11 +321,6 @@ const ErrorPlugin: NewPlugin = { refs, printer, )}}` - config.budget.used += result.length - if (config.budget.used > config.budget.max) { - config.maxDepth = 0 - } - return result }, } From 1374fdc86028e8c0adc9344c1841c5fbabbe36e7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 15:59:36 +0900 Subject: [PATCH 12/14] refactor: slop --- packages/pretty-format/src/index.ts | 110 ++++++++++++++------------- test/core/test/pretty-format.test.ts | 8 +- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 63b024aeff06..c13b989f5627 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -212,11 +212,9 @@ function printComplexValue( return printer(val.toJSON(), config, indentation, depth, refs, true) } - let result: string - const toStringed = toString.call(val) if (toStringed === '[object Arguments]') { - result = hitMaxDepth + return hitMaxDepth ? '[Arguments]' : `${min ? '' : 'Arguments '}[${printListItems( val, @@ -227,8 +225,8 @@ function printComplexValue( printer, )}]` } - else if (isToStringedArrayType(toStringed)) { - result = hitMaxDepth + if (isToStringedArrayType(toStringed)) { + return hitMaxDepth ? `[${val.constructor.name}]` : `${ min @@ -238,8 +236,8 @@ function printComplexValue( : `${val.constructor.name} ` }[${printListItems(val, config, indentation, depth, refs, printer)}]` } - else if (toStringed === '[object Map]') { - result = hitMaxDepth + if (toStringed === '[object Map]') { + return hitMaxDepth ? '[Map]' : `Map {${printIteratorEntries( val.entries(), @@ -251,8 +249,8 @@ function printComplexValue( ' => ', )}}` } - else if (toStringed === '[object Set]') { - result = hitMaxDepth + if (toStringed === '[object Set]') { + return hitMaxDepth ? '[Set]' : `Set {${printIteratorValues( val.values(), @@ -263,36 +261,25 @@ function printComplexValue( printer, )}}` } + // Avoid failure to serialize global window object in jsdom test environment. // For example, not even relevant if window is prop of React element. - else { - result = hitMaxDepth || isWindow(val) - ? `[${getConstructorName(val)}]` - : `${ - min - ? '' - : !config.printBasicPrototype && getConstructorName(val) === 'Object' - ? '' - : `${getConstructorName(val)} ` - }{${printObjectProperties( - val, - config, - indentation, - depth, - refs, - printer, - )}}` - } - - // Post-hoc budget check - // Accumulate output length and if exceeded, force no recursion by patching maxDepth. - // Inspired by node's util.inspect bail out heuristics. - config.budget.used += result.length - if (config.budget.used > config.budget.max) { - config.maxDepth = 0 - } - - return result + return hitMaxDepth || isWindow(val) + ? `[${getConstructorName(val)}]` + : `${ + min + ? '' + : !config.printBasicPrototype && getConstructorName(val) === 'Object' + ? '' + : `${getConstructorName(val)} ` + }{${printObjectProperties( + val, + config, + indentation, + depth, + refs, + printer, + )}}` } const ErrorPlugin: NewPlugin = { @@ -393,29 +380,44 @@ function printer( refs: Refs, hasCalledToJSON?: boolean, ): string { + let result: string + const plugin = findPlugin(config.plugins, val) if (plugin !== null) { - return printPlugin(plugin, val, config, indentation, depth, refs) + result = printPlugin(plugin, val, config, indentation, depth, refs) + } + else { + const basicResult = printBasicValue( + val, + config.printFunctionName, + config.escapeRegex, + config.escapeString, + ) + if (basicResult !== null) { + result = basicResult + } + else { + result = printComplexValue( + val, + config, + indentation, + depth, + refs, + hasCalledToJSON, + ) + } } - const basicResult = printBasicValue( - val, - config.printFunctionName, - config.escapeRegex, - config.escapeString, - ) - if (basicResult !== null) { - return basicResult + // Check string length budget: + // accumulate output length and if exceeded, + // force no further recursion by patching maxDepth. + // Inspired by Node's util.inspect bail out approach. + config.budget.used += result.length + if (config.budget.used > config.budget.max) { + config.maxDepth = 0 } - return printComplexValue( - val, - config, - indentation, - depth, - refs, - hasCalledToJSON, - ) + return result } const DEFAULT_THEME: Theme = { diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index e03f9c0bc9b1..70c188862f65 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -153,8 +153,8 @@ describe('maxOutputLength', () => { // depending on object/array shape, output can exceed the limit 1mb // but the output size is proportional to the amount of objects and the size of array. - expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`999009`) - expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1497738`) + expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`936779`) + expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1236779`) }) test('early elements expanded, later elements folded after budget trips', () => { @@ -175,9 +175,7 @@ describe('maxOutputLength', () => { Object { "i": 3, }, - Object { - "i": 4, - }, + [Object], [Object], [Object], [Object], From 57e01d2ee2fbdcf499d61765cdbdd777c1c60947 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 16:13:06 +0900 Subject: [PATCH 13/14] refactor: rename slop Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/pretty-format/src/index.ts | 7 ++++--- packages/pretty-format/src/types.ts | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index c13b989f5627..1348e6b953cf 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -412,8 +412,8 @@ function printer( // accumulate output length and if exceeded, // force no further recursion by patching maxDepth. // Inspired by Node's util.inspect bail out approach. - config.budget.used += result.length - if (config.budget.used > config.budget.max) { + config.outputLength += result.length + if (config.outputLength > config.maxOutputLength) { config.maxDepth = 0 } @@ -527,7 +527,8 @@ function getConfig(options?: OptionsReceived): Config { printShadowRoot: options?.printShadowRoot ?? true, spacingInner: options?.min ? ' ' : '\n', spacingOuter: options?.min ? '' : '\n', - budget: { used: 0, max: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength }, + maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength, + outputLength: 0, } } diff --git a/packages/pretty-format/src/types.ts b/packages/pretty-format/src/types.ts index 49c2b2ceea66..2e37ebb4f595 100644 --- a/packages/pretty-format/src/types.ts +++ b/packages/pretty-format/src/types.ts @@ -72,7 +72,8 @@ export interface Config { printShadowRoot: boolean spacingInner: string spacingOuter: string - budget: { used: number; max: number } + maxOutputLength: number + outputLength: number } export type Printer = ( From daa613d474e2bbc5828ed468d0e24c9f9f9f567b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Mar 2026 16:14:42 +0900 Subject: [PATCH 14/14] chore: comment --- packages/pretty-format/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 1348e6b953cf..7dcee15ebdb1 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -441,7 +441,7 @@ export const DEFAULT_OPTIONS: Options = { indent: 2, maxDepth: Number.POSITIVE_INFINITY, // Practical default hard-limit to avoid too long string being generated - // (Node's limit is 512MB) + // (Node's limit is buffer.constants.MAX_STRING_LENGTH ~ 512MB) maxOutputLength: 1_000_000, maxWidth: Number.POSITIVE_INFINITY, min: false,