98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
////
|
|
// Shell utilities and helper functions.
|
|
|
|
import { statSync } from "fs"
|
|
import { basename } from "path"
|
|
import { stat } from "node:fs/promises"
|
|
|
|
import { NOSE_ICON } from "./config"
|
|
|
|
// End the process with an instructive error if a directory doesn't exist.
|
|
export function expectDir(path: string) {
|
|
if (!isDir(path)) {
|
|
console.error(NOSE_ICON)
|
|
console.error(`No ${basename(path)} directory detected. Please run:`)
|
|
console.error("\tmkdir -p", path)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// 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}`), "~")
|
|
}
|
|
|
|
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
|
|
}
|