Files
workdsh/scripts/bilingual-docs.mjs

103 lines
3.6 KiB
JavaScript

import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'
const HASH_PATTERN = /^[0-9a-f]{40}$/u
const DOCUMENT_PATTERN = /\.mdx?$/u
function normalizePath(path) {
return path.replaceAll('\\', '/')
}
function runGit(root, args) {
return execFileSync('git', args, {
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
}
export function parseBilingualRecord(recordPath, content) {
const entries = []
for (const [index, line] of content.split(/\r?\n/u).entries()) {
const trimmed = line.trim()
if (trimmed.length === 0 || trimmed.startsWith('#')) continue
const match = /^([^:]+):\s*([0-9a-f]{40})$/u.exec(trimmed)
if (match === null) {
throw new Error(`${recordPath}:${String(index + 1)} is not a filename and Git blob hash`)
}
const filename = match[1]?.trim() ?? ''
const hash = match[2] ?? ''
if (filename !== basename(filename) || isAbsolute(filename) || !DOCUMENT_PATTERN.test(filename)) {
throw new Error(`${recordPath}:${String(index + 1)} must name one Markdown file beside the record`)
}
if (!HASH_PATTERN.test(hash)) {
throw new Error(`${recordPath}:${String(index + 1)} has an invalid Git blob hash`)
}
entries.push({ filename, hash })
}
if (entries.length !== 2) {
throw new Error(`${recordPath} must contain exactly two bilingual document records`)
}
if (entries[0]?.filename === entries[1]?.filename) {
throw new Error(`${recordPath} must reference two distinct bilingual documents`)
}
return entries
}
export function verifyBilingualRecords({ root, recordPaths, readText, hashDocument }) {
let documentCount = 0
const violations = []
for (const recordPath of recordPaths) {
const normalizedRecord = normalizePath(recordPath)
const recordDirectory = dirname(resolve(root, normalizedRecord))
let entries
try {
entries = parseBilingualRecord(normalizedRecord, readText(resolve(root, normalizedRecord)))
} catch (cause) {
violations.push(cause instanceof Error ? cause.message : String(cause))
continue
}
for (const entry of entries) {
const documentPath = resolve(recordDirectory, entry.filename)
const relativeDocument = normalizePath(relative(root, documentPath))
if (relativeDocument.startsWith('../') || isAbsolute(relativeDocument)) {
violations.push(`${normalizedRecord} references a document outside the repository`)
continue
}
try {
readText(documentPath)
const actual = hashDocument(relativeDocument)
if (actual !== entry.hash) {
violations.push(`${normalizedRecord} is stale for ${relativeDocument}: expected ${actual}, recorded ${entry.hash}`)
}
} catch (cause) {
violations.push(`${normalizedRecord} cannot verify ${relativeDocument}: ${cause instanceof Error ? cause.message : String(cause)}`)
}
documentCount += 1
}
}
if (violations.length > 0) {
throw new Error(`bilingual documentation gate failed:\n- ${violations.join('\n- ')}`)
}
return { recordCount: recordPaths.length, documentCount }
}
export function verifyTrackedBilingualRecords(root) {
const recordPaths = runGit(root, ['ls-files', '-z', '--', '*.i18n.yaml'])
.split('\0')
.filter(Boolean)
.map(normalizePath)
return verifyBilingualRecords({
root,
recordPaths,
readText: path => readFileSync(path, 'utf8'),
hashDocument: relativePath => runGit(root, [
'hash-object',
`--path=${relativePath}`,
'--',
relativePath,
]).trim(),
})
}