Skip to content

Commit 19311cf

Browse files
SukkaWisaacs
authored andcommitted
refactor: switch to native fs.promises
PR-URL: #284 Credit: @SukkaW, @pmmmwh, @lukekarrys Close: #284 Close: #314 Reviewed-by: @isaacs
1 parent 0c437d4 commit 19311cf

2 files changed

Lines changed: 39 additions & 72 deletions

File tree

src/fs.ts

Lines changed: 14 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
// promisify ourselves, because older nodes don't have fs.promises
2-
3-
import fs, { Dirent } from 'fs'
4-
import { readdirSync as rdSync } from 'fs'
1+
import fs, { Dirent, readdirSync as rdSync } from 'fs'
2+
import fsPromises from 'fs/promises'
53

64
// sync ones just take the sync version from node
5+
// readdir forces withFileTypes: true
6+
77
export {
88
chmodSync,
99
mkdirSync,
@@ -18,64 +18,15 @@ export {
1818
export const readdirSync = (path: fs.PathLike): Dirent[] =>
1919
rdSync(path, { withFileTypes: true })
2020

21-
// unrolled for better inlining, this seems to get better performance
22-
// than something like:
23-
// const makeCb = (res, rej) => (er, ...d) => er ? rej(er) : res(...d)
24-
// which would be a bit cleaner.
25-
26-
const chmod = (path: fs.PathLike, mode: fs.Mode): Promise<void> =>
27-
new Promise((res, rej) => fs.chmod(path, mode, er => (er ? rej(er) : res())))
28-
29-
const mkdir = (
30-
path: fs.PathLike,
31-
options?:
32-
| fs.Mode
33-
| (fs.MakeDirectoryOptions & { recursive?: boolean | null })
34-
| null,
35-
): Promise<string | undefined> =>
36-
new Promise((res, rej) =>
37-
fs.mkdir(path, options, (er, made) => (er ? rej(er) : res(made))),
38-
)
39-
40-
const readdir = (path: fs.PathLike): Promise<Dirent[]> =>
41-
new Promise<Dirent[]>((res, rej) =>
42-
fs.readdir(path, { withFileTypes: true }, (er, data) =>
43-
er ? rej(er) : res(data),
44-
),
45-
)
46-
47-
const rename = (oldPath: fs.PathLike, newPath: fs.PathLike): Promise<void> =>
48-
new Promise((res, rej) =>
49-
fs.rename(oldPath, newPath, er => (er ? rej(er) : res())),
50-
)
51-
52-
const rm = (path: fs.PathLike, options: fs.RmOptions): Promise<void> =>
53-
new Promise((res, rej) => fs.rm(path, options, er => (er ? rej(er) : res())))
54-
55-
const rmdir = (path: fs.PathLike): Promise<void> =>
56-
new Promise((res, rej) => fs.rmdir(path, er => (er ? rej(er) : res())))
57-
58-
const stat = (path: fs.PathLike): Promise<fs.Stats> =>
59-
new Promise((res, rej) =>
60-
fs.stat(path, (er, data) => (er ? rej(er) : res(data))),
61-
)
62-
63-
const lstat = (path: fs.PathLike): Promise<fs.Stats> =>
64-
new Promise((res, rej) =>
65-
fs.lstat(path, (er, data) => (er ? rej(er) : res(data))),
66-
)
67-
68-
const unlink = (path: fs.PathLike): Promise<void> =>
69-
new Promise((res, rej) => fs.unlink(path, er => (er ? rej(er) : res())))
70-
7121
export const promises = {
72-
chmod,
73-
mkdir,
74-
readdir,
75-
rename,
76-
rm,
77-
rmdir,
78-
stat,
79-
lstat,
80-
unlink,
22+
chmod: fsPromises.chmod,
23+
mkdir: fsPromises.mkdir,
24+
readdir: (path: fs.PathLike) =>
25+
fsPromises.readdir(path, { withFileTypes: true }),
26+
rename: fsPromises.rename,
27+
rm: fsPromises.rm,
28+
rmdir: fsPromises.rmdir,
29+
stat: fsPromises.stat,
30+
lstat: fsPromises.lstat,
31+
unlink: fsPromises.unlink,
8132
}

test/fs.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,49 @@ import t, { Test } from 'tap'
55
// and that when the cb returns an error, the promised version fails,
66
// and when the cb returns data, the promisified version resolves to it.
77
import realFS from 'fs'
8+
import realFSP from 'fs/promises'
89
import * as fs from '../src/fs.js'
910
import { useNative } from '../src/use-native.js'
1011

1112
type MockCb = (e: Error | null, m?: string) => void
1213
type MockFsCb = Record<string, (cb: MockCb) => void>
1314
type MockFsPromise = Record<string, () => Promise<void>>
1415

15-
const mockFs = async (t: Test, mock: MockFsCb) =>
16+
const mockFs = async (t: Test, fs: MockFsCb = {}, fsp: MockFsPromise = {}) =>
1617
(await t.mockImport('../src/fs.js', {
17-
fs: t.createMock(realFS, mock),
18+
fs: t.createMock(realFS, fs),
19+
'fs/promises': t.createMock(realFSP, fsp),
1820
})) as typeof import('../src/fs.js')
1921

2022
const mockFSMethodPass =
2123
(method: string) =>
2224
(...args: unknown[]) => {
2325
process.nextTick(() => (args.at(-1) as MockCb)(null, method))
2426
}
27+
const mockFSPromiseMethodPass =
28+
(_method: string) =>
29+
() => new Promise<void>((resolve, _reject) => {
30+
resolve()
31+
})
2532
const mockFSMethodFail =
26-
() =>
33+
(_: string) =>
2734
(...args: unknown[]) => {
2835
process.nextTick(() => (args.at(-1) as MockCb)(new Error('oops')))
2936
}
37+
const mockFSPromiseMethodFail =
38+
(_method: string) =>
39+
() => new Promise<void>((_resolve, reject) => {
40+
reject(new Error('oops'))
41+
})
3042

3143
t.type(fs.promises, Object)
3244
const mockFSPass: MockFsCb = {}
3345
const mockFSFail: MockFsCb = {}
46+
const mockFSPromisesFail: MockFsPromise = {}
47+
const mockFSPromisesPass: MockFsPromise = {}
3448

3549
for (const method of Object.keys(fs.promises)) {
50+
3651
// of course fs.rm is missing when we shouldn't use native :)
3752
// also, readdirSync is clubbed to always return file types
3853
if (method !== 'rm' || useNative()) {
@@ -52,7 +67,9 @@ for (const method of Object.keys(fs.promises)) {
5267

5368
// set up our pass/fails for the next tests
5469
mockFSPass[method] = mockFSMethodPass(method)
55-
mockFSFail[method] = mockFSMethodFail()
70+
mockFSPromisesPass[method] = mockFSPromiseMethodPass(method)
71+
mockFSFail[method] = mockFSMethodFail(method)
72+
mockFSPromisesFail[method] = mockFSPromiseMethodFail(method)
5673
}
5774

5875
// doesn't have any sync versions that aren't promisified
@@ -69,18 +86,17 @@ for (const method of Object.keys(fs)) {
6986
}
7087

7188
t.test('passing resolves promise', async t => {
72-
const fs = await mockFs(t, mockFSPass)
89+
const fs = await mockFs(t, mockFSPass, mockFSPromisesPass)
7390
for (const [m, fn] of Object.entries(
7491
fs.promises as unknown as MockFsPromise,
7592
)) {
76-
const expected =
77-
['chmod', 'rename', 'rm', 'rmdir', 'unlink'].includes(m) ? undefined : m
78-
t.same(await fn(), expected, `got expected value for ${m}`)
93+
t.same(await fn(), undefined, `${m} is a Promise<void> method`)
7994
}
8095
})
8196

8297
t.test('failing rejects promise', async t => {
83-
const fs = await mockFs(t, mockFSFail)
98+
const fs = await mockFs(t, mockFSFail, mockFSPromisesFail)
99+
84100
for (const [m, fn] of Object.entries(
85101
fs.promises as unknown as MockFsPromise,
86102
)) {

0 commit comments

Comments
 (0)