Remove dev check in payload/pojo debugger (#5152)

* Remove dev dcheck in payload/pojo debugger

Signed-off-by: Calum H. <contact@cal.engineer>

* feat: better payload debugger

---------

Signed-off-by: Calum H. <contact@cal.engineer>
This commit is contained in:
Calum H.
2026-01-19 18:28:25 +00:00
committed by GitHub
parent 2efcd383bb
commit 3f3e6f5199
3 changed files with 91 additions and 20 deletions

View File

@@ -57,6 +57,7 @@
"ace-builds": "^1.36.2",
"ansi-to-html": "^0.7.2",
"dayjs": "^1.11.7",
"devalue": "^5.6.2",
"dompurify": "^3.1.7",
"floating-vue": "^5.2.2",
"fuse.js": "^6.6.2",

View File

@@ -1,37 +1,99 @@
import { stringify } from 'devalue'
function findNonPOJOs(
obj: unknown,
path: string,
found: Array<{ path: string; type: string }> = [],
seen = new WeakSet(),
): Array<{ path: string; type: string }> {
if (obj === null || typeof obj !== 'object') return found
const proto = Object.getPrototypeOf(obj)
if (proto !== Object.prototype && proto !== null && !Array.isArray(obj)) {
found.push({ path, type: obj.constructor?.name ?? 'Unknown' })
// Prevent circular reference infinite loops
if (seen.has(obj)) return found
seen.add(obj)
if (typeof obj === 'function') {
found.push({ path, type: 'Function' })
return found
}
for (const [key, value] of Object.entries(obj)) {
findNonPOJOs(value, `${path}.${key}`, found)
const proto = Object.getPrototypeOf(obj)
const constructorName = obj.constructor?.name ?? 'Unknown'
// Check for non-POJOs (not Object, Array, or null prototype)
if (proto !== Object.prototype && proto !== null && !Array.isArray(obj)) {
found.push({ path, type: constructorName })
}
// Check for Vue internals that shouldn't be serialized
if ('__v_isRef' in obj || '__v_isReactive' in obj || '_rawValue' in obj) {
found.push({ path, type: `Vue:${constructorName}` })
}
// Check for IntlMessageFormat specifically
if ('_ast' in obj || constructorName === 'IntlMessageFormat') {
found.push({ path, type: 'IntlMessageFormat' })
}
try {
for (const [key, value] of Object.entries(obj)) {
findNonPOJOs(value, `${path}.${key}`, found, seen)
}
} catch {
found.push({ path, type: `NonIterable:${constructorName}` })
}
return found
}
function checkPayload(payload: unknown, hookName: string): void {
try {
stringify(payload)
} catch (e) {
const nonPOJOs = findNonPOJOs(payload, 'payload')
console.error(`[payload-debugger] [${hookName}] Devalue serialization failed:`, e)
if (nonPOJOs.length > 0) {
console.error(`[payload-debugger] [${hookName}] Non-POJO objects found:`)
for (const { path, type } of nonPOJOs.slice(0, 20)) {
console.error(` - ${path}: ${type}`)
}
if (nonPOJOs.length > 20) {
console.error(` ... and ${nonPOJOs.length - 20} more`)
}
} else {
console.error(
`[payload-debugger] [${hookName}] No non-POJOs found by walker - issue may be circular refs or special values`,
)
}
}
}
export default defineNuxtPlugin((nuxtApp) => {
if (!import.meta.dev || !import.meta.server) return
if (!import.meta.server) return
nuxtApp.hooks.hook('app:rendered', () => {
try {
JSON.stringify(nuxtApp.payload)
} catch (e) {
console.error('[payload-debugger] Payload serialization would fail:', e)
const nonPOJOs = findNonPOJOs(nuxtApp.payload, 'payload')
if (nonPOJOs.length > 0) {
console.error('[payload-debugger] Non-POJO objects found in payload:')
for (const { path, type } of nonPOJOs) {
console.error(` - ${path}: ${type}`)
}
}
}
checkPayload(nuxtApp.payload, 'app:rendered')
})
if (nuxtApp.payload.data && typeof nuxtApp.payload.data === 'object') {
const originalData = nuxtApp.payload.data
const allowedConstructors = new Set(['Object', 'Array', 'String', 'Number', 'Boolean', 'Date'])
nuxtApp.payload.data = new Proxy(originalData, {
set(target, prop, value) {
if (value !== null && typeof value === 'object') {
const constructorName = value.constructor?.name
if (constructorName && !allowedConstructors.has(constructorName)) {
console.error(
`[payload-debugger] [proxy] Non-POJO assigned to payload.data.${String(prop)}:`,
constructorName,
)
console.error(new Error('Stack trace').stack)
}
}
;(target as Record<string | symbol, unknown>)[prop] = value
return true
},
})
}
})

12
pnpm-lock.yaml generated
View File

@@ -293,6 +293,9 @@ importers:
dayjs:
specifier: ^1.11.7
version: 1.11.19
devalue:
specifier: ^5.6.2
version: 5.6.2
dompurify:
specifier: ^3.1.7
version: 3.3.1
@@ -5268,6 +5271,9 @@ packages:
devalue@5.6.1:
resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==}
devalue@5.6.2:
resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -11150,7 +11156,7 @@ snapshots:
consola: 3.4.2
defu: 6.1.4
destr: 2.0.5
devalue: 5.6.1
devalue: 5.6.2
errx: 0.1.0
escape-string-regexp: 5.0.0
exsolve: 1.0.8
@@ -14359,6 +14365,8 @@ snapshots:
devalue@5.6.1: {}
devalue@5.6.2: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
@@ -16809,7 +16817,7 @@ snapshots:
cookie-es: 2.0.0
defu: 6.1.4
destr: 2.0.5
devalue: 5.6.1
devalue: 5.6.2
errx: 0.1.0
escape-string-regexp: 5.0.0
exsolve: 1.0.8