privatenumber/fs-fixture

Simple API to create test fixtures on disk

TypeScript

98

99 commits

updated Sep 3, 2026

See the code

README

fs-fixture

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

npm install fs-fixture

Quick start

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 for automatic cleanup:

await using fixture = await createFixture({
    'config.json': '{ "setting": true }'
})

// Fixture is automatically cleaned up when exiting scope

Already a sponsor? Join the discussion in the Development repo!

Usage

Creating fixtures

From an object:

const fixture = await createFixture({
    'package.json': '{ "name": "test" }',
    'src/index.js': 'export default () => {}',
    'src/utils': {
        'helper.js': 'export const help = () => {}'
    }
})

From a template directory:

// Copies an existing directory structure
const fixture = await createFixture('./test-templates/basic')

Empty fixture:

// Create an empty temporary directory
const fixture = await createFixture()

From an initializer:

const fixture = await createFixture(async ({ path, writeJson }) => {
    await writeJson('package.json', {
        name: 'test-package'
    })

    // Test-specific setup that needs the fixture path.
    await initializeProject(path)

    return {
        'src/index.js': 'export default 42'
    }
})

Prefer a FileTree when a fixture only needs files and directories. Use an initializer function when setup is complex, imperative, or ordered, such as initializing a Git repository or running a project setup helper. It keeps that setup with the fixture it configures.

The initializer receives the new fixture before it is returned. It can perform setup directly and optionally return a FileTree to create after setup completes. Returned files overwrite regular files created during setup, but the tree does not replace the fixture directory. If setup fails, fs-fixture removes the fixture before it rethrows the error.

Working with files

readFile and writeFile inherit their type signatures directly from Node.js fs/promises. readdir preserves Node.js overloads and also lists the fixture root when called without a path.

Read files:

// 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:

await fixture.writeFile('output.txt', 'Hello world')
await fixture.writeFile('data.bin', Buffer.from([0x89, 0x50]))

JSON operations:

// 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

// Create directories
await fixture.mkdir('nested/folders')

// List fixture root contents
const rootFiles = await fixture.readdir()

// List directory contents
const files = await fixture.readdir('src')

// List root entries with options
const rootEntries = await fixture.readdir('', { withFileTypes: true })

// 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')) {
    // ...
}

Call readdir() to list the fixture root. Pass '' as the path when listing the root with options.

Advanced features

Dynamic content with functions:

const fixture = await createFixture({
    'target.txt': 'original file',
    'info.txt': ({ fixturePath }) => `Created at: ${fixturePath}`,
    'link.txt': ({ symlink }) => symlink('./target.txt')
})

Use a FileTree entry function for isolated dynamic file content. Use an initializer function when setup requires multiple imperative or ordered operations.

Symlinks:

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:

const fixture = await createFixture({
    'image.png': Buffer.from(imageData),
    'generated.bin': () => Buffer.from('dynamic binary content')
})

Path syntax:

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:

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:

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, the future node:vfs, and 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, FileTree object defining the structure, or initializer function
  • 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>

const fixture = await createFixture()
const fixture = await createFixture({ 'file.txt': 'content' })
const fixture = await createFixture('./template-dir')
const fixture = await createFixture(fixture => ({ 'path.txt': fixture.path }))
const fixture = await createFixture({}, { tempDir: './custom-temp' })

FsFixture Methods

MethodDescription
fixture.pathAbsolute path to the fixture directory
fixture.fsThe 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(), readdir(path, options?)List fixture root or directory contents. Pass '' for root options.
mkdir(path)Create directory (recursive)
cp(source, dest?)Copy file/directory into fixture
mv(source, dest)Move or rename file/directory

Types

FileTree
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
}
FsPromises

FsPromises is the exported contract for custom filesystem implementations:

import type { FsPromises } from 'fs-fixture'
CapabilityMethodsRequirement
Core fixture operationsreadFile, writeFile, readdir, mkdir, rename, accessRequired
Removalrm, or unlink and rmdirOne removal strategy is required when calling fixture.rm()
SymlinkssymlinkRequired only when a FileTree contains a symlink
CopyingcpRequired only when calling fixture.cp()
Temporary directoriesmkdtempOptional. fs-fixture generates fixture paths with a counter when omitted.

The exported type defines the exact overloads and option shapes. fixture.readdir() is a fixture convenience method. A custom fs readdir always receives the fixture's absolute path.

manten

Lightweight testing library for Node.js

fixture
fs
json
object
template
test
utility

Contributors

privatenumber

85 commits

renovate[bot]

9 commits

danielbayley

4 commits

bluwy

1 commits

privatenumber/fs-fixture

