Skip to content

Commit be59d9a

Browse files
committed
Require Node.js 20
1 parent 301b078 commit be59d9a

File tree

3 files changed

+176
-65
lines changed

3 files changed

+176
-65
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
node-version:
13-
- 22
13+
- 24
1414
- 20
15-
- 18
1615
steps:
17-
- uses: actions/checkout@v4
18-
- uses: actions/setup-node@v4
16+
- uses: actions/checkout@v5
17+
- uses: actions/setup-node@v5
1918
with:
2019
node-version: ${{ matrix.node-version }}
2120
- run: npm install

package.json

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
},
1818
"sideEffects": false,
1919
"engines": {
20-
"node": ">=18"
20+
"node": ">=20"
2121
},
2222
"scripts": {
23-
"test": "xo && ava && tsd"
23+
"test": "xo && node test.js && tsd"
2424
},
2525
"files": [
2626
"index.js",
@@ -43,13 +43,11 @@
4343
"dependencies": {
4444
"detect-indent": "^7.0.1",
4545
"is-plain-obj": "^4.1.0",
46-
"sort-keys": "^5.0.0",
47-
"write-file-atomic": "^5.0.1"
46+
"sort-keys": "^6.0.0",
47+
"write-file-atomic": "^6.0.0"
4848
},
4949
"devDependencies": {
50-
"ava": "^6.1.3",
51-
"tempy": "^2.0.0",
52-
"tsd": "^0.31.1",
53-
"xo": "^0.59.2"
50+
"tsd": "^0.33.0",
51+
"xo": "^1.2.2"
5452
}
5553
}

test.js

