Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2289ff5
refactor(utils): consolidate flatted into @vitest/utils/serialization
hi-ogawa Jan 29, 2026
0888ded
chore: license
hi-ogawa Jan 29, 2026
194e4d8
chore: update package.json deps
hi-ogawa Jan 29, 2026
4d8bb42
chore: add blob reporter benchmark example
hi-ogawa Jan 29, 2026
993487f
chore: cleanup
hi-ogawa Jan 29, 2026
5111d92
chore: cleanup
hi-ogawa Jan 29, 2026
8e5051b
refactor: cleanup
hi-ogawa Jan 29, 2026
4344ebc
refactor: cleanup
hi-ogawa Jan 29, 2026
dff600d
perf: use devalue
hi-ogawa Jan 29, 2026
94f0eec
chore: demo
hi-ogawa Jan 29, 2026
4d26b35
Merge branch 'main' into refactor/consolidate-flatted-serialization
hi-ogawa Jan 30, 2026
932c942
chore: lint
hi-ogawa Jan 30, 2026
ff94f17
Merge branch 'main' into refactor/consolidate-flatted-serialization
hi-ogawa Jan 31, 2026
1234ac5
chore: cleanup
hi-ogawa Feb 2, 2026
28d5563
fix: fix type
hi-ogawa Feb 2, 2026
0540a36
fix: align devalue serialization with flatted
hi-ogawa Feb 2, 2026
483c961
chore: tweak demo
hi-ogawa Feb 2, 2026
bf446dc
test: update snapshot
hi-ogawa Feb 2, 2026
6d31bf1
(tmp) test:ci:no-bail
hi-ogawa Feb 2, 2026
0b29e42
chore: comment
hi-ogawa Feb 2, 2026
927a502
fix: drop symbol keys in devalue fallback
hi-ogawa Feb 2, 2026
066bebd
chore: .gitignore
hi-ogawa Feb 2, 2026
cadc940
chore: add bench
hi-ogawa Feb 2, 2026
ee76aa0
chore: bench ungap
hi-ogawa Feb 2, 2026
9a60bce
chore: bench ungap
hi-ogawa Feb 2, 2026
7575a53
chore: summary
hi-ogawa Feb 2, 2026
8bb837b
chore: readme
hi-ogawa Feb 2, 2026
b997d5f
chore: cleanup
hi-ogawa Feb 2, 2026
8b50ae8
refactor: simplify custom serializer
hi-ogawa Feb 2, 2026
11b24f0
chore: cleanup
hi-ogawa Feb 2, 2026
960f2a0
chore: lint
hi-ogawa Feb 2, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
run: pnpm run build

- name: Test
run: pnpm run test:ci
run: pnpm run test:ci:no-bail

- name: Test Examples
run: pnpm run test:examples
Expand Down
1 change: 0 additions & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
"@types/ws": "catalog:",
"@vitest/runner": "workspace:*",
"birpc": "catalog:",
"flatted": "catalog:",
"ivya": "^1.7.0",
"mime": "^4.1.0",
"pathe": "catalog:",
Expand Down
14 changes: 2 additions & 12 deletions packages/browser/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { CancelReason } from '@vitest/runner'
import type { BirpcReturn } from 'birpc'
import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../types'
import type { IframeOrchestrator } from './orchestrator'
import { parse, stringify } from '@vitest/utils/serialization'
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { getBrowserState } from './utils'

const PAGE_TYPE = getBrowserState().type
Expand Down Expand Up @@ -112,17 +112,7 @@ function createClient() {
{
post: msg => ctx.ws.send(msg),
on: fn => (onMessage = fn),
serialize: e =>
stringify(e, (_, v) => {
if (v instanceof Error) {
return {
name: v.name,
message: v.message,
stack: v.stack,
}
}
return v
}),
serialize: e => stringify(e),
deserialize: parse,
timeout: -1, // createTesters can take a while
},
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/tester.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { BrowserRPC, IframeChannelEvent } from '@vitest/browser/client'
import type { FileSpecification } from '@vitest/runner'
import { channel, client, onCancel } from '@vitest/browser/client'
import { parse } from 'flatted'
import { parse } from '@vitest/utils/serialization'
import { page, server, userEvent } from 'vitest/browser'
import {
collectTests,
Expand Down
36 changes: 2 additions & 34 deletions packages/browser/src/node/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import type { BrowserServerState } from './state'
import { existsSync, promises as fs, readFileSync } from 'node:fs'
import { AutomockedModule, AutospiedModule, ManualMockedModule, RedirectedModule } from '@vitest/mocker'
import { ServerMockResolver } from '@vitest/mocker/node'
import { parse, stringify } from '@vitest/utils/serialization'
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { dirname, join, resolve } from 'pathe'
import { createDebugger, isFileLoadingAllowed, isValidApiRequest } from 'vitest/node'
import { WebSocketServer } from 'ws'
Expand Down Expand Up @@ -398,7 +398,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
post: msg => ws.send(msg),
on: fn => ws.on('message', fn),
eventNames: ['onCancel', 'cdpEvent'],
serialize: (data: any) => stringify(data, stringifyReplace),
serialize: (data: any) => stringify(data),
deserialize: parse,
timeout: -1, // createTesters can take a long time
},
Expand All @@ -424,35 +424,3 @@ function retrieveSourceMapURL(source: string): string | null {
}
return lastMatch[1]
}

