Skip to content

Commit 3286ae7

Browse files
committed
feat: 🎸 support recursive flag in FsaNodeFs
1 parent bdee8da commit 3286ae7

2 files changed

Lines changed: 86 additions & 35 deletions

File tree

packages/fs-fsa-to-node/src/FsaNodeFs.ts

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,41 @@ const notImplemented: (...args: any[]) => any = () => {
3636

3737
const noop: (...args: any[]) => any = () => {};
3838

39+
/** Lists a directory, descending into sub-directories when `recursive`. */
40+
const listDir = async (
41+
dir: fsa.IFileSystemDirectoryHandle,
42+
parentPath: string,
43+
recursive: boolean,
44+
sort: boolean,
45+
): Promise<FsaNodeDirent[]> => {
46+
const separator = FsaToNodeConstants.Separator;
47+
const entries: [name: string, handle: fsa.IFileSystemHandle][] = [];
48+
for await (const entry of dir.entries()) entries.push(entry);
49+
if (sort)
50+
entries.sort((a, b) => {
51+
if (a[0] < b[0]) return -1;
52+
if (a[0] > b[0]) return 1;
53+
return 0;
54+
});
55+
const list: FsaNodeDirent[] = [];
56+
for (const [name, handle] of entries) {
57+
list.push(new FsaNodeDirent(name, parentPath, handle.kind));
58+
if (recursive && handle.kind === 'directory') {
59+
const childPath = parentPath === separator ? parentPath + name : parentPath + separator + name;
60+
list.push(...(await listDir(handle as fsa.IFileSystemDirectoryHandle, childPath, true, sort)));
61+
}
62+
}
63+
return list;
64+
};
65+
66+
const direntToRelative = (dirent: FsaNodeDirent, parentPath: string): string => {
67+
const separator = FsaToNodeConstants.Separator;
68+
const name = String(dirent.name);
69+
if (dirent.parentPath === parentPath) return name;
70+
const prefix = parentPath === separator ? parentPath : parentPath + separator;
71+
return dirent.parentPath.slice(prefix.length) + separator + name;
72+
};
73+
3974
/**
4075
* Constructs a Node.js `fs` API from a File System Access API
4176
* [`FileSystemDirectoryHandle` object](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle).
@@ -461,29 +496,11 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
461496
this.getDir(folder, false, 'readdir')
462497
.then(dir =>
463498
(async () => {
464-
if (options.withFileTypes) {
465-
const list: misc.IDirent[] = [];
466-
const parentPath = folder.length ? '/' + folder.join(FsaToNodeConstants.Separator) : '/';
467-
for await (const [name, handle] of dir.entries()) {
468-
const dirent = new FsaNodeDirent(name, parentPath, handle.kind);
469-
list.push(dirent);
470-
}
471-
if (!isWin && options.encoding !== 'buffer')
472-
list.sort((a, b) => {
473-
if (a.name < b.name) return -1;
474-
if (a.name > b.name) return 1;
475-
return 0;
476-
});
477-
return list;
478-
} else {
479-
const list: string[] = [];
480-
481-
for await (const key of dir.keys()) list.push(key);
482-
483-
if (!isWin && options.encoding !== 'buffer') list.sort();
484-
485-
return list;
486-
}
499+
const parentPath = folder.length ? '/' + folder.join(FsaToNodeConstants.Separator) : '/';
500+
const sort = !isWin && options.encoding !== 'buffer';
501+
const list = await listDir(dir, parentPath, !!options.recursive, sort);
502+
if (options.withFileTypes) return list;
503+
return list.map(dirent => direntToRelative(dirent, parentPath));
487504
})(),
488505
)
489506
.then(
@@ -1095,20 +1112,25 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
10951112
const filename = util.pathToFilename(path);
10961113
const [folder] = pathToLocation(filename);
10971114
const adapter = this.getSyncAdapter();
1098-
const list = adapter.call('readdir', [filename]);
1099-
if (opts.withFileTypes) {
1100-
const res: misc.IDirent[] = [];
1101-
const parentPath = folder.length ? '/' + folder.join(FsaToNodeConstants.Separator) : '/';
1102-
for (const entry of list) res.push(new FsaNodeDirent(entry.name, parentPath, entry.kind));
1103-
return res;
1104-
} else {
1105-
const res: misc.TDataOut[] = [];
1106-
for (const entry of list) {
1107-
const buffer = Buffer.from(entry.name);
1108-
res.push(util.bufferToEncoding(buffer, opts.encoding));
1115+
const separator = FsaToNodeConstants.Separator;
1116+
const parentPath = folder.length ? separator + folder.join(separator) : separator;
1117+
const walk = (dir: string, at: string): FsaNodeDirent[] => {
1118+
const res: FsaNodeDirent[] = [];
1119+
for (const entry of adapter.call('readdir', [dir])) {
1120+
res.push(new FsaNodeDirent(entry.name, at, entry.kind));
1121+
if (opts.recursive && entry.kind === 'directory')
1122+
res.push(
1123+
...walk(
1124+
dir.endsWith(separator) ? dir + entry.name : dir + separator + entry.name,
1125+
at === separator ? at + entry.name : at + separator + entry.name,
1126+
),
1127+
);
11091128
}
11101129
return res;
1111-
}
1130+
};
1131+
const list = walk(filename, parentPath);
1132+
if (opts.withFileTypes) return list;
1133+
return list.map(dirent => util.bufferToEncoding(Buffer.from(direntToRelative(dirent, parentPath)), opts.encoding));
11121134
};
11131135

11141136
public readonly realpathSync: FsSynchronousApi['realpathSync'] = (

packages/fs-fsa-to-node/src/__tests__/FsaNodeFs.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,35 @@ onlyOnNode20('FsaNodeFs', () => {
363363
expect(list.find(item => item.name === 'f.html')?.isFile()).toBe(true);
364364
expect(list.find(item => item.name === 'f.html')?.isDirectory()).toBe(false);
365365
});
366+
367+
test('can read the whole subtree with "recursive" flag set', async () => {
368+
const { fs } = setup({ folder: { sub: { deep: 'test' }, file: 'test' }, 'f.html': 'test' });
369+
const res = (await fs.promises.readdir('/', { recursive: true })) as string[];
370+
expect(res).toEqual(['f.html', 'folder', 'folder/file', 'folder/sub', 'folder/sub/deep']);
371+
});
372+
373+
test('"recursive" paths are relative to the directory read', async () => {
374+
const { fs } = setup({ folder: { sub: { deep: 'test' } } });
375+
const res = (await fs.promises.readdir('/folder', { recursive: true })) as string[];
376+
expect(res).toEqual(['sub', 'sub/deep']);
377+
});
378+
379+
test('"recursive" with "withFileTypes" reports each entry against its own parent', async () => {
380+
const { fs } = setup({ folder: { sub: { deep: 'test' } }, 'f.html': 'test' });
381+
const list = (await fs.promises.readdir('/', { recursive: true, withFileTypes: true })) as IDirent[];
382+
expect(list.map(item => [item.parentPath, item.name])).toEqual([
383+
['/', 'f.html'],
384+
['/', 'folder'],
385+
['/folder', 'sub'],
386+
['/folder/sub', 'deep'],
387+
]);
388+
expect(list.find(item => item.name === 'deep')?.isFile()).toBe(true);
389+
});
390+
391+
test('without "recursive" only the top level is listed', async () => {
392+
const { fs } = setup({ folder: { sub: { deep: 'test' } } });
393+
expect((await fs.promises.readdir('/')) as string[]).toEqual(['folder']);
394+
});
366395
});
367396

368397
describe('.appendFile()', () => {

0 commit comments

Comments
 (0)