diff --git a/src/node/FsPromises.ts b/src/node/FsPromises.ts index c7896137c..ccdcbfb53 100644 --- a/src/node/FsPromises.ts +++ b/src/node/FsPromises.ts @@ -4,6 +4,79 @@ import type * as opts from './types/options'; import type * as misc from './types/misc'; import type { FsCallbackApi, FsPromisesApi } from './types'; +// AsyncIterator implementation for promises.glob +class GlobAsyncIterator implements AsyncIterableIterator { + private results: (string | misc.IDirent)[]; + private index = 0; + + constructor( + private fs: any, + private pattern: string | readonly string[], + private options: opts.IGlobOptions = {}, + ) { + this.results = this.getResults(); + } + + private getResults(): (string | misc.IDirent)[] { + const { globSync } = require('./glob'); + const patterns = Array.isArray(this.pattern) ? this.pattern : [this.pattern]; + const allResults: (string | misc.IDirent)[] = []; + + for (const pat of patterns) { + const results = globSync(this.fs, pat, this.options); + + if (this.options.withFileTypes) { + // Convert string paths to Dirent objects + for (const result of results) { + try { + const fullPath = this.options.cwd ? require('path').join(this.options.cwd, result) : result; + const stat = this.fs.lstatSync(fullPath); + allResults.push({ + name: result, + isDirectory: () => stat.isDirectory(), + isFile: () => stat.isFile(), + isBlockDevice: () => stat.isBlockDevice(), + isCharacterDevice: () => stat.isCharacterDevice(), + isSymbolicLink: () => stat.isSymbolicLink(), + isFIFO: () => stat.isFIFO(), + isSocket: () => stat.isSocket(), + }); + } catch (err) { + // Skip files that can't be statted + } + } + } else { + allResults.push(...results); + } + } + + return allResults; + } + + async next(): Promise> { + if (this.index >= this.results.length) { + return { value: undefined, done: true }; + } + + const value = this.results[this.index++]; + return { value, done: false }; + } + + async return(): Promise> { + this.index = this.results.length; + return { value: undefined, done: true }; + } + + async throw(error: any): Promise> { + this.index = this.results.length; + throw error; + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + return this; + } +} + // AsyncIterator implementation for promises.watch class FSWatchAsyncIterator implements AsyncIterableIterator<{ eventType: string; filename: string | Buffer }> { private watcher: any; @@ -137,7 +210,25 @@ export class FsPromises implements FsPromisesApi { public readonly opendir = promisify(this.fs, 'opendir'); public readonly statfs = promisify(this.fs, 'statfs'); public readonly lutimes = promisify(this.fs, 'lutimes'); - public readonly glob = promisify(this.fs, 'glob'); + public glob(pattern: string | readonly string[]): AsyncIterableIterator; + public glob( + pattern: string | readonly string[], + options: opts.IGlobOptions & { withFileTypes: true }, + ): AsyncIterableIterator; + public glob( + pattern: string | readonly string[], + options: opts.IGlobOptions & { withFileTypes?: false }, + ): AsyncIterableIterator; + public glob( + pattern: string | readonly string[], + options?: opts.IGlobOptions, + ): AsyncIterableIterator; + public glob( + pattern: string | readonly string[], + options?: opts.IGlobOptions, + ): AsyncIterableIterator { + return new GlobAsyncIterator(this.fs, pattern, options || {}); + } public readonly access = promisify(this.fs, 'access'); public readonly chmod = promisify(this.fs, 'chmod'); public readonly chown = promisify(this.fs, 'chown'); diff --git a/src/node/__tests__/glob.test.ts b/src/node/__tests__/glob.test.ts index 83ae8557c..ffbf8e16a 100644 --- a/src/node/__tests__/glob.test.ts +++ b/src/node/__tests__/glob.test.ts @@ -103,22 +103,51 @@ describe('glob APIs', () => { }); describe('promises.glob', () => { - it('should return promise resolving to matching files', async () => { + async function collectFromAsyncIterator(iterator: AsyncIterableIterator): Promise { + const results: T[] = []; + for await (const item of iterator) { + results.push(item); + } + return results; + } + + it('should return AsyncIterator yielding matching files', async () => { const { vol } = setup(); - const files = await vol.promises.glob('*.js', { cwd: '/test' }); + const iterator = vol.promises.glob('*.js', { cwd: '/test' }); + const files = await collectFromAsyncIterator(iterator); expect(files).toEqual(['file1.js']); }); it('should work with recursive patterns', async () => { const { vol } = setup(); - const files = await vol.promises.glob('**/*.js', { cwd: '/test' }); + const iterator = vol.promises.glob('**/*.js', { cwd: '/test' }); + const files = await collectFromAsyncIterator(iterator); expect(files.sort()).toEqual(['file1.js', 'subdir/nested.js']); }); it('should work without options', async () => { const { vol } = setup(); - const files = await vol.promises.glob('/test/*.js'); + const iterator = vol.promises.glob('/test/*.js'); + const files = await collectFromAsyncIterator(iterator); expect(files).toEqual(['/test/file1.js']); }); + + it('should support withFileTypes option returning Dirent objects', async () => { + const { vol } = setup(); + const iterator = vol.promises.glob('*.js', { cwd: '/test', withFileTypes: true }); + const dirents = await collectFromAsyncIterator(iterator); + expect(dirents).toHaveLength(1); + expect(dirents[0]).toHaveProperty('name', 'file1.js'); + expect(dirents[0]).toHaveProperty('isFile'); + expect(typeof dirents[0].isFile).toBe('function'); + expect(dirents[0].isFile()).toBe(true); + }); + + it('should work with multiple patterns', async () => { + const { vol } = setup(); + const iterator = vol.promises.glob(['*.js', '*.ts'], { cwd: '/test' }); + const files = await collectFromAsyncIterator(iterator); + expect(files.sort()).toEqual(['file1.js', 'file2.ts']); + }); }); }); diff --git a/src/node/types/FsPromisesApi.ts b/src/node/types/FsPromisesApi.ts index 99dd29dc2..e23e8e3c9 100644 --- a/src/node/types/FsPromisesApi.ts +++ b/src/node/types/FsPromisesApi.ts @@ -38,5 +38,16 @@ export interface FsPromisesApi { options?: opts.IWatchOptions, ) => AsyncIterableIterator<{ eventType: string; filename: string | Buffer }>; writeFile: (id: misc.TFileHandle, data: misc.TPromisesData, options?: opts.IWriteFileOptions) => Promise; - glob: (pattern: string, options?: opts.IGlobOptions) => Promise; + glob: { + (pattern: string | readonly string[]): AsyncIterableIterator; + ( + pattern: string | readonly string[], + options: opts.IGlobOptions & { withFileTypes: true }, + ): AsyncIterableIterator; + ( + pattern: string | readonly string[], + options: opts.IGlobOptions & { withFileTypes?: false }, + ): AsyncIterableIterator; + (pattern: string | readonly string[], options?: opts.IGlobOptions): AsyncIterableIterator; + }; }