Files
ims/scripts/generate-qr.ts
T
adminandClaude Fable 5 81282fbd8a refactor: make generate-qr testable with injectable deps + entry guard
Extracts buildQrUrl and generateZoneQrs with injectable I/O deps and adds
tests/scripts/generate-qr.test.ts (20 tests). Refactor + tests originated
from a concurrent working session; this commit adds on top:
- import.meta entry guard — the unconditional top-level main() executed on
  test import and wrote (sometimes truncated) PNGs into public/qr
- typed vi.fn generics in makeDeps so strict tsc passes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 10:40:38 +08:00

76 lines
2.2 KiB
TypeScript

// scripts/generate-qr.ts
import QRCode from 'qrcode'
import { mkdirSync } from 'fs'
import path from 'path'
import { pathToFileURL } from 'url'
export const DEFAULT_BASE_URL = 'http://localhost:3000'
// Matches seeded zone qr_code_token values in 20260709000008_seed.sql
export const ZONES = [
{ token: 'scw1-dock-a-qr-2026', name: 'SCW1 — Dock A' },
{ token: 'scw1-cold-storage-qr-2026', name: 'SCW1 — Cold Storage' },
{ token: 'scw1-loading-bay-qr-2026', name: 'SCW1 — Loading Bay' },
]
export interface Zone {
token: string
name: string
}
/** Pure helper: builds the URL encoded in each QR code. */
export function buildQrUrl(baseUrl: string, token: string): string {
return `${baseUrl}/report?zone=${token}`
}
export interface GenerateDeps {
toFile: (outputPath: string, url: string, opts: object) => Promise<void>
mkdirSync: (dir: string, opts: { recursive: boolean }) => void
log: (...args: unknown[]) => void
}
/** Core logic, with injectable deps so tests never touch real I/O. */
export async function generateZoneQrs(
zones: Zone[],
outDir: string,
baseUrl: string,
deps: GenerateDeps,
): Promise<void> {
deps.mkdirSync(outDir, { recursive: true })
for (const zone of zones) {
const url = buildQrUrl(baseUrl, zone.token)
const outputPath = path.join(outDir, `${zone.token}.png`)
await deps.toFile(outputPath, url, {
width: 400,
margin: 2,
color: { dark: '#000000', light: '#FFFFFF' },
})
deps.log(`✓ ${zone.name}`)
deps.log(` → ${url}`)
deps.log(` → ${outputPath}`)
}
}
async function main() {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_BASE_URL
const outDir = path.join(process.cwd(), 'public', 'qr')
await generateZoneQrs(ZONES, outDir, baseUrl, {
toFile: (outputPath, url, opts) => QRCode.toFile(outputPath, url, opts as Parameters<typeof QRCode.toFile>[2]),
mkdirSync,
log: console.log,
})
}
// Only run when executed directly (npm run generate-qr) — importing this
// module (e.g. from tests) must not touch the filesystem.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((err) => {
console.error(err)
process.exit(1)
})
}