UNPKG

fs-fixture

Version:

Easily create test fixtures at a temporary file-system path

323 lines (253 loc) โ€ข 10 kB
<p align="center"> <img width="160" src=".github/logo.webp"> </p> <h1 align="center"> <sup>fs-fixture</sup> <br> <a href="https://npm.im/fs-fixture"><img src="https://badgen.net/npm/v/fs-fixture"></a> <a href="https://npm.im/fs-fixture"><img src="https://badgen.net/npm/dm/fs-fixture"></a> </h1> Simple API to create disposable test fixtures on disk. Tiny (`1.1 kB` gzipped) with zero dependencies! ### Features - ๐Ÿ“ Create files & directories from simple objects - ๐Ÿงน Automatic cleanup with `using` keyword - ๐Ÿ“ Built-in JSON read/write support - ๐Ÿ”— Symlink support - ๐Ÿ’พ Binary file support with Buffers - ๐ŸŽฏ TypeScript-first with full type safety - ๐Ÿ”„ File methods inherit types directly from Node.js `fs` module - ๐Ÿ”Œ Pluggable filesystem โ€” use with @platformatic/vfs, memfs, or any `fs/promises`-compatible API ## Installation ```sh npm install fs-fixture ``` ## Quick start ```ts import { createFixture } from 'fs-fixture' // Create a temporary fixture const fixture = await createFixture({ 'package.json': JSON.stringify({ name: 'my-app' }), 'src/index.js': 'console.log("Hello world")' }) // Read files const content = await fixture.readFile('src/index.js', 'utf8') // Cleanup when done await fixture.rm() ``` ### Auto cleanup with `using` keyword Uses TypeScript 5.2+ [Explicit Resource Management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html) for automatic cleanup: ```ts await using fixture = await createFixture({ 'config.json': '{ "setting": true }' }) // Fixture is automatically cleaned up when exiting scope ``` <p align="center"> <a href="https://github.com/sponsors/privatenumber/sponsorships?tier_id=398771"><img width="412" src="https://raw.githubusercontent.com/privatenumber/sponsors/master/banners/assets/donate.webp"></a> <a href="https://github.com/sponsors/privatenumber/sponsorships?tier_id=397608"><img width="412" src="https://raw.githubusercontent.com/privatenumber/sponsors/master/banners/assets/sponsor.webp"></a> </p> <p align="center"><sup><i>Already a sponsor?</i> Join the discussion in the <a href="https://github.com/pvtnbr/fs-fixture">Development repo</a>!</sup></p> ## Usage ### Creating fixtures **From an object:** ```ts const fixture = await createFixture({ 'package.json': '{ "name": "test" }', 'src/index.js': 'export default () => {}', 'src/utils': { 'helper.js': 'export const help = () => {}' } }) ``` **From a template directory:** ```ts // Copies an existing directory structure const fixture = await createFixture('./test-templates/basic') ``` **Empty fixture:** ```ts // Create an empty temporary directory const fixture = await createFixture() ``` ### Working with files File methods (`readFile`, `writeFile`, `readdir`) inherit their type signatures directly from Node.js `fs/promises`, preserving all overloads and type narrowing behavior. **Read files:** ```ts // Read as string (type: Promise<string>) const text = await fixture.readFile('config.txt', 'utf8') // Read as buffer (type: Promise<Buffer>) const binary = await fixture.readFile('image.png') ``` **Write files:** ```ts await fixture.writeFile('output.txt', 'Hello world') await fixture.writeFile('data.bin', Buffer.from([0x89, 0x50])) ``` **JSON operations:** ```ts // Write JSON with formatting await fixture.writeJson('config.json', { port: 3000 }) // Read and parse JSON with type safety type Config = { port: number } const config = await fixture.readJson<Config>('config.json') ``` ### Working with directories ```ts // Create directories await fixture.mkdir('nested/folders') // List directory contents const files = await fixture.readdir('src') // Copy files into fixture await fixture.cp('/path/to/file.txt', 'copied-file.txt') // Move or rename files await fixture.mv('old-name.txt', 'new-name.txt') await fixture.mv('file.txt', 'src/file.txt') // Check if path exists if (await fixture.exists('optional-file.txt')) { // ... } ``` ### Advanced features **Dynamic content with functions:** ```ts const fixture = await createFixture({ 'target.txt': 'original file', 'info.txt': ({ fixturePath }) => `Created at: ${fixturePath}`, 'link.txt': ({ symlink }) => symlink('./target.txt') }) ``` **Symlinks:** ```ts const fixture = await createFixture({ 'index.js': 'import pkg from \'pkg\'', // Symlink individual file or directory 'node_modules/pkg': ({ symlink }) => symlink(process.cwd()), // Symlink entire directory (useful for sharing node_modules) node_modules: ({ symlink }) => symlink(path.resolve('node_modules')) }) ``` **Binary files:** ```ts const fixture = await createFixture({ 'image.png': Buffer.from(imageData), 'generated.bin': () => Buffer.from('dynamic binary content') }) ``` **Path syntax:** ```ts const fixture = await createFixture({ // Nested object syntax src: { utils: { 'helper.js': 'export const help = () => {}' } }, // Or path syntax (creates same structure) 'src/utils/helper.js': 'export const help = () => {}' }) ``` > [!TIP] > Path syntax also works for grouped prefixes, so you can keep related files together without repeating the shared path: > > ```ts > await createFixture({ > 'file.js': 'import { a } from "my-pkg";', > > 'node_modules/my-pkg': { > 'package.json': JSON.stringify({ > name: 'my-pkg', > type: 'module', > exports: './index.js' > }), > 'index.js': 'export const a = 1;' > } > }) > ``` ### Custom filesystem Pass any `fs/promises`-compatible API via the `fs` option to use a virtual filesystem instead of disk: ```ts import { create, MemoryProvider } from '@platformatic/vfs' import { createFixture } from 'fs-fixture' const fs = create(new MemoryProvider()).promises const fixture = await createFixture({ 'package.json': JSON.stringify({ name: 'test' }), 'src/index.js': 'export default 42' }, { fs }) await fixture.readFile('src/index.js', 'utf8') // 'export default 42' ``` Works with any library that implements the `fs/promises` API shape, including [@platformatic/vfs](https://github.com/platformatic/vfs), the future [`node:vfs`](https://github.com/nodejs/node/pull/61478), and [memfs](https://github.com/streamich/memfs). > [!NOTE] > With a custom fs, files only exist in that fs instance. Use `fixture.readFile()` or `fixture.fs` to access them โ€” `fixture.path` is a virtual path that doesn't exist on the real disk. > [!NOTE] > Template directory sources (string paths) are not supported with custom filesystems because most virtual fs implementations lack recursive `cp`. Use a `FileTree` object instead. ## API ### `createFixture(source?, options?)` Creates a temporary fixture directory and returns a `FsFixture` instance. **Parameters:** - `source` (optional): String path to template directory, or `FileTree` object defining the structure - `options.tempDir` (optional): Custom temp directory. Defaults to `os.tmpdir()` - `options.templateFilter` (optional): Filter function when copying from template directory - `options.fs` (optional): Custom `fs/promises`-compatible API for virtual filesystem support **Returns:** `Promise<FsFixture>` ```ts const fixture = await createFixture() const fixture = await createFixture({ 'file.txt': 'content' }) const fixture = await createFixture('./template-dir') const fixture = await createFixture({}, { tempDir: './custom-temp' }) ``` ### `FsFixture` Methods | Method | Description | |--------|-------------| | `fixture.path` | Absolute path to the fixture directory | | `fixture.fs` | The underlying `fs/promises` API used by the fixture | | `getPath(...paths)` | Get absolute path to file/directory in fixture | | `exists(path?)` | Check if file/directory exists | | `rm(path?)` | Delete file/directory (or entire fixture if no path) | | `readFile(path, encoding?)` | Read file as string or Buffer | | `writeFile(path, content)` | Write string or Buffer to file | | `readJson<T>(path)` | Read and parse JSON file | | `writeJson(path, data, space?)` | Write JSON with optional formatting | | `readdir(path, options?)` | List directory contents | | `mkdir(path)` | Create directory (recursive) | | `cp(source, dest?)` | Copy file/directory into fixture | | `mv(source, dest)` | Move or rename file/directory | ### Types <details> <summary><strong>FileTree</strong></summary> ```ts type FileTree = { [path: string]: string | Buffer | FileTree | ((api: Api) => string | Buffer | Symlink) } type Api = { fixturePath: string // Fixture root path filePath: string // Current file path getPath: (...paths: string[]) => string // Get path from fixture root symlink: (target: string) => Symlink // Create a symlink } ``` </details> <details> <summary><strong>FsPromises</strong></summary> The subset of `fs/promises` methods that custom filesystem implementations must provide: ```ts type FsPromises = { // Required readFile(path: string, options?): Promise<Buffer | string> writeFile(path: string, data: string | Buffer, options?): Promise<void> readdir(path: string, options?): Promise<string[] | Dirent[]> mkdir(path: string, options?): Promise<string | undefined> rename(oldPath: string, newPath: string): Promise<void> access(path: string, mode?: number): Promise<void> // Optional rm?(path: string, options?): Promise<void> unlink?(path: string): Promise<void> rmdir?(path: string): Promise<void> symlink?(target: string, path: string, type?: string): Promise<void> cp?(source: string, destination: string, options?): Promise<void> mkdtemp?(prefix: string): Promise<string> } ``` If `rm` is not available, fs-fixture falls back to recursive removal using `readdir({ withFileTypes })` + `unlink` + `rmdir`. If `mkdtemp` is not available, fixture paths are generated with a counter. </details> ## Related ### [manten](https://github.com/privatenumber/manten) Lightweight testing library for Node.js