核心是确认「改配置而不弄乱文件」这件事成立:新增一路入站之后,其余 52 行逐字节不变。 搬进来的改写引擎用默认输出参数时会重排整个文件——行尾注释的对齐空格消失、流式数组被加上内边距—— 连根本没改动的段落也一起波及。不修的话「改一处只动一处」这个承诺直接落空,所以关掉了它的自动排版, 并在写回之后补了一步逐行还原。
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
/**
|
|
* 往返验证:读一份真实配置 → 改一处 → 写回,看未改动的部分保住了多少。
|
|
*
|
|
* 这是整个项目最先要回答的问题。如果写回之后文件面目全非,界面做得再好也没人敢用。
|
|
*
|
|
* 用法:
|
|
* bun run scripts/roundtrip.ts <源配置> <输出目录>
|
|
*
|
|
* 产出三个文件,各对应一种改动,再用 git diff --no-index 跟源文件比对。
|
|
*/
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { replaceDocumentData, openDocument } from '../vendor/config-editor/index.ts'
|
|
import type { ConfigObject } from '../vendor/config-editor/index.ts'
|
|
import { restoreInlineCommentAlignment } from '../shared/yaml-cosmetics.ts'
|
|
|
|
const [sourcePath, outDir] = process.argv.slice(2)
|
|
if (!sourcePath || !outDir) {
|
|
console.error('用法:bun run scripts/roundtrip.ts <源配置> <输出目录>')
|
|
process.exit(1)
|
|
}
|
|
|
|
const source = readFileSync(sourcePath, 'utf8')
|
|
const doc = openDocument(source)
|
|
mkdirSync(outDir, { recursive: true })
|
|
|
|
const clone = (v: unknown) => JSON.parse(JSON.stringify(v)) as ConfigObject
|
|
|
|
/** 每个场景改一处,模拟界面上一次真实操作。 */
|
|
const scenarios: Array<{ file: string; what: string; mutate: (d: ConfigObject) => void }> = [
|
|
{
|
|
file: 'A-加一路入站.yaml',
|
|
what: '新增命名入站 7778,出站固定走 AUTO(本项目的核心操作)',
|
|
mutate: (d) => {
|
|
d.listeners = [
|
|
{ name: '容器A', type: 'mixed', port: 7778, listen: '0.0.0.0', proxy: 'AUTO' },
|
|
]
|
|
},
|
|
},
|
|
{
|
|
file: 'B-改一条规则.yaml',
|
|
what: '把最后一条兜底规则的出站从 PROXY 换成 AUTO',
|
|
mutate: (d) => {
|
|
const rules = d.rules as string[]
|
|
rules[rules.length - 1] = 'MATCH,AUTO'
|
|
},
|
|
},
|
|
{
|
|
file: 'C-改一个策略组.yaml',
|
|
what: '给 PROXY 组的成员列表加一个 REJECT',
|
|
mutate: (d) => {
|
|
const groups = d['proxy-groups'] as Array<Record<string, unknown>>
|
|
const proxy = groups.find((g) => g.name === 'PROXY')!
|
|
proxy.proxies = [...(proxy.proxies as string[]), 'REJECT']
|
|
},
|
|
},
|
|
]
|
|
|
|
const countComments = (yaml: string) =>
|
|
yaml.split('\n').filter((line) => line.trim().startsWith('#') || / #\s/.test(line)).length
|
|
|
|
console.log(`源文件 ${sourcePath}`)
|
|
console.log(` ${source.split('\n').length} 行,其中带注释 ${countComments(source)} 行\n`)
|
|
|
|
for (const scenario of scenarios) {
|
|
const draft = clone(doc.data)
|
|
scenario.mutate(draft)
|
|
const result = replaceDocumentData(doc, draft)
|
|
const written = restoreInlineCommentAlignment(source, result.yaml)
|
|
const outPath = join(outDir, scenario.file)
|
|
writeFileSync(outPath, written, 'utf8')
|
|
|
|
const before = countComments(source)
|
|
const after = countComments(written)
|
|
console.log(`${scenario.file}`)
|
|
console.log(` 改动:${scenario.what}`)
|
|
console.log(` 注释行 ${before} → ${after}${after < before ? `(丢了 ${before - after} 行)` : '(没丢)'}`)
|
|
console.log(` 写到 ${outPath}\n`)
|
|
}
|