Skip to content

Commit

Permalink
feat(file): withJsonFile
Browse files Browse the repository at this point in the history
  • Loading branch information
seb-cr committed May 10, 2023
1 parent 68a90e9 commit 1db6e15
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 2 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ await withFile('example.txt', (f) => {

For all available methods see the [`Text`](src/text.ts) class.

Work with YAML files using the `withYamlFile` function. Like `withText`, it passes the file's content to a callback for editing. In this case the file is parsed into a YAML `Document` using the [`yaml`](https://www.npmjs.com/package/yaml) package.
Work with JSON files using the `withJsonFile` function. Like `withText`, it passes the file's content to a callback for editing. In this case the text content is parsed as JSON, and the resulting plain object (or primitive, if that's what the JSON represents) is passed to the callback.

```ts
await withJsonFile('example.json', (f) => {
f.foo.bar.baz = 42;
});
```

Work with YAML files using the `withYamlFile` function. The file is parsed into a YAML `Document` using the [`yaml`](https://www.npmjs.com/package/yaml) package.

```ts
await withYamlFile('example.yaml', (f) => {
Expand All @@ -77,3 +85,5 @@ await withYamlFile('example.yaml', (f) => {
```

See [the YAML package docs](https://eemeli.org/yaml/#documents) for documentation of `Document`.

Both `withJsonFile` and `withYamlFile` will make some effort to preserve the original indentation of the file (and comments in YAML).
29 changes: 29 additions & 0 deletions src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,35 @@ export async function withFile(
}
}

/**
* Work with a JSON file.
*
* Opens the file, parses its content and passes to `callback`, then
* reserialises back to the file.
*
* An attempt will be made to preserve indentation, based on the first indented line
* found. This should be adequate for well-formatted documents.
*
* @param path File path.
* @param callback Function that does something with the JSON document.
*/
export async function withJsonFile<T = any>(
path: string,
callback: (f: T) => void | Promise<void>,
): Promise<void> {
const rawContent = (await readFile(path)).toString();
const doc = JSON.parse(rawContent);

await callback(doc);

// set indentation based on the first indented line
const indent = /^(\s+)/m.exec(rawContent)?.[1] || 0;
const newContent = JSON.stringify(doc, null, indent);
if (newContent !== rawContent) {
await writeFile(path, newContent);
}
}

/**
* Work with a YAML file.
*
Expand Down
55 changes: 54 additions & 1 deletion tests/file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { readFile, unlink, writeFile } from 'fs/promises';

import { expect } from 'chai';

import { exists, withFile, withYamlFile } from '@/src';
import {
exists,
withFile,
withJsonFile,
withYamlFile,
} from '@/src';

describe('exists', () => {
it('should return true if the path is a file', async () => {
Expand Down Expand Up @@ -50,6 +55,54 @@ describe('withFile', () => {
});
});

describe('withJsonFile', () => {
const TEST_FILE = 'test.json';
const TEST_DATA = { foo: { bar: { baz: 'bing' } } };
const EXPECTED_DATA = { foo: { bar: { baz: 'wiggle' } } };

before('create test file', async () => {
await writeFile(TEST_FILE, JSON.stringify(TEST_DATA));
});

after('delete test file', async () => {
await unlink(TEST_FILE);
});

it('should pass file content to callback', async () => {
let wasCalled = false;

await withJsonFile(TEST_FILE, (f) => {
expect(f).to.deep.equal(TEST_DATA);
wasCalled = true;
});

expect(wasCalled, 'callback was not called').to.be.true;
});

it('should write back any changes made', async () => {
await withJsonFile(TEST_FILE, (f) => {
f.foo.bar.baz = 'wiggle';
});

const newContent = (await readFile(TEST_FILE)).toString();
const doc = JSON.parse(newContent);
expect(doc).to.deep.equal(EXPECTED_DATA);
});

[0, 2, 4, '\t'].forEach((indentation) => {
it(`should preserve indentation (${JSON.stringify(indentation)})`, async () => {
await writeFile(TEST_FILE, JSON.stringify(TEST_DATA, null, indentation));

await withJsonFile(TEST_FILE, (f) => {
f.foo.bar.baz = 'wiggle';
});

const newContent = (await readFile(TEST_FILE)).toString();
expect(newContent).to.equal(JSON.stringify(EXPECTED_DATA, null, indentation));
});
});
});

describe('withYamlFile', () => {
const TEST_FILE = 'test.yaml';

Expand Down

0 comments on commit 1db6e15

Please sign in to comment.