Simple API to create test fixtures on disk

TypeScript

98

99 commits

updated Sep 3, 2026

See the code

README

fs-fixture

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

npm install fs-fixture

Quick start

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 for automatic cleanup:

await using fixture = await createFixture({
    'config.json': '{ "setting": true }'
})

// Fixture is automatically cleaned up when exiting scope

Already a sponsor? Join the discussion in the Development repo!

Usage

Creating fixtures

From an object:

const fixture = await createFixture({
    'package.json': '{ "name": "test" }',
    'src/index.js': 'export default () => {}',
    'src/utils': {
        'helper.js': 'export const help = () => {}'
    }
})

From a template directory:

// Copies an existing directory structure
const fixture = await createFixture('./test-templates/basic')

Empty fixture:

// Create an empty temporary directory
const fixture = await createFixture()

From an initializer:

const fixture = await createFixture(async ({ path, writeJson }) => {
    await writeJson('package.json', {
        name: 'test-package'
    })

    // Test-specific setup that needs the fixture path.
    await initializeProject(path)

    return {
        'src/index.js': 'export default 42'
    }
})

Prefer a FileTree when a fixture only needs files and directories. Use an initializer function when setup is complex, imperative, or ordered, such as initializing a Git repository or running a project setup helper. It keeps that setup with the fixture it configures.

The initializer receives the new fixture before it is returned. It can perform setup directly and optionally return a FileTree to create after setup completes. Returned files overwrite regular files created during setup, but the tree does not replace the fixture directory. If setup fails, fs-fixture removes the fixture before it rethrows the error.

Working with files

readFile and writeFile inherit their type signatures directly from Node.js fs/promises. readdir preserves Node.js overloads and also lists the fixture root when called without a path.

Read files:

// 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:

await fixture.writeFile('output.txt', 'Hello world')
await fixture.writeFile('data.bin', Buffer.from([0x89, 0x50]))

JSON operations:

// 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

// Create directories
await fixture.mkdir('nested/folders')

// List fixture root contents
const rootFiles = await fixture.readdir()

// List directory contents
const files = await fixture.readdir('src')

// List root entries with options
const rootEntries = await fixture.readdir('', { withFileTypes: true })

// 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')) {
    // ...
}

Call readdir() to list the fixture root. Pass '' as the path when listing the root with options.

Advanced features

Dynamic content with functions:

const fixture = await createFixture({
    'target.txt': 'original file',
    'info.txt': ({ fixturePath }) => `Created at: ${fixturePath}`,
    'link.txt': ({ symlink }) => symlink('./target.txt')
})

Use a FileTree entry function for isolated dynamic file content. Use an initializer function when setup requires multiple imperative or ordered operations.

Symlinks:

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:

const fixture = await createFixture({
    'image.png': Buffer.from(imageData),
    'generated.bin': () => Buffer.from('dynamic binary content')
})

Path syntax:

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:

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:

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, the future node:vfs, and 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, FileTree object defining the structure, or initializer function
  • 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>

const fixture = await createFixture()
const fixture = await createFixture({ 'file.txt': 'content' })
const fixture = await createFixture('./template-dir')
const fixture = await createFixture(fixture => ({ 'path.txt': fixture.path }))
const fixture = await createFixture({}, { tempDir: './custom-temp' })

FsFixture Methods

MethodDescription
fixture.pathAbsolute path to the fixture directory
fixture.fsThe 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(), readdir(path, options?)List fixture root or directory contents. Pass '' for root options.
mkdir(path)Create directory (recursive)
cp(source, dest?)Copy file/directory into fixture
mv(source, dest)Move or rename file/directory

Types

FileTree
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
}
FsPromises

FsPromises is the exported contract for custom filesystem implementations:

import type { FsPromises } from 'fs-fixture'
CapabilityMethodsRequirement
Core fixture operationsreadFile, writeFile, readdir, mkdir, rename, accessRequired
Removalrm, or unlink and rmdirOne removal strategy is required when calling fixture.rm()
SymlinkssymlinkRequired only when a FileTree contains a symlink
CopyingcpRequired only when calling fixture.cp()
Temporary directoriesmkdtempOptional. fs-fixture generates fixture paths with a counter when omitted.

The exported type defines the exact overloads and option shapes. fixture.readdir() is a fixture convenience method. A custom fs readdir always receives the fixture's absolute path.

manten

Lightweight testing library for Node.js

fixture
fs
json
object
template
test
utility

Contributors

privatenumber

85 commits

renovate[bot]

9 commits

danielbayley

4 commits

bluwy

1 commits

Languages

TypeScript

100.0%