[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(test-utils): use correct base

Nate Moore (Mar 24, 2026, 10:52 PM EDT) 9d7ff09c f9719a07

+140 -140
+140 -140
src/test-utils.ts
··· 7 7 import { expect, onTestFinished } from "vitest"; 8 8 9 9 interface ScopedHfsImpl extends HfsImpl { 10 - text(file: string | URL): Promise<string | undefined>; 11 - json(file: string | URL): Promise<unknown | undefined>; 10 + text(file: string | URL): Promise<string | undefined>; 11 + json(file: string | URL): Promise<unknown | undefined>; 12 12 } 13 13 14 14 /** ··· 17 17 * Includes all `hfs` methods — paths are resolved relative to the fixture root. 18 18 */ 19 19 export interface Fixture extends ScopedHfsImpl { 20 - /** The fixture root as a `file://` URL. */ 21 - root: URL; 22 - /** Resolve a relative path within the fixture root. */ 23 - resolve: (...segments: string[]) => URL; 24 - /** Delete the fixture directory. Also runs automatically via `onTestFinished`. */ 25 - cleanup: () => Promise<void>; 20 + /** The fixture root as a `file://` URL. */ 21 + root: URL; 22 + /** Resolve a relative path within the fixture root. */ 23 + resolve: (...segments: string[]) => URL; 24 + /** Delete the fixture directory. Also runs automatically via `onTestFinished`. */ 25 + cleanup: () => Promise<void>; 26 26 } 27 27 28 28 /** Context passed to dynamic file content functions. */ 29 29 export interface FileContext { 30 - /** 31 - * Metadata about the fixture root, analogous to `import.meta`. 32 - * 33 - * - `url` — the fixture root as a `file://` URL string 34 - * - `filename` — absolute filesystem path to the fixture root 35 - * - `dirname` — same as `filename` (root is a directory) 36 - * - `resolve(path)` — resolve a relative path against the fixture root 37 - */ 38 - importMeta: { 39 - url: string; 40 - filename: string; 41 - dirname: string; 42 - resolve: (path: string) => string; 43 - }; 44 - /** 45 - * Create a symbolic link to `target`. 46 - * 47 - * Returns a `SymlinkMarker` — the fixture will create the symlink on disk. 48 - * 49 - * @example 50 - * ```ts 51 - * { 'link.txt': ({ symlink }) => symlink('./target.txt') } 52 - * ``` 53 - */ 54 - symlink: (target: string) => SymlinkMarker; 30 + /** 31 + * Metadata about the fixture root, analogous to `import.meta`. 32 + * 33 + * - `url` — the fixture root as a `file://` URL string 34 + * - `filename` — absolute filesystem path to the fixture root 35 + * - `dirname` — same as `filename` (root is a directory) 36 + * - `resolve(path)` — resolve a relative path against the fixture root 37 + */ 38 + importMeta: { 39 + url: string; 40 + filename: string; 41 + dirname: string; 42 + resolve: (path: string) => string; 43 + }; 44 + /** 45 + * Create a symbolic link to `target`. 46 + * 47 + * Returns a `SymlinkMarker` — the fixture will create the symlink on disk. 48 + * 49 + * @example 50 + * ```ts 51 + * { 'link.txt': ({ symlink }) => symlink('./target.txt') } 52 + * ``` 53 + */ 54 + symlink: (target: string) => SymlinkMarker; 55 55 } 56 56 57 57 const SYMLINK = Symbol("symlink"); 58 58 59 59 /** Opaque marker returned by `ctx.symlink()`. */ 60 60 export interface SymlinkMarker { 61 - [SYMLINK]: true; 62 - target: string; 61 + [SYMLINK]: true; 62 + target: string; 63 63 } 64 64 65 65 /** ··· 74 74 * | Function | `({ importMeta, symlink }) => symlink('./target')` | 75 75 */ 76 76 export type FileTreeValue = 77 - | string 78 - | Buffer 79 - | Record<string, unknown> 80 - | unknown[] 81 - | FileTree 82 - | ((ctx: FileContext) => string | Buffer | SymlinkMarker); 77 + | string 78 + | Buffer 79 + | Record<string, unknown> 80 + | unknown[] 81 + | FileTree 82 + | ((ctx: FileContext) => string | Buffer | SymlinkMarker); 83 83 84 84 /** A recursive tree of files and directories. */ 85 85 export interface FileTree { 86 - [key: string]: FileTreeValue; 86 + [key: string]: FileTreeValue; 87 87 } 88 88 89 89 function isSymlinkMarker(value: unknown): value is SymlinkMarker { 90 - return typeof value === "object" && value !== null && SYMLINK in value; 90 + return typeof value === "object" && value !== null && SYMLINK in value; 91 91 } 92 92 93 93 function isFileTree(value: unknown): value is FileTree { 94 - return ( 95 - typeof value === "object" && 96 - value !== null && 97 - !Buffer.isBuffer(value) && 98 - !Array.isArray(value) && 99 - !isSymlinkMarker(value) 100 - ); 94 + return ( 95 + typeof value === "object" && 96 + value !== null && 97 + !Buffer.isBuffer(value) && 98 + !Array.isArray(value) && 99 + !isSymlinkMarker(value) 100 + ); 101 101 } 102 102 103 103 function scopeHfs(inner: NodeHfs, base: URL): ScopedHfsImpl { 104 - const r = (p: string | URL) => new URL(`./${p}`, base); 105 - const r2 = (a: string | URL, b: string | URL) => [r(a), r(b)] as const; 104 + const r = (p: string | URL) => new URL(`./${p}`, base); 105 + const r2 = (a: string | URL, b: string | URL) => [r(a), r(b)] as const; 106 106 107 - return { 108 - text: (p: string | URL) => inner.text(r(p)), 109 - json: (p: string | URL) => inner.json(r(p)), 110 - bytes: (p) => inner.bytes(r(p)), 111 - write: (p, c) => inner.write(r(p), c), 112 - append: (p, c) => inner.append(r(p), c), 113 - isFile: (p) => inner.isFile(r(p)), 114 - isDirectory: (p) => inner.isDirectory(r(p)), 115 - createDirectory: (p) => inner.createDirectory(r(p)), 116 - delete: (p) => inner.delete(r(p)), 117 - deleteAll: (p) => inner.deleteAll(r(p)), 118 - list: (p) => inner.list(r(p)), 119 - size: (p) => inner.size(r(p)), 120 - lastModified: (p) => inner.lastModified(r(p)), 121 - copy: (s, d) => inner.copy(...r2(s, d)), 122 - copyAll: (s, d) => inner.copyAll(...r2(s, d)), 123 - move: (s, d) => inner.move(...r2(s, d)), 124 - moveAll: (s, d) => inner.moveAll(...r2(s, d)), 125 - }; 107 + return { 108 + text: (p: string | URL) => inner.text(r(p)), 109 + json: (p: string | URL) => inner.json(r(p)), 110 + bytes: (p) => inner.bytes(r(p)), 111 + write: (p, c) => inner.write(r(p), c), 112 + append: (p, c) => inner.append(r(p), c), 113 + isFile: (p) => inner.isFile(r(p)), 114 + isDirectory: (p) => inner.isDirectory(r(p)), 115 + createDirectory: (p) => inner.createDirectory(r(p)), 116 + delete: (p) => inner.delete(r(p)), 117 + deleteAll: (p) => inner.deleteAll(r(p)), 118 + list: (p) => inner.list(r(p)), 119 + size: (p) => inner.size(r(p)), 120 + lastModified: (p) => inner.lastModified(r(p)), 121 + copy: (s, d) => inner.copy(...r2(s, d)), 122 + copyAll: (s, d) => inner.copyAll(...r2(s, d)), 123 + move: (s, d) => inner.move(...r2(s, d)), 124 + moveAll: (s, d) => inner.moveAll(...r2(s, d)), 125 + }; 126 126 } 127 127 128 128 /** ··· 148 148 * ``` 149 149 */ 150 150 export async function createFixture(files: FileTree): Promise<Fixture> { 151 - const raw = expect.getState().currentTestName ?? "bsh"; 152 - const prefix = raw 153 - .toLowerCase() 154 - .replace(/[^a-z0-9]+/g, "-") 155 - .replace(/^-|-$/g, ""); 156 - const root = new URL(`${prefix}-`, `file://${tmpdir()}/`); 157 - const path = await mkdtemp(fileURLToPath(root)); 158 - const base = pathToFileURL(path + sep); 151 + const raw = expect.getState().currentTestName ?? "bsh"; 152 + const prefix = raw 153 + .toLowerCase() 154 + .replace(/[^a-z0-9]+/g, "-") 155 + .replace(/^-|-$/g, ""); 156 + const root = new URL(`${prefix}-`, `file://${tmpdir()}/`); 157 + const path = await mkdtemp(fileURLToPath(root)); 158 + const base = pathToFileURL(path + sep); 159 159 160 - const inner = new NodeHfs(); 161 - const scoped = scopeHfs(inner, base); 162 - const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); 160 + const inner = new NodeHfs(); 161 + const scoped = scopeHfs(inner, base); 162 + const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); 163 163 164 - const ctx: FileContext = { 165 - importMeta: { 166 - url: base.toString(), 167 - filename: fileURLToPath(base), 168 - dirname: fileURLToPath(base), 169 - resolve: (p: string) => new URL(`./${p}`, base).toString(), 170 - }, 171 - symlink: (target: string): SymlinkMarker => ({ [SYMLINK]: true, target }), 172 - }; 164 + const ctx: FileContext = { 165 + importMeta: { 166 + url: base.toString(), 167 + filename: fileURLToPath(base), 168 + dirname: fileURLToPath(base), 169 + resolve: (p: string) => new URL(`./${p}`, base).toString(), 170 + }, 171 + symlink: (target: string): SymlinkMarker => ({ [SYMLINK]: true, target }), 172 + }; 173 173 174 - async function writeTree(tree: FileTree, dir: URL): Promise<void> { 175 - for (const [name, raw] of Object.entries(tree)) { 176 - const url = new URL(name, dir); 174 + async function writeTree(tree: FileTree, dir: URL): Promise<void> { 175 + for (const [name, raw] of Object.entries(tree)) { 176 + const url = new URL(name, dir); 177 177 178 - // Nested directory object (not a plain value) 179 - if ( 180 - typeof raw !== "function" && 181 - !Buffer.isBuffer(raw) && 182 - !Array.isArray(raw) && 183 - isFileTree(raw) && 184 - !name.includes(".") 185 - ) { 186 - await inner.createDirectory(url); 187 - // Trailing slash so nested entries resolve relative to the dir 188 - await writeTree(raw, new URL(`${url}/`)); 189 - continue; 190 - } 178 + // Nested directory object (not a plain value) 179 + if ( 180 + typeof raw !== "function" && 181 + !Buffer.isBuffer(raw) && 182 + !Array.isArray(raw) && 183 + isFileTree(raw) && 184 + !name.includes(".") 185 + ) { 186 + await inner.createDirectory(url); 187 + // Trailing slash so nested entries resolve relative to the dir 188 + await writeTree(raw, new URL(`${url}/`)); 189 + continue; 190 + } 191 191 192 - // Ensure parent directory exists 193 - const parent = new URL("./", url); 194 - await inner.createDirectory(parent); 192 + // Ensure parent directory exists 193 + const parent = new URL("./", url); 194 + await inner.createDirectory(parent); 195 195 196 - // Resolve functions 197 - const content = typeof raw === "function" ? raw(ctx) : raw; 196 + // Resolve functions 197 + const content = typeof raw === "function" ? raw(ctx) : raw; 198 198 199 - // Symlink 200 - if (isSymlinkMarker(content)) { 201 - await fsSymlink(content.target, url); 202 - continue; 203 - } 199 + // Symlink 200 + if (isSymlinkMarker(content)) { 201 + await fsSymlink(content.target, url); 202 + continue; 203 + } 204 204 205 - // Buffer 206 - if (Buffer.isBuffer(content)) { 207 - await inner.write(url, content); 208 - continue; 209 - } 205 + // Buffer 206 + if (Buffer.isBuffer(content)) { 207 + await inner.write(url, content); 208 + continue; 209 + } 210 210 211 - // JSON auto-serialization for .json files with non-string content 212 - if (name.endsWith(".json") && typeof content !== "string") { 213 - await inner.write(url, JSON.stringify(content, null, 2)); 214 - continue; 215 - } 211 + // JSON auto-serialization for .json files with non-string content 212 + if (name.endsWith(".json") && typeof content !== "string") { 213 + await inner.write(url, JSON.stringify(content, null, 2)); 214 + continue; 215 + } 216 216 217 - // String content 218 - await inner.write(url, content as string); 219 - } 220 - } 217 + // String content 218 + await inner.write(url, content as string); 219 + } 220 + } 221 221 222 - await writeTree(files, base); 222 + await writeTree(files, base); 223 223 224 - const cleanup = () => inner.deleteAll(path).then(() => undefined); 225 - onTestFinished(cleanup); 224 + const cleanup = () => inner.deleteAll(path).then(() => undefined); 225 + onTestFinished(cleanup); 226 226 227 - return { 228 - root: pathToFileURL(path), 229 - resolve, 230 - cleanup, 231 - ...scoped, 232 - }; 227 + return { 228 + root: base, 229 + resolve, 230 + cleanup, 231 + ...scoped, 232 + }; 233 233 }