// Serialization support utils.
function cloneByOwnProperties(value: any) {
// Clones the value's properties into a new Object. The simpler approach of
// Object.assign() won't work in the case that properties are not enumerable.
return Object.getOwnPropertyNames(value).reduce(
(clone, prop) => ({
...clone,
[prop]: value[prop],
}),
{},
)
}

/**
* Replacer function for serialization methods such as JS.stringify() or
* flatted.stringify().
*/
export function stringifyReplace(key: string, value: any): any {
if (value instanceof Error) {
const cloned = cloneByOwnProperties(value)
return {
name: value.name,
message: value.message,
stack: value.stack,
...cloned,
}
}
else {
return value
}
}
2 changes: 1 addition & 1 deletion packages/browser/src/node/serverOrchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { ProjectBrowser } from './project'
import type { ParentBrowserProject } from './projectParent'
import { stringify } from 'flatted'
import { stringify } from '@vitest/utils/serialization'
import { replacer } from './utils'

export async function resolveOrchestrator(
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/client/components/FileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import type { RunnerTask, RunnerTestCase } from 'vitest'
import type { ModuleGraph } from '~/composables/module-graph'
import type { Params } from '~/composables/params'
import { toJSON } from '@vitest/utils/serialization'
import { debouncedWatch } from '@vueuse/core'
import { toJSON } from 'flatted'
import { computed, nextTick, ref } from 'vue'
import DetailsHeaderButtons from '~/components/DetailsHeaderButtons.vue'
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/client/composables/client/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {
WebSocketEvents,
WebSocketHandlers,
} from 'vitest'
import { parse } from '@vitest/utils/serialization'
import { decompressSync, strFromU8 } from 'fflate'
import { parse } from 'flatted'
import { reactive } from 'vue'
import { StateManager } from '../../../../ws-client/src/state'

Expand Down
2 changes: 1 addition & 1 deletion packages/ui/node/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { readFile, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { gzip, constants as zlibConstants } from 'node:zlib'
import { stringify } from 'flatted'
import { stringify } from '@vitest/utils/serialization'
import mime from 'mime/lite'
import { dirname, extname, relative, resolve } from 'pathe'
import { globSync } from 'tinyglobby'
Expand Down
1 change: 0 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"dependencies": {
"@vitest/utils": "workspace:*",
"fflate": "^0.8.2",
"flatted": "catalog:",
"pathe": "catalog:",
"sirv": "catalog:",
"tinyglobby": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default defineConfig({
alias: {
'~/': `${resolve(import.meta.dirname, 'client')}/`,
'@vitest/ws-client': `${resolve(import.meta.dirname, '../ws-client/src/index.ts')}`,
'@vitest/utils/serialization': `${resolve(import.meta.dirname, '../utils/src/serialization.ts')}`,
},
},
define: {
Expand Down
6 changes: 6 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
"types": "./dist/serialize.d.ts",
"default": "./dist/serialize.js"
},
"./serialization": {
"types": "./dist/serialization.d.ts",
"default": "./dist/serialization.js"
},
"./*": "./*"
},
"main": "./dist/index.js",
Expand All @@ -86,12 +90,14 @@
},
"dependencies": {
"@vitest/pretty-format": "workspace:*",
"devalue": "^5.6.2",
"tinyrainbow": "catalog:"
},
"devDependencies": {
"@jridgewell/trace-mapping": "catalog:",
"@types/estree": "catalog:",
"diff-sequences": "^29.6.3",
"flatted": "3.3.3",
"loupe": "^3.2.1",
"tinyhighlight": "^0.3.2"
}
Expand Down
1 change: 1 addition & 0 deletions packages/utils/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const entries = {
'display': 'src/display.ts',
'resolver': 'src/resolver.ts',
'serialize': 'src/serialize.ts',
'serialization': 'src/serialization.ts',
}

const external = [
Expand Down
116 changes: 116 additions & 0 deletions packages/utils/src/serialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as devalue from 'devalue'

function cloneByOwnProperties(value: object): Record<string, unknown> {
// Clones the value's properties into a new Object. The simpler approach of
// Object.assign() won't work in the case that properties are not enumerable.
return Object.getOwnPropertyNames(value).reduce<Record<string, unknown>>(
(clone, prop) => {
clone[prop] = (value as Record<string, unknown>)[prop]
return clone
},
{},
)
}

function serializeError(error: Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
...cloneByOwnProperties(error),
}
}

// https://github.com/sveltejs/devalue/blob/fcf4e88275f2e2e45b9ea70ffaa5247c8f55f057/src/stringify.js
const devalueBuiltins = new Set([
'[object Array]',
'[object Date]',
'[object RegExp]',
'[object Map]',
'[object Set]',
'[object URL]',
'[object URLSearchParams]',
'[object ArrayBuffer]',
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]',
'[object BigInt64Array]',
'[object BigUint64Array]',
'[object Number]',
'[object String]',
'[object Boolean]',
'[object BigInt]',
'[object Temporal.Duration]',
'[object Temporal.Instant]',
'[object Temporal.PlainDate]',
'[object Temporal.PlainTime]',
'[object Temporal.PlainDateTime]',
'[object Temporal.PlainMonthDay]',
'[object Temporal.PlainYearMonth]',
'[object Temporal.ZonedDateTime]',
])

function isCustomObject(value: unknown): value is object {
// check primitive
if (!value || typeof value !== 'object') {
return false
}
// check plain object
const proto = Object.getPrototypeOf(value)
if (proto === Object.prototype || proto === null) {
if (Object.getOwnPropertySymbols(value).length > 0) {
return true
}
return false
}
// check devalue builtin support
const tag = Object.prototype.toString.call(value)
if (devalueBuiltins.has(tag)) {
return false
}
return true
}

const customTypes = {
stringify: {
vi_custom: (v: unknown) => {
if (v instanceof Error) {
return serializeError(v)
}
// handle non-pojo like flatted since devalue throws otherwise
if (isCustomObject(v)) {
// mirror JSON/flatted behavior for custom toJSON
if (typeof (v as any).toJSON === 'function') {
return (v as any).toJSON()
}
// drop symbol keys to mirror JSON/flatted behavior
const clone: any = {}
for (const key of Object.keys(v)) {
clone[key] = (v as any)[key]
}
return clone
}
},
},
parse: {
vi_custom: (v: unknown) => v,
},
}

export function parse<T = any>(text: string): T {
return devalue.parse(text, customTypes.parse)
}

export function stringify(value: unknown): string {
return devalue.stringify(value, customTypes.stringify)
}

export function toJSON<T>(value: T): T {
return parse(stringify(value))
}
25 changes: 1 addition & 24 deletions packages/vitest/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ SOFTWARE.

# Licenses of bundled dependencies
The published Vitest artifact additionally contains code with the following licenses:
BSD-3-Clause, ISC, MIT
BSD-3-Clause, MIT

# Bundled dependencies:
## @antfu/install-pkg
Expand Down Expand Up @@ -333,29 +333,6 @@ Repository: lukeed/empathic

---------------------------------------

## flatted
License: ISC
By: Andrea Giammarchi
Repository: git+https://github.com/WebReflection/flatted.git

> ISC License
>
> Copyright (c) 2018-2020, Andrea Giammarchi, @WebReflection
>
> Permission to use, copy, modify, and/or distribute this software for any
> purpose with or without fee is hereby granted, provided that the above
> copyright notice and this permission notice appear in all copies.
>
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
> REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
> AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
> INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
> LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
> OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
> PERFORMANCE OF THIS SOFTWARE.

---------------------------------------

## js-tokens
License: MIT
By: Simon Lydell
Expand Down
1 change: 0 additions & 1 deletion packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@
"birpc": "catalog:",
"cac": "catalog:",
"empathic": "^2.0.0",
"flatted": "catalog:",
"happy-dom": "^20.4.0",
"jsdom": "^27.4.0",
"local-pkg": "^1.1.2",
Expand Down
5 changes: 2 additions & 3 deletions packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ import type {
import { existsSync, promises as fs } from 'node:fs'
import { performance } from 'node:perf_hooks'
import { noop } from '@vitest/utils/helpers'
import { parse, stringify } from '@vitest/utils/serialization'
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { WebSocketServer } from 'ws'
import { API_PATH } from '../constants'
import { isFileServingAllowed } from '../node/vite'
import { getTestFileEnvironment } from '../utils/environments'
import { getModuleGraph } from '../utils/graph'
import { stringifyReplace } from '../utils/serialization'
import { isValidApiRequest } from './check'

export function setup(ctx: Vitest, _server?: ViteDevServer): void {
Expand Down Expand Up @@ -196,7 +195,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
'onCollected',
'onTaskUpdate',
],
serialize: (data: any) => stringify(data, stringifyReplace),
serialize: (data: any) => stringify(data),
deserialize: parse,
timeout: -1,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/cache/fsModuleCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Vitest } from '../core'
import type { ResolvedConfig } from '../types/config'
import fs, { existsSync, mkdirSync, readFileSync } from 'node:fs'
import { readFile, rename, rm, stat, unlink, writeFile } from 'node:fs/promises'
import { parse, stringify } from 'flatted'
import { parse, stringify } from '@vitest/utils/serialization'
import { dirname, join } from 'pathe'
import c from 'tinyrainbow'
import { searchForWorkspaceRoot } from 'vite'
Expand Down
Loading
Loading