import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { existsSync, readFileSync } from 'node:fs' /** * The git description of the checkout of dir now, a commit or a tag changing it without a restart; outside a * checkout, or without git, the variable named fallbackVariable, which a container image sets, or "dev". */ export function gitVersion(dir: string, fallbackVariable: string) { const described = spawnSync('git', ['describe', '--tags', '--always', '--dirty'], { cwd: dir, encoding: 'utf8' }) return described.status === 0 ? described.stdout.trim() : process.env[fallbackVariable] || 'dev' } /** * Ctrl-C and SIGTERM, which watch mode sends on a restart, call close and end the process when it settles, or after 5 * seconds. */ export function exitOnSignals(close: () => Promise) { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { setTimeout(() => process.exit(0), 5000).unref() void close().then(() => process.exit(0)) }) } } /** * The JSON object of the file at path, and its version, the SHA-256 of the bytes. With missingAsEmpty a missing file * reads as {}, whose version is that of "{}". A file that cannot be read, invalid JSON or another value than an * object throws with the reason. */ export function readJsonObject(path: string, options: { missingAsEmpty: boolean }): { data: Record; version: string } { let bytes: Buffer let data: unknown try { bytes = options.missingAsEmpty && !existsSync(path) ? Buffer.from('{}') : readFileSync(path) data = JSON.parse(bytes.toString('utf8')) } catch (error) { throw new Error(`${path} cannot be read: ${(error as Error).message}`) } if (data === null || typeof data !== 'object' || Array.isArray(data)) { throw new Error(`${path} must hold a JSON object`) } return { data: data as Record, version: createHash('sha256').update(bytes).digest('hex') } }