114 lines
3.1 KiB
TypeScript
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
|
|
}
|