102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import { diffJson } from 'diff'
|
|
|
|
/** 统一键名大小写,避免预览(camelCase)与落库(PascalCase)误判差异 */
|
|
function canonicalKey(key: string): string {
|
|
if (!key) return key
|
|
return key.charAt(0).toLowerCase() + key.slice(1)
|
|
}
|
|
|
|
/** 递归规范化快照对象,便于语义对比 */
|
|
export function normalizeSnapshotForDiff(input: unknown): unknown {
|
|
if (input === null || input === undefined) return input
|
|
if (Array.isArray(input)) {
|
|
return input.map((item) => normalizeSnapshotForDiff(item))
|
|
}
|
|
if (typeof input === 'object') {
|
|
const record = input as Record<string, unknown>
|
|
const keys = Object.keys(record).sort((a, b) =>
|
|
canonicalKey(a).localeCompare(canonicalKey(b))
|
|
)
|
|
const out: Record<string, unknown> = {}
|
|
for (const key of keys) {
|
|
out[canonicalKey(key)] = normalizeSnapshotForDiff(record[key])
|
|
}
|
|
return out
|
|
}
|
|
return input
|
|
}
|
|
|
|
export function parseSnapshotJson(raw: unknown): unknown {
|
|
if (raw == null || raw === '') return {}
|
|
if (typeof raw === 'string') {
|
|
try {
|
|
return JSON.parse(raw)
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
return raw
|
|
}
|
|
|
|
export function formatSnapshotJson(raw: unknown): string {
|
|
try {
|
|
const parsed = parseSnapshotJson(raw)
|
|
return JSON.stringify(parsed, null, 2)
|
|
} catch {
|
|
return typeof raw === 'string' ? raw : '{}'
|
|
}
|
|
}
|
|
|
|
export type SnapshotDiffRow =
|
|
| { type: 'add' | 'remove' | 'same'; text: string }
|
|
| { type: 'change'; oldText: string; newText: string }
|
|
|
|
function flattenDiffParts(diff: ReturnType<typeof diffJson>) {
|
|
const flat: Array<{ text: string; status: 'add' | 'remove' | 'same' }> = []
|
|
diff.forEach((part) => {
|
|
const status = part.added ? 'add' : part.removed ? 'remove' : 'same'
|
|
const lines = (part.value || '').split('\n')
|
|
lines.forEach((line, idx) => {
|
|
if (idx === lines.length - 1 && line === '') return
|
|
flat.push({ text: line, status })
|
|
})
|
|
})
|
|
const merged: SnapshotDiffRow[] = []
|
|
let i = 0
|
|
while (i < flat.length) {
|
|
const curr = flat[i]
|
|
const next = flat[i + 1]
|
|
if (curr?.status === 'remove' && next?.status === 'add') {
|
|
merged.push({ type: 'change', oldText: curr.text, newText: next.text })
|
|
i += 2
|
|
} else if (curr) {
|
|
merged.push({ type: curr.status, text: curr.text })
|
|
i += 1
|
|
} else {
|
|
i += 1
|
|
}
|
|
}
|
|
return merged
|
|
}
|
|
|
|
export function computeSnapshotDiff(
|
|
originalRaw: unknown,
|
|
modifiedRaw: unknown
|
|
): { length: number; rows: SnapshotDiffRow[] } {
|
|
try {
|
|
const original = normalizeSnapshotForDiff(parseSnapshotJson(originalRaw))
|
|
const modified = normalizeSnapshotForDiff(parseSnapshotJson(modifiedRaw))
|
|
const diff = diffJson(original, modified)
|
|
const rows = flattenDiffParts(diff)
|
|
const length = diff.filter((item) => item.added || item.removed).length
|
|
return { length, rows }
|
|
} catch {
|
|
return { length: 0, rows: [] }
|
|
}
|
|
}
|
|
|
|
export function filterDiffRows(rows: SnapshotDiffRow[], showOnlyDiff: boolean) {
|
|
if (!showOnlyDiff) return rows
|
|
return rows.filter((row) => row.type !== 'same')
|
|
}
|