nose-pluto/src/utils.tsx
2025-10-01 21:39:43 -07:00

114 lines
3.1 KiB
TypeScript

////
// Shell utilities and helper functions.
import { $ } from "bun"
import { statSync } from "fs"
import { setFatal } from "./fatal"
import { stat } from "fs/promises"
// Convert /Users/$USER or /home/$USER to ~ for simplicity
export function tilde(path: string): string {
return path.replace(new RegExp(`/(Users|home)/${process.env.USER}`), "~")
}
// Convert ~ to /Users/$USER or /home/$USER for simplicity
export function untilde(path: string): string {
const prefix = process.platform === 'darwin' ? 'Users' : 'home'
return path.replace("~", `/${prefix}/${process.env.USER}`)
}
// Cause a fatal error if a directory doesn't exist.
export function expectDir(path: string): boolean {
if (!isDir(path)) {
setFatal(`Missing critical directory: ${path}`)
return false
}
return true
}
// Cause a fatal error if a system binary doesn't exist.
export async function expectShellCmd(cmd: string): Promise<boolean> {
try {
await $`which ${cmd}`
return true
} catch {
setFatal(`Missing critical dependency: avahi-publish`)
return false
}
}
// Is the given `path` a file?
export function isFile(path: string): boolean {
try {
const stats = statSync(path)
return stats.isFile()
} catch {
return false
}
}
// Is the given `path` a directory?
export function isDir(path: string): boolean {
try {
const stats = statSync(path)
return stats.isDirectory()
} catch {
return false
}
}
export async function mtime(path: string): Promise<Date> {
const { mtime } = await stat(path)
return mtime
}
// is the given file binary?
export async function isBinaryFile(path: string): Promise<boolean> {
// Create a stream to read just the beginning
const file = Bun.file(path)
const stream = file.stream()
const reader = stream.getReader()
try {
// Read first chunk (typically 16KB, which is more than enough to detect binary)
const { value } = await reader.read()
if (!value) return false
// Check first 512 bytes or less
const bytes = new Uint8Array(value)
const checkLength = Math.min(bytes.length, 512)
for (let i = 0; i < checkLength; i++) {
const byte = bytes[i]!
if (byte === 0) return true // null byte
if (byte < 32 && ![9, 10, 13].includes(byte)) return true // control char
}
return false
} finally {
reader.releaseLock()
stream.cancel()
}
}
const transpiler = new Bun.Transpiler({ loader: 'tsx' })
const transpileCache: Record<string, string> = {}
// Transpile the frontend *.ts file at `path` to JavaScript.
export async function transpile(path: string): Promise<string> {
const { mtime } = await stat(path)
const key = `${path}?${mtime}`
let cached = transpileCache[key]
if (!cached) {
const code = await Bun.file(path).text()
cached = transpiler.transformSync(code)
cached = cached.replaceAll(/\bjsxDEV_?\w*\(/g, "jsx(")
cached = cached.replaceAll(/\bFragment_?\w*,/g, "Fragment,")
transpileCache[key] = cached
}
return cached
}