UNPKG

@bomb.sh/tools

Version:

The internal dev, build, and lint CLI for Bombshell projects

1 lines 10.6 kB
{"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 { 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\t/** Strings are UTF-8 encoded automatically. */\n\twrite(file: string | URL, contents: string | Uint8Array): Promise<void>;\n\t/** Strings are UTF-8 encoded automatically. */\n\tappend(file: string | URL, contents: string | Uint8Array): Promise<void>;\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\tconst encoder = new TextEncoder();\n\tconst bytes = (c: string | Uint8Array) => (typeof c === 'string' ? encoder.encode(c) : c);\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), bytes(c)),\n\t\tappend: (p, c) => inner.append(r(p), bytes(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 = new URL(`${pathToFileURL(path).href}/`);\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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"}