├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Replacer = (this: unknown, key: string, value: unknown) => unknown; 2 | export type SortKeys = (a: string, b: string) => number; 3 | 4 | export type Options = { 5 | /** 6 | Indentation as a string or number of spaces. 7 | 8 | Pass in `undefined` for no formatting. 9 | 10 | If you set both this and `detectIndent`, this value will be used when the indentation cannot be detected. 11 | 12 | @default '\t' 13 | */ 14 | readonly indent?: string | number | undefined; 15 | 16 | /** 17 | Detect indentation automatically if the file exists. 18 | 19 | @default false 20 | */ 21 | readonly detectIndent?: boolean; 22 | 23 | /** 24 | Sort the keys recursively. 25 | 26 | Optionally pass in a compare function. 27 | 28 | @default false 29 | */ 30 | readonly sortKeys?: boolean | SortKeys; 31 | 32 | /** 33 | Passed into `JSON.stringify`. 34 | */ 35 | readonly replacer?: Replacer | ReadonlyArray; 36 | 37 | /** 38 | The mode used when writing the file. 39 | 40 | @default 0o666 41 | */ 42 | readonly mode?: number; 43 | }; 44 | 45 | /** 46 | Stringify and write JSON to a file atomically. 47 | 48 | Creates directories for you as needed. 49 | 50 | @example 51 | ``` 52 | import {writeJsonFile} from 'write-json-file'; 53 | 54 | await writeJsonFile('foo.json', {foo: true}); 55 | ``` 56 | */ 57 | export function writeJsonFile( 58 | filePath: string, 59 | data: unknown, 60 | options?: Options 61 | ): Promise; 62 | 63 | /** 64 | Stringify and write JSON to a file atomically. 65 | 66 | Creates directories for you as needed. 67 | 68 | @example 69 | ``` 70 | import {writeJsonFileSync} from 'write-json-file'; 71 | 72 | writeJsonFileSync('foo.json', {foo: true}); 73 | ``` 74 | */ 75 | export function writeJsonFileSync( 76 | filePath: string, 77 | data: unknown, 78 | options?: Options 79 | ): void; 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs, {promises as fsPromises} from 'node:fs'; 3 | import writeFileAtomic from 'write-file-atomic'; 4 | import sortKeys from 'sort-keys'; 5 | import detectIndent from 'detect-indent'; 6 | import isPlainObj from 'is-plain-obj'; 7 | 8 | const init = (function_, filePath, data, options) => { 9 | if (!filePath) { 10 | throw new TypeError('Expected a filepath'); 11 | } 12 | 13 | if (data === undefined) { 14 | throw new TypeError('Expected data to stringify'); 15 | } 16 | 17 | options = { 18 | indent: '\t', 19 | sortKeys: false, 20 | ...options, 21 | }; 22 | 23 | if (options.sortKeys && isPlainObj(data)) { 24 | data = sortKeys(data, { 25 | deep: true, 26 | compare: typeof options.sortKeys === 'function' ? options.sortKeys : undefined, 27 | }); 28 | } 29 | 30 | return function_(filePath, data, options); 31 | }; 32 | 33 | const main = async (filePath, data, options) => { 34 | let {indent} = options; 35 | let trailingNewline = '\n'; 36 | try { 37 | const file = await fsPromises.readFile(filePath, 'utf8'); 38 | if (!file.endsWith('\n')) { 39 | trailingNewline = ''; 40 | } 41 | 42 | if (options.detectIndent) { 43 | indent = detectIndent(file).indent; 44 | } 45 | } catch (error) { 46 | if (error.code !== 'ENOENT') { 47 | throw error; 48 | } 49 | } 50 | 51 | const json = JSON.stringify(data, options.replacer, indent); 52 | 53 | return writeFileAtomic(filePath, `${json}${trailingNewline}`, {mode: options.mode, chown: false}); 54 | }; 55 | 56 | const mainSync = (filePath, data, options) => { 57 | let {indent} = options; 58 | let trailingNewline = '\n'; 59 | try { 60 | const file = fs.readFileSync(filePath, 'utf8'); 61 | if (!file.endsWith('\n')) { 62 | trailingNewline = ''; 63 | } 64 | 65 | if (options.detectIndent) { 66 | indent = detectIndent(file).indent; 67 | } 68 | } catch (error) { 69 | if (error.code !== 'ENOENT') { 70 | throw error; 71 | } 72 | } 73 | 74 | const json = JSON.stringify(data, options.replacer, indent); 75 | 76 | return writeFileAtomic.sync(filePath, `${json}${trailingNewline}`, {mode: options.mode, chown: false}); 77 | }; 78 | 79 | export async function writeJsonFile(filePath, data, options) { 80 | await fsPromises.mkdir(path.dirname(filePath), {recursive: true}); 81 | await init(main, filePath, data, options); 82 | } 83 | 84 | export function writeJsonFileSync(filePath, data, options) { 85 | fs.mkdirSync(path.dirname(filePath), {recursive: true}); 86 | init(mainSync, filePath, data, options); 87 | } 88 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType, expectAssignable} from 'tsd'; 2 | import { 3 | writeJsonFile, 4 | writeJsonFileSync, 5 | type Replacer, 6 | type SortKeys, 7 | } from './index.js'; 8 | 9 | expectAssignable(() => 1); 10 | expectAssignable((a: string) => a.length); 11 | expectAssignable((a: string, b: string) => a.length - b.length); 12 | 13 | expectAssignable(() => 1); 14 | expectAssignable(() => 'unicorn'); 15 | expectAssignable(() => true); 16 | expectAssignable(() => null); 17 | expectAssignable(() => undefined); 18 | expectAssignable(() => ({unicorn: '🦄'})); 19 | expectAssignable(() => ['unicorn', 1]); 20 | expectAssignable(() => () => 'foo'); 21 | expectAssignable((key: string) => key.toUpperCase()); 22 | expectAssignable((key: string, value: unknown) => (key + (value as string)).toUpperCase()); 23 | 24 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'})); 25 | expectType>(writeJsonFile('unicorn.json', '🦄')); 26 | expectType>(writeJsonFile('date.json', new Date())); 27 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {detectIndent: true})); 28 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {indent: ' '})); 29 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {indent: 4})); 30 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {mode: 0o666})); 31 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {replacer: ['unicorn', 1]})); 32 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {replacer: () => 'unicorn'})); 33 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {sortKeys: () => -1})); 34 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {sortKeys: (a: string, b: string) => a.length - b.length})); 35 | expectType>(writeJsonFile('unicorn.json', {unicorn: '🦄'}, {sortKeys: true})); 36 | 37 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'})); 38 | expectType(writeJsonFileSync('unicorn.json', '🦄')); 39 | expectType(writeJsonFileSync('date.json', new Date())); 40 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {detectIndent: true})); 41 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {indent: ' '})); 42 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {indent: 4})); 43 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {mode: 0o666})); 44 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {replacer: ['unicorn', 1]})); 45 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {replacer: () => 'unicorn'})); 46 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {sortKeys: () => -1})); 47 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {sortKeys: (a: string, b: string) => a.length - b.length})); 48 | expectType(writeJsonFileSync('unicorn.json', {unicorn: '🦄'}, {sortKeys: true})); 49 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "write-json-file", 3 | "version": "6.0.0", 4 | "description": "Stringify and write JSON to a file atomically", 5 | "license": "MIT", 6 | "repository": "sindresorhus/write-json-file", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "write", 31 | "json", 32 | "stringify", 33 | "file", 34 | "fs", 35 | "graceful", 36 | "stable", 37 | "sort", 38 | "newline", 39 | "indent", 40 | "atomic", 41 | "atomically" 42 | ], 43 | "dependencies": { 44 | "detect-indent": "^7.0.1", 45 | "is-plain-obj": "^4.1.0", 46 | "sort-keys": "^5.0.0", 47 | "write-file-atomic": "^5.0.1" 48 | }, 49 | "devDependencies": { 50 | "ava": "^6.1.3", 51 | "tempy": "^2.0.0", 52 | "tsd": "^0.31.1", 53 | "xo": "^0.59.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # write-json-file 2 | 3 | > Stringify and write JSON to a file [atomically](https://github.com/npm/write-file-atomic) 4 | 5 | Creates directories for you as needed. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install write-json-file 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import {writeJsonFile} from 'write-json-file'; 17 | 18 | await writeJsonFile('foo.json', {foo: true}); 19 | ``` 20 | 21 | ## API 22 | 23 | ### writeJsonFile(filePath, data, options?) 24 | 25 | Returns a `Promise`. 26 | 27 | ### writeJsonFileSync(filePath, data, options?) 28 | 29 | #### options 30 | 31 | Type: `object` 32 | 33 | ##### indent 34 | 35 | Type: `string | number | undefined`\ 36 | Default: `'\t'` 37 | 38 | Indentation as a string or number of spaces. 39 | 40 | Pass in `undefined` for no formatting. 41 | 42 | If you set both this and `detectIndent`, this value will be used when the indentation cannot be detected. 43 | 44 | ##### detectIndent 45 | 46 | Type: `boolean`\ 47 | Default: `false` 48 | 49 | Detect indentation automatically if the file exists. 50 | 51 | ##### sortKeys 52 | 53 | Type: `boolean | Function`\ 54 | Default: `false` 55 | 56 | Sort the keys recursively. 57 | 58 | Optionally pass in a [`compare`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) function. 59 | 60 | ##### replacer 61 | 62 | Type: `Function` 63 | 64 | Passed into [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). 65 | 66 | ##### mode 67 | 68 | Type: `number`\ 69 | Default: `0o666` 70 | 71 | The [mode](https://en.wikipedia.org/wiki/File_system_permissions#Numeric_notation) used when writing the file. 72 | 73 | ## Related 74 | 75 | - [load-json-file](https://github.com/sindresorhus/load-json-file) - Read and parse a JSON file 76 | - [make-dir](https://github.com/sindresorhus/make-dir) - Make a directory and its parents if needed 77 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import test from 'ava'; 3 | import tempy from 'tempy'; 4 | import {writeJsonFile, writeJsonFileSync} from './index.js'; 5 | 6 | test('async', async t => { 7 | const temporaryFile = tempy.file(); 8 | await writeJsonFile(temporaryFile, {foo: true}, {indent: 2}); 9 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true\n}\n'); 10 | }); 11 | 12 | test('sync', t => { 13 | const temporaryFile = tempy.file(); 14 | writeJsonFileSync(temporaryFile, {foo: true}, {detectIndent: true, indent: 2}); 15 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true\n}\n'); 16 | }); 17 | 18 | test('detect indent', async t => { 19 | const temporaryFile = tempy.file(); 20 | await writeJsonFile(temporaryFile, {foo: true}, {indent: 2}); 21 | await writeJsonFile(temporaryFile, {foo: true, bar: true, foobar: true}, {detectIndent: true}); 22 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n'); 23 | }); 24 | 25 | test('detect indent synchronously', t => { 26 | const temporaryFile = tempy.file(); 27 | writeJsonFileSync(temporaryFile, {foo: true}, {indent: 2}); 28 | writeJsonFileSync(temporaryFile, {foo: true, bar: true, foobar: true}, {detectIndent: true}); 29 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n'); 30 | }); 31 | 32 | test('fall back to default indent if file doesn\'t exist', async t => { 33 | const temporaryFile = tempy.file(); 34 | await writeJsonFile(temporaryFile, {foo: true, bar: true, foobar: true}, {detectIndent: true}); 35 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"foo": true,\n\t"bar": true,\n\t"foobar": true\n}\n'); 36 | }); 37 | 38 | test('async - {sortKeys: true}', async t => { 39 | const temporaryFile = tempy.file(); 40 | await writeJsonFile(temporaryFile, {c: true, b: true, a: true}, {sortKeys: true}); 41 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"a": true,\n\t"b": true,\n\t"c": true\n}\n'); 42 | 43 | await writeJsonFile(temporaryFile, ['c', 'b', 'a'], {sortKeys: true}); 44 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '[\n\t"c",\n\t"b",\n\t"a"\n]\n'); 45 | }); 46 | 47 | test('async - {sortKeys: false}', async t => { 48 | const temporaryFile = tempy.file(); 49 | await writeJsonFile(temporaryFile, {c: true, b: true, a: true}, {sortKeys: false}); 50 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"c": true,\n\t"b": true,\n\t"a": true\n}\n'); 51 | }); 52 | 53 | test('async - `replacer` option', async t => { 54 | const temporaryFile = tempy.file(); 55 | await writeJsonFile(temporaryFile, {foo: true, bar: true}, {replacer: ['foo']}); 56 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"foo": true\n}\n'); 57 | }); 58 | 59 | test('sync - `replacer` option', t => { 60 | const temporaryFile = tempy.file(); 61 | writeJsonFileSync(temporaryFile, {foo: true, bar: true}, {replacer: ['foo']}); 62 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"foo": true\n}\n'); 63 | }); 64 | 65 | test('async - respect trailing newline at the end of the file', async t => { 66 | const temporaryFile = tempy.file(); 67 | fs.writeFileSync(temporaryFile, JSON.stringify({foo: true})); 68 | await writeJsonFile(temporaryFile, {bar: true}); 69 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"bar": true\n}'); 70 | }); 71 | 72 | test('sync - respect trailing newline at the end of the file', t => { 73 | const temporaryFile = tempy.file(); 74 | fs.writeFileSync(temporaryFile, JSON.stringify({foo: true})); 75 | writeJsonFileSync(temporaryFile, {bar: true}); 76 | t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"bar": true\n}'); 77 | }); 78 | --------------------------------------------------------------------------------