fs-fixture
Version:
Easily create test fixtures at a temporary file-system path
323 lines (253 loc) โข 10 kB
Markdown
<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