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
76 lines
2.2 KiB
TypeScript
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)
|
|
})
|
|
}
|