@bomb.sh/tools
Version:
The internal dev, build, and lint CLI for Bombshell projects
1 lines • 10.1 kB
Source Map (JSON)
{"version":3,"file":"fixture.mjs","names":["fsSymlink"],"sources":["../../src/test-utils/fixture.ts"],"sourcesContent":["import { mkdtemp, symlink as fsSymlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { sep } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { NodeHfs } from \"@humanfs/node\";\nimport type { HfsImpl } from \"@humanfs/types\";\nimport { expect, onTestFinished } from \"vitest\";\n\ninterface ScopedHfsImpl extends Required<HfsImpl> {\n\ttext(file: string | URL): Promise<string | undefined>;\n\tjson(file: string | URL): Promise<unknown | undefined>;\n}\n\n/**\n * A temporary fixture directory with a scoped `hfs` filesystem.\n *\n * Includes all `hfs` methods — paths are resolved relative to the fixture root.\n */\nexport interface Fixture extends ScopedHfsImpl {\n\t/** The fixture root as a `file://` URL. */\n\troot: URL;\n\t/** Resolve a relative path within the fixture root. */\n\tresolve: (...segments: string[]) => URL;\n\t/** Delete the fixture directory. Also runs automatically via `onTestFinished`. */\n\tcleanup: () => Promise<void>;\n}\n\n/** Context passed to dynamic file content functions. */\nexport interface FileContext {\n\t/**\n\t * Metadata about the fixture root, analogous to `import.meta`.\n\t *\n\t * - `url` — the fixture root as a `file://` URL string\n\t * - `filename` — absolute filesystem path to the fixture root\n\t * - `dirname` — same as `filename` (root is a directory)\n\t * - `resolve(path)` — resolve a relative path against the fixture root\n\t */\n\timportMeta: {\n\t\turl: string;\n\t\tfilename: string;\n\t\tdirname: string;\n\t\tresolve: (path: string) => string;\n\t};\n\t/**\n\t * Create a symbolic link to `target`.\n\t *\n\t * Returns a `SymlinkMarker` — the fixture will create the symlink on disk.\n\t *\n\t * @example\n\t * ```ts\n\t * { 'link.txt': ({ symlink }) => symlink('./target.txt') }\n\t * ```\n\t */\n\tsymlink: (target: string) => SymlinkMarker;\n}\n\nconst SYMLINK = Symbol(\"symlink\");\n\n/** Opaque marker returned by `ctx.symlink()`. */\nexport interface SymlinkMarker {\n\t[SYMLINK]: true;\n\ttarget: string;\n}\n\n/**\n * A value in the file tree.\n *\n * | Type | Example |\n * |------|---------|\n * | `string` | `'file content'` |\n * | `object` / `array` | `{ name: 'cool' }` — auto-serialized as JSON for `.json` keys |\n * | `Buffer` | `Buffer.from([0x89, 0x50])` |\n * | Nested directory | `{ dir: { 'file.txt': 'content' } }` |\n * | Function | `({ importMeta, symlink }) => symlink('./target')` |\n */\nexport type FileTreeValue =\n\t| string\n\t| Buffer\n\t| Record<string, unknown>\n\t| unknown[]\n\t| FileTree\n\t| ((ctx: FileContext) => string | Buffer | SymlinkMarker);\n\n/** A recursive tree of files and directories. */\nexport interface FileTree {\n\t[key: string]: FileTreeValue;\n}\n\nfunction isSymlinkMarker(value: unknown): value is SymlinkMarker {\n\treturn typeof value === \"object\" && value !== null && SYMLINK in value;\n}\n\nfunction isFileTree(value: unknown): value is FileTree {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t!Buffer.isBuffer(value) &&\n\t\t!Array.isArray(value) &&\n\t\t!isSymlinkMarker(value)\n\t);\n}\n\nfunction scopeHfs(inner: NodeHfs, base: URL): ScopedHfsImpl {\n\tconst r = (p: string | URL) => new URL(`./${p}`, base);\n\tconst r2 = (a: string | URL, b: string | URL) => [r(a), r(b)] as const;\n\n\treturn {\n\t\ttext: (p: string | URL) => inner.text(r(p)),\n\t\tjson: (p: string | URL) => inner.json(r(p)),\n\t\tbytes: (p) => inner.bytes(r(p)),\n\t\twrite: (p, c) => inner.write(r(p), c),\n\t\tappend: (p, c) => inner.append(r(p), c),\n\t\tisFile: (p) => inner.isFile(r(p)),\n\t\tisDirectory: (p) => inner.isDirectory(r(p)),\n\t\tcreateDirectory: (p) => inner.createDirectory(r(p)),\n\t\tdelete: (p) => inner.delete(r(p)),\n\t\tdeleteAll: (p) => inner.deleteAll(r(p)),\n\t\tlist: (p) => inner.list(r(p)),\n\t\tsize: (p) => inner.size(r(p)),\n\t\tlastModified: (p) => inner.lastModified(r(p)),\n\t\tcopy: (s, d) => inner.copy(...r2(s, d)),\n\t\tcopyAll: (s, d) => inner.copyAll(...r2(s, d)),\n\t\tmove: (s, d) => inner.move(...r2(s, d)),\n\t\tmoveAll: (s, d) => inner.moveAll(...r2(s, d)),\n\t};\n}\n\n/**\n * Create a temporary fixture directory from an inline file tree.\n *\n * Returns a {@link Fixture} with all `hfs` methods scoped to the fixture root.\n *\n * @example\n * ```ts\n * const fixture = await createFixture({\n * 'hello.txt': 'hello world',\n * 'package.json': { name: 'test', version: '1.0.0' },\n * 'icon.png': Buffer.from([0x89, 0x50]),\n * src: {\n * 'index.ts': 'export default 1',\n * },\n * 'link.txt': ({ symlink }) => symlink('./hello.txt'),\n * 'info.txt': ({ importMeta }) => `Root: ${importMeta.url}`,\n * })\n *\n * const text = await fixture.text('hello.txt')\n * const json = await fixture.json('package.json')\n * ```\n */\nexport async function createFixture(files: FileTree): Promise<Fixture> {\n\tconst raw = expect.getState().currentTestName ?? \"bsh\";\n\tconst prefix = raw\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\");\n\tconst root = new URL(`${prefix}-`, `file://${tmpdir()}/`);\n\tconst path = await mkdtemp(fileURLToPath(root));\n\tconst base = pathToFileURL(path + sep);\n\n\tconst inner = new NodeHfs();\n\tconst scoped = scopeHfs(inner, base);\n\tconst resolve = (...segments: string[]) => new URL(`./${segments.join(\"/\")}`, base);\n\n\tconst ctx: FileContext = {\n\t\timportMeta: {\n\t\t\turl: base.toString(),\n\t\t\tfilename: fileURLToPath(base),\n\t\t\tdirname: fileURLToPath(base),\n\t\t\tresolve: (p: string) => new URL(`./${p}`, base).toString(),\n\t\t},\n\t\tsymlink: (target: string): SymlinkMarker => ({ [SYMLINK]: true, target }),\n\t};\n\n\tasync function writeTree(tree: FileTree, dir: URL): Promise<void> {\n\t\tfor (const [name, raw] of Object.entries(tree)) {\n\t\t\tconst url = new URL(name, dir);\n\n\t\t\t// Nested directory object (not a plain value)\n\t\t\tif (\n\t\t\t\ttypeof raw !== \"function\" &&\n\t\t\t\t!Buffer.isBuffer(raw) &&\n\t\t\t\t!Array.isArray(raw) &&\n\t\t\t\tisFileTree(raw) &&\n\t\t\t\t!name.includes(\".\")\n\t\t\t) {\n\t\t\t\tawait inner.createDirectory(url);\n\t\t\t\t// Trailing slash so nested entries resolve relative to the dir\n\t\t\t\tawait writeTree(raw, new URL(`${url}/`));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Ensure parent directory exists\n\t\t\tconst parent = new URL(\"./\", url);\n\t\t\tawait inner.createDirectory(parent);\n\n\t\t\t// Resolve functions\n\t\t\tconst content = typeof raw === \"function\" ? raw(ctx) : raw;\n\n\t\t\t// Symlink\n\t\t\tif (isSymlinkMarker(content)) {\n\t\t\t\tawait fsSymlink(content.target, url);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Buffer\n\t\t\tif (Buffer.isBuffer(content)) {\n\t\t\t\tawait inner.write(url, content);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// JSON auto-serialization for .json files with non-string content\n\t\t\tif (name.endsWith(\".json\") && typeof content !== \"string\") {\n\t\t\t\tawait inner.write(url, JSON.stringify(content, null, 2));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// String content\n\t\t\tawait inner.write(url, content as string);\n\t\t}\n\t}\n\n\tawait writeTree(files, base);\n\n\tconst cleanup = () => inner.deleteAll(path).then(() => undefined);\n\tonTestFinished(cleanup);\n\n\treturn {\n\t\troot: base,\n\t\tresolve,\n\t\tcleanup,\n\t\t...scoped,\n\t};\n}\n"],"mappings":"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"}