Files
clash-verge-rev/scripts/gen-contract.mjs
T
Tunglies 5bf0de0d0b feat(contract): export wire contract from backend and generate frontend types
Adds a zero-dependency `frontend-contract` bin that serializes the real
NoticeStatus variants and FrontendEvent payloads, scripts/gen-contract.mjs
which emits the committed src/services/contract.ts, and a CI job that
regenerates it and fails on a non-empty diff. events.ts now derives its
event map from the generated contract instead of hand-typed shapes.
2026-10-01 14:34:17 +08:00

92 lines
3.0 KiB
JavaScript

import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
/**
* Regenerates src/services/contract.ts from the backend wire contract.
* Source of truth: `cargo run -p clash-verge --bin frontend-contract` inside
* src-tauri, which serializes the same enum variants and event payloads the
* app emits at runtime. CI reruns this and fails on a non-empty diff.
*/
const ROOT = path.resolve(import.meta.dirname, '..')
const OUT = path.join(ROOT, 'src/services/contract.ts')
const contract = JSON.parse(
execFileSync(
'cargo',
['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '-q'],
{
cwd: path.join(ROOT, 'src-tauri'),
encoding: 'utf8',
maxBuffer: 16 * 1024 * 1024,
},
),
)
if (contract.version !== 1) {
throw new Error(`unsupported wire contract version: ${contract.version}`)
}
/** TS type per event, derived from the serialized sample payload's JSON shape. */
const payloadType = (name, sample) => {
if (sample === null) return 'null'
if (typeof sample === 'string') return 'string'
if (Array.isArray(sample)) {
if (name === 'verge://notice-message') return '[NoticeStatus, string]'
throw new Error(`unexpected array payload for ${name}`)
}
if (typeof sample === 'object') {
const keys = Object.keys(sample)
if (keys.length === 1 && keys[0] === 'uid') return '{ uid: string }'
if (name === 'verge://run-state-changed') return 'RunState'
throw new Error(`unexpected object payload for ${name}: ${keys.join(',')}`)
}
throw new Error(`unexpected payload for ${name}`)
}
const needsRunState = contract.events.some(
(e) => e.name === 'verge://run-state-changed',
)
if (!needsRunState)
throw new Error('run-state-changed event missing from contract')
const lines = []
lines.push(
'// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.',
)
lines.push('// Regen: pnpm gen:contract')
lines.push('')
lines.push("import type { RunState } from './cmds'")
lines.push('')
lines.push(
'/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */',
)
lines.push('export type NoticeStatus =')
for (const status of contract.noticeStatuses) {
lines.push(` | ${JSON.stringify(status)}`)
}
lines.push('')
lines.push('/** Event name → payload type for every backend-emitted event. */')
lines.push('export interface VergeEventPayloads {')
for (const event of contract.events) {
const key = JSON.stringify(event.name)
lines.push(` ${key}: ${payloadType(event.name, event.sample)}`)
}
lines.push('}')
lines.push('')
const content = `${lines.join('\n')}\n`
fs.writeFileSync(OUT, content)
// Match the repo's biome style so CI parity (`git diff --exit-code`) is stable.
execFileSync(
process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
['exec', 'biome', 'format', '--write', path.relative(ROOT, OUT)],
{ cwd: ROOT, stdio: 'ignore' },
)
console.log(
`[gen-contract] wrote ${path.relative(ROOT, OUT)}: ${contract.events.length} events, ${contract.noticeStatuses.length} statuses`,
)