Lines changed: 167 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,191 @@
1-
import fs from 'node:fs';
2-
import test from 'ava';
3-
import tempy from 'tempy';
1+
import fs, {promises as fsPromises} from 'node:fs';
2+
import path from 'node:path';
3+
import {fileURLToPath} from 'node:url';
4+
import {test} from 'node:test';
5+
import assert from 'node:assert/strict';
6+
import {setTimeout as delay} from 'node:timers/promises';
7+
import {tmpdir} from 'node:os';
48
import {writeJsonFile, writeJsonFileSync} from './index.js';
59

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+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
12+
function temporaryFile() {
13+
return path.join(tmpdir(), `write-json-file-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
14+
}
15+
16+
test('async', async () => {
17+
const temporaryFilePath = temporaryFile();
18+
await writeJsonFile(temporaryFilePath, {foo: true}, {indent: 2});
19+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true\n}\n');
20+
fs.unlinkSync(temporaryFilePath);
21+
});
22+
23+
test('sync', () => {
24+
const temporaryFilePath = temporaryFile();
25+
writeJsonFileSync(temporaryFilePath, {foo: true}, {detectIndent: true, indent: 2});
26+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true\n}\n');
27+
fs.unlinkSync(temporaryFilePath);
1028
});
1129

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');
30+
test('detect indent', async () => {
31+
const temporaryFilePath = temporaryFile();
32+
await writeJsonFile(temporaryFilePath, {foo: true}, {indent: 2});
33+
await writeJsonFile(temporaryFilePath, {foo: true, bar: true, foobar: true}, {detectIndent: true});
34+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n');
35+
fs.unlinkSync(temporaryFilePath);
1636
});
1737

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');
38+
test('detect indent synchronously', () => {
39+
const temporaryFilePath = temporaryFile();
40+
writeJsonFileSync(temporaryFilePath, {foo: true}, {indent: 2});
41+
writeJsonFileSync(temporaryFilePath, {foo: true, bar: true, foobar: true}, {detectIndent: true});
42+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n');
43+
fs.unlinkSync(temporaryFilePath);
2344
});
2445

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');
46+
test('fall back to default indent if file doesn\'t exist', async () => {
47+
const temporaryFilePath = temporaryFile();
48+
await writeJsonFile(temporaryFilePath, {foo: true, bar: true, foobar: true}, {detectIndent: true});
49+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"foo": true,\n\t"bar": true,\n\t"foobar": true\n}\n');
50+
fs.unlinkSync(temporaryFilePath);
3051
});
3152

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');
53+
test('async - {sortKeys: true}', async () => {
54+
const temporaryFilePath = temporaryFile();
55+
await writeJsonFile(temporaryFilePath, {c: true, b: true, a: true}, {sortKeys: true});
56+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"a": true,\n\t"b": true,\n\t"c": true\n}\n');
57+
58+
await writeJsonFile(temporaryFilePath, ['c', 'b', 'a'], {sortKeys: true});
59+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '[\n\t"c",\n\t"b",\n\t"a"\n]\n');
60+
fs.unlinkSync(temporaryFilePath);
3661
});
3762

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');
63+
test('async - {sortKeys: false}', async () => {
64+
const temporaryFilePath = temporaryFile();
65+
await writeJsonFile(temporaryFilePath, {c: true, b: true, a: true}, {sortKeys: false});
66+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"c": true,\n\t"b": true,\n\t"a": true\n}\n');
67+
fs.unlinkSync(temporaryFilePath);
68+
});
4269

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');
70+
test('async - `replacer` option', async () => {
71+
const temporaryFilePath = temporaryFile();
72+
await writeJsonFile(temporaryFilePath, {foo: true, bar: true}, {replacer: ['foo']});
73+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"foo": true\n}\n');
74+
fs.unlinkSync(temporaryFilePath);
4575
});
4676

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');
77+
test('sync - `replacer` option', () => {
78+
const temporaryFilePath = temporaryFile();
79+
writeJsonFileSync(temporaryFilePath, {foo: true, bar: true}, {replacer: ['foo']});
80+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"foo": true\n}\n');
81+
fs.unlinkSync(temporaryFilePath);
5182
});
5283

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');
84+
test('async - respect trailing newline at the end of the file', async () => {
85+
const temporaryFilePath = temporaryFile();
86+
fs.writeFileSync(temporaryFilePath, JSON.stringify({foo: true}));
87+
await writeJsonFile(temporaryFilePath, {bar: true});
88+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"bar": true\n}');
89+
fs.unlinkSync(temporaryFilePath);
5790
});
5891

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');
92+
test('sync - respect trailing newline at the end of the file', () => {
93+
const temporaryFilePath = temporaryFile();
94+
fs.writeFileSync(temporaryFilePath, JSON.stringify({foo: true}));
95+
writeJsonFileSync(temporaryFilePath, {bar: true});
96+
assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"bar": true\n}');
97+
fs.unlinkSync(temporaryFilePath);
6398
});
6499

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}');
100+
test('concurrent writes', async () => {
101+
/* eslint-disable no-await-in-loop */
102+
const testDir = path.join(__dirname, 'test-temp-concurrent');
103+
const targetFile = path.join(testDir, 'concurrent-test.json');
104+
const sourceFile = path.join(testDir, 'source.json');
105+
106+
await fsPromises.rm(testDir, {recursive: true, force: true});
107+
await fsPromises.mkdir(testDir, {recursive: true});
108+
109+
await fsPromises.writeFile(sourceFile, JSON.stringify({test: 'data', value: 1}));
110+
111+
const errors = [];
112+
let successCount = 0;
113+
114+
async function worker(id) {
115+
for (let index = 0; index < 100; index++) {
116+
try {
117+
const data = JSON.parse(await fsPromises.readFile(sourceFile, 'utf8'));
118+
data.worker = id;
119+
data.iteration = index;
120+
await writeJsonFile(targetFile, data);
121+
successCount++;
122+
} catch (error) {
123+
errors.push({
124+
worker: id, iteration: index, error: error.message, code: error.code,
125+
});
126+
}
127+
128+
await delay(Math.random() * 10);
129+
}
130+
}
131+
132+
const workers = [];
133+
for (let index = 0; index < 5; index++) {
134+
workers.push(worker(index));
135+
}
136+
137+
await Promise.all(workers);
138+
139+
assert.equal(successCount, 500, `Expected 500 successful writes, got ${successCount}`);
140+
assert.equal(errors.length, 0, `Expected no errors, got ${errors.length}`);
141+
142+
await fsPromises.rm(testDir, {recursive: true, force: true});
143+
/* eslint-enable no-await-in-loop */
70144
});
71145

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}');
146+
test('aggressive concurrent writes', async () => {
147+
/* eslint-disable no-await-in-loop */
148+
const testDir = path.join(__dirname, 'test-temp-aggressive');
149+
const sourceFile = path.join(testDir, 'bar.json');
150+
const targetFile = path.join(testDir, 'foo.json');
151+
152+
await fsPromises.rm(testDir, {recursive: true, force: true});
153+
await fsPromises.mkdir(testDir, {recursive: true});
154+
155+
await fsPromises.writeFile(sourceFile, JSON.stringify({test: 'data', value: 1}));
156+
157+
let errorCount = 0;
158+
let successCount = 0;
159+
const maxIterations = 100;
160+
161+
async function run() {
162+
await delay(10);
163+
const file = await fsPromises.readFile(sourceFile, 'utf8');
164+
const data = JSON.parse(file);
165+
await writeJsonFile(targetFile, data);
166+
}
167+
168+
async function worker() {
169+
for (let index = 0; index < maxIterations; index++) {
170+
try {
171+
await run();
172+
successCount++;
173+
} catch {
174+
errorCount++;
175+
}
176+
}
177+
}
178+
179+
const workers = [];
180+
for (let index = 0; index < 3; index++) {
181+
workers.push(worker());
182+
}
183+
184+
await Promise.all(workers);
185+
186+
assert.equal(successCount, maxIterations * 3, `Expected ${maxIterations * 3} successful writes, got ${successCount}`);
187+
assert.equal(errorCount, 0, `Expected no errors, got ${errorCount}`);
188+
189+
await fsPromises.rm(testDir, {recursive: true, force: true});
190+
/* eslint-enable no-await-in-loop */
77191
});

0 commit comments

Comments
 (0)