Skip to content

A helper to recursively read and write files to a specified directory

License

Notifications You must be signed in to change notification settings

mmkal/fs-syncer

Repository files navigation

fs-syncer

A helper to recursively read and write text files to a specified directory.

CI npm version

The idea

It's a pain to write tests for tools that interact with the filesystem. It would be useful to write assertions that look something like:

expect(someDirectory.read()).toEqual({
  'file1.txt': 'some info',
  'file2.log': 'something logged',
  nested: {
    sub: {
      directory: {
        'deeply-nested-file.sql': 'SELECT * FROM abc'
      },
    },
  },
})

Similarly, as part of test setup, you might want to write several files, e.g.:

write({
  migrations: {
    'migration1.sql': 'create table one(id text)',
    'migration2.sql': 'create table two(id text)',
    down: {
      'migration1.sql': 'drop table one',
      'migration2.sql': 'drop table two',
    },
  },
})

The problem is that usually, you have to write a recursive directory-walker function, an object-to-filepath converter function, a nested-object-getter-function and a few more functions that tie them all together.

Then, if you have the energy, you should also write a function that cleans up any extraneous files after tests have been run. Or, you can pull in several dependencies that do some of these things for you, then write some functions that tie them together.

Now, you can just use fs-syncer, which does all of the above. Here's the API:

import {fsSyncer} from 'fs-syncer'

const syncer = fsSyncer(__dirname + '/migrations', {
  'migration1.sql': 'create table one(id text)',
  'migration2.sql': 'create table two(id text)',
  down: {
    'migration1.sql': 'drop table one',
    'migration2.sql': 'drop table two',
  },
})

syncer.sync() // replaces all content in `./migrations` with what's described in the target state

syncer.read() // returns the filesystem state as an object, in the same format as the target state

// write a file that's not in part of the target state
require('fs').writeFileSync(__dirname + '/migrations/extraneous.txt', 'abc', 'utf8')

syncer.read() // includes `extraneous.txt: 'abc'`

syncer.sync() // 'extraneous.txt' will now have been removed

syncer.write() // like `syncer.sync()`, but doesn't remove extraneous files

Usage with vitest or jest

⚠️⚠️⚠️ This feature is new and experimental - if you try it out, be aware that the API is in flux. Feedback is welcome! ⚠️⚠️⚠️

If you happen to want to use this in a jest test, there's an opinionated helper which allows you to avoid supplying a baseDir parameter.

Let's say you want to test a file modification tool, which appends // comments to all the files it finds under a certain directory, and also creates a log file:

import {testFixture} from 'fs-syncer'

import {fileModificationToolThatYouWantToTest} from '../src/your-library'

test('files are modified', async () => {
  const fixture = testFixture({
    expect,
    targetState: {
      'file1.txt': 'hello I am a file',
      nested: {
        'file2.txt': 'I am also a file',
      }
    }
  })

  fixture.sync()

  await fileModificationToolThatYouWantToTest.run({
    directory: fixture.baseDir,
    logFile: 'abc.log',
  })

  expect(fixture.yaml()).toMatchInlineSnapshot()
})

fixture.yaml() is a helper that returns a yaml string representing the file tree. It's intended to be human-readable and familiar, and should not be relied on to be valid yaml, it's mostly for test snapshots.

Let's assume the test file containing this test is called my-test-file.test.ts. When run, the above test will generate a directory fixtures/my-test-file.test.ts/files-are-modified next to the test file. The directory structure described in targetState will be created inside that folder. The test above might end up looking something like when run:

import {testFixture} from 'fs-syncer'

import {fileModificationToolThatYouWantToTest} from '../src/your-library'

test('files are modified', async () => {
  const fixture = testFixture({
    expect,
    targetState: {
      'file1.txt': 'hello I am a file',
      nested: {
        'file2.txt': 'I am also a file',
      }
    }
  })

  fixture.sync()

  await fileModificationToolThatYouWantToTest.run({
    directory: fixture.baseDir,
    logFile: 'abc.log',
  })

  expect(fixture.yaml()).toMatchInlineSnapshot(
    `"---
    abc.log: |-
      added content to file1.txt
      added content to nested/file2.txt
    file1.txt: |-
      hello I am a file

      // this content was auto-generated by the tool
    nested:
      file2.txt: |-
        hello I am a file

        // this content was auto-generated by the tool
    "`
  )
})

Not supported (right now)

  • File content other than text, e.g. Buffers. The library assumes you are solely dealing with utf8 strings.
  • Any performance optimisations - you will probably have a bad time if you try to use it to read or write a very large number of files.
  • Any custom symlink behaviour.

Comparison with mock-fs

This isn't a mocking library. There's no magic under the hood, it just calls fs.readFileSync, fs.writeFileSync and fs.mkdirSync directly. Which means you can use it anywhere - it could even be a runtime dependency as a wrapper for the fs module. And using it doesn't have any weird side-effects like breaking jest snapshot testing. Not being a mocking library means you could use it in combination with mock-fs, if you really wanted.

About

A helper to recursively read and write files to a specified directory

Resources

License

Stars

Watchers

Forks

Packages

No packages published