Skip to content

Commit

Permalink
Implemented fs.promises.glob and fs.globSync
Browse files Browse the repository at this point in the history
Organized emulation extra types
  • Loading branch information
james-pre committed Dec 2, 2024
1 parent 901467d commit 65551e6
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 91 deletions.
7 changes: 4 additions & 3 deletions src/emulation/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { R_OK } from './constants.js';
import type { Dirent } from './dir.js';
import type { Dir } from './dir.js';
import * as promises from './promises.js';
import { fd2file, fdMap, type _AnyGlobOptions } from './shared.js';
import { fd2file, fdMap } from './shared.js';
import { ReadStream, WriteStream } from './streams.js';
import { FSWatcher, StatWatcher } from './watchers.js';
import type { V_Context } from '../context.js';
import type { GlobOptionsU } from './types.js';

const nop = () => {};

Expand Down Expand Up @@ -885,12 +886,12 @@ export function glob(this: V_Context, pattern: string | string[], options: fs.Gl
export function glob(
this: V_Context,
pattern: string | string[],
options: _AnyGlobOptions | Callback<[string[]], null>,
options: GlobOptionsU | Callback<[string[]], null>,
callback: Callback<[Dirent[]], null> | Callback<[string[]], null> = nop
): void {
callback = typeof options == 'function' ? options : callback;

const it = promises.glob.call<V_Context, [string | string[], _AnyGlobOptions?], NodeJS.AsyncIterator<Dirent | string>>(
const it = promises.glob.call<V_Context, [string | string[], GlobOptionsU?], NodeJS.AsyncIterator<Dirent | string>>(
this,
pattern,
typeof options === 'function' ? undefined : options
Expand Down
77 changes: 41 additions & 36 deletions src/emulation/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import { config } from './config.js';
import * as constants from './constants.js';
import { Dir, Dirent } from './dir.js';
import { dirname, join, parse, resolve } from './path.js';
import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type _AnyGlobOptions, type InternalOptions, type ReaddirOptions } from './shared.js';
import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount } from './shared.js';
import { ReadStream, WriteStream } from './streams.js';
import { FSWatcher, emitChange } from './watchers.js';
import type { GlobOptionsU, InternalOptions, NullEnc, ReaddirOptions, ReaddirOptsI, ReaddirOptsU } from './types.js';
export * as constants from './constants.js';

export class FileHandle implements promises.FileHandle {
Expand Down Expand Up @@ -729,28 +730,12 @@ mkdir satisfies typeof promises.mkdir;
* @param path A path to a file. If a URL is provided, it must use the `file:` protocol.
* @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'`.
*/
export async function readdir(
this: V_Context,
path: fs.PathLike,
options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null
): Promise<string[]>;
export async function readdir(this: V_Context, path: fs.PathLike, options?: ReaddirOptsI<{ withFileTypes?: false }> | NullEnc): Promise<string[]>;
export async function readdir(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & { withFileTypes?: false }): Promise<Buffer[]>;
export async function readdir(
this: V_Context,
path: fs.PathLike,
options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null
): Promise<string[] | Buffer[]>;
export async function readdir(this: V_Context, path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes: true }): Promise<Dirent[]>;
export async function readdir(
this: V_Context,
path: fs.PathLike,
options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null
): Promise<string[] | Dirent[] | Buffer[]>;
export async function readdir(
this: V_Context,
path: fs.PathLike,
options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null
): Promise<string[] | Dirent[] | Buffer[]> {
export async function readdir(this: V_Context, path: fs.PathLike, options?: ReaddirOptsI<{ withFileTypes?: false }> | NullEnc): Promise<string[] | Buffer[]>;
export async function readdir(this: V_Context, path: fs.PathLike, options: ReaddirOptsI<{ withFileTypes: true }>): Promise<Dirent[]>;
export async function readdir(this: V_Context, path: fs.PathLike, options?: ReaddirOptsU<fs.BufferEncodingOption> | NullEnc): Promise<string[] | Dirent[] | Buffer[]>;
export async function readdir(this: V_Context, path: fs.PathLike, options?: ReaddirOptsU<fs.BufferEncodingOption> | NullEnc): Promise<string[] | Dirent[] | Buffer[]> {
options = typeof options === 'object' ? options : { encoding: options };
path = await realpath.call(this, path);

Expand Down Expand Up @@ -1190,22 +1175,42 @@ export function glob(this: V_Context, pattern: string | string[]): NodeJS.AsyncI
export function glob(this: V_Context, pattern: string | string[], opt: fs.GlobOptionsWithFileTypes): NodeJS.AsyncIterator<Dirent>;
export function glob(this: V_Context, pattern: string | string[], opt: fs.GlobOptionsWithoutFileTypes): NodeJS.AsyncIterator<string>;
export function glob(this: V_Context, pattern: string | string[], opt: fs.GlobOptions): NodeJS.AsyncIterator<Dirent | string>;
export function glob(this: V_Context, pattern: string | string[], opt?: _AnyGlobOptions): NodeJS.AsyncIterator<Dirent | string> {
export function glob(this: V_Context, pattern: string | string[], opt?: GlobOptionsU): NodeJS.AsyncIterator<Dirent | string> {
pattern = Array.isArray(pattern) ? pattern : [pattern];
for (const p of pattern) {
const { fs } = resolveMount(p, this);
const { cwd = '/', withFileTypes = false, exclude = () => false } = opt || {};

type Entries = true extends typeof withFileTypes ? Dirent[] : string[];

// Escape special characters in pattern
const regexPatterns = pattern.map(p => {
p = p
.replace(/([.?+^$(){}|[\]/])/g, '$1')
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\?/g, '.');
return new RegExp(`^${p}$`);
});

async function* recursiveList(dir: string): AsyncGenerator<string | Dirent> {
const entries = await readdir(dir, { withFileTypes, encoding: 'utf8' });

for (const entry of entries as Entries) {
const fullPath = withFileTypes ? entry.path : dir + '/' + entry;
if (exclude((withFileTypes ? entry : fullPath) as any)) continue;

/**
* @todo it the pattern.source check correct?
*/
if ((await stat(fullPath)).isDirectory() && regexPatterns.some(pattern => pattern.source.includes('.*'))) {
yield* recursiveList(fullPath);
}

if (regexPatterns.some(pattern => pattern.test(fullPath.replace(/^\/+/g, '')))) {
yield withFileTypes ? entry : fullPath.replace(/^\/+/g, '');
}
}
}

return {
next(): Promise<IteratorResult<any>> {
return Promise.resolve() as any;
},
[Symbol.asyncIterator]() {
return this;
},
[Symbol.asyncDispose]() {
return Promise.resolve();
},
};
return recursiveList(cwd);
}
glob satisfies typeof promises.glob;
23 changes: 0 additions & 23 deletions src/emulation/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,29 +173,6 @@ export function _statfs<const T extends boolean>(fs: FileSystem, bigint?: T): T
} as T extends true ? fs.BigIntStatsFs : fs.StatsFs;
}

/**
* Options used for caching, among other things.
* @internal @hidden *UNSTABLE*
*/
export interface InternalOptions {
/**
* If true, then this readdir was called from another function.
* In this case, don't clear the cache when done.
* @internal *UNSTABLE*
*/
_isIndirect?: boolean;
}

export interface ReaddirOptions extends InternalOptions {
withFileTypes?: boolean;
recursive?: boolean;
}

/**
* @hidden
*/
export type _AnyGlobOptions = fs.GlobOptionsWithFileTypes | fs.GlobOptionsWithoutFileTypes | fs.GlobOptions;

/**
* Change the root path
* @param inPlace if true, this changes the root for the current context instead of creating a new one (if associated with a context).
Expand Down
70 changes: 47 additions & 23 deletions src/emulation/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { config } from './config.js';
import * as constants from './constants.js';
import { Dir, Dirent } from './dir.js';
import { dirname, join, parse, resolve } from './path.js';
import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type _AnyGlobOptions, type InternalOptions, type ReaddirOptions } from './shared.js';
import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount } from './shared.js';
import { emitChange } from './watchers.js';
import type { V_Context } from '../context.js';
import type { GlobOptionsU, ReaddirOptsI, ReaddirOptsU, InternalOptions, ReaddirOptions, NullEnc } from './types.js';

export function renameSync(this: V_Context, oldPath: fs.PathLike, newPath: fs.PathLike): void {
oldPath = normalizePath(oldPath);
Expand Down Expand Up @@ -444,28 +445,12 @@ export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode
}
mkdirSync satisfies typeof fs.mkdirSync;

export function readdirSync(
this: V_Context,
path: fs.PathLike,
options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null
): string[];
export function readdirSync(this: V_Context, path: fs.PathLike, options?: ReaddirOptsI<{ withFileTypes?: false }> | NullEnc): string[];
export function readdirSync(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & { withFileTypes?: false }): Buffer[];
export function readdirSync(
this: V_Context,
path: fs.PathLike,
options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null
): string[] | Buffer[];
export function readdirSync(this: V_Context, path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes: true }): Dirent[];
export function readdirSync(
this: V_Context,
path: fs.PathLike,
options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null
): string[] | Dirent[] | Buffer[];
export function readdirSync(
this: V_Context,
path: fs.PathLike,
options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null
): string[] | Dirent[] | Buffer[] {
export function readdirSync(this: V_Context, path: fs.PathLike, options?: ReaddirOptsI<{ withFileTypes?: false }> | NullEnc): string[] | Buffer[];
export function readdirSync(this: V_Context, path: fs.PathLike, options: ReaddirOptsI<{ withFileTypes: true }>): Dirent[];
export function readdirSync(this: V_Context, path: fs.PathLike, options?: ReaddirOptsU<fs.BufferEncodingOption> | NullEnc): string[] | Dirent[] | Buffer[];
export function readdirSync(this: V_Context, path: fs.PathLike, options?: ReaddirOptsU<fs.BufferEncodingOption> | NullEnc): string[] | Dirent[] | Buffer[] {
options = typeof options === 'object' ? options : { encoding: options };
path = normalizePath(path);
const { fs, path: resolved } = resolveMount(realpathSync.call(this, path), this);
Expand Down Expand Up @@ -877,5 +862,44 @@ export function globSync(pattern: string | string[]): string[];
export function globSync(pattern: string | string[], options: fs.GlobOptionsWithFileTypes): Dirent[];
export function globSync(pattern: string | string[], options: fs.GlobOptionsWithoutFileTypes): string[];
export function globSync(pattern: string | string[], options: fs.GlobOptions): Dirent[] | string[];
export function globSync(pattern: string | string[], options?: _AnyGlobOptions): Dirent[] | string[] {}
export function globSync(pattern: string | string[], options: GlobOptionsU = {}): Dirent[] | string[] {
pattern = Array.isArray(pattern) ? pattern : [pattern];
const { cwd = '/', withFileTypes = false, exclude = () => false } = options;

type Entries = true extends typeof withFileTypes ? Dirent[] : string[];

// Escape special characters in pattern
const regexPatterns = pattern.map(p => {
p = p
.replace(/([.?+^$(){}|[\]/])/g, '\\$1')
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\?/g, '.');
return new RegExp(`^${p}$`);
});

const results: string[] = [];
function recursiveList(dir: string) {
const entries = readdirSync(dir, { withFileTypes, encoding: 'utf8' });

for (const entry of entries as Entries) {
const fullPath = withFileTypes ? entry.path : dir + '/' + entry;
if (exclude((withFileTypes ? entry : fullPath) as any)) continue;

/**
* @todo it the pattern.source check correct?
*/
if (statSync(fullPath).isDirectory() && regexPatterns.some(pattern => pattern.source.includes('.*'))) {
recursiveList(fullPath);
}

if (regexPatterns.some(pattern => pattern.test(fullPath.replace(/^\/+/g, '')))) {
results.push(withFileTypes ? entry.path : fullPath.replace(/^\/+/g, ''));
}
}
}

recursiveList(cwd);
return results;
}
globSync satisfies typeof fs.globSync;
33 changes: 33 additions & 0 deletions src/emulation/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type * as fs from 'node:fs';

/**
* Options used for caching, among other things.
* @internal @hidden *UNSTABLE*
*/
export interface InternalOptions {
/**
* If true, then this readdir was called from another function.
* In this case, don't clear the cache when done.
* @internal *UNSTABLE*
*/
_isIndirect?: boolean;
}

export interface ReaddirOptions extends InternalOptions {
withFileTypes?: boolean;
recursive?: boolean;
}

// Helper types to make the emulation types more readable

/** Helper union @hidden */
export type GlobOptionsU = fs.GlobOptionsWithFileTypes | fs.GlobOptionsWithoutFileTypes | fs.GlobOptions;

/** Helper with union @hidden */
export type ReaddirOptsU<T> = (ReaddirOptions & (fs.ObjectEncodingOptions | T)) | NullEnc;

/** Helper with intersection @hidden */
export type ReaddirOptsI<T> = ReaddirOptions & fs.ObjectEncodingOptions & T;

/** @hidden */
export type NullEnc = BufferEncoding | null;
12 changes: 6 additions & 6 deletions tests/fs/streams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ suite('ReadStream', () => {
closed = true;
});
readStream.close(err => {
assert.strictEqual(err, undefined);
assert.equal(err, null);
assert(closed);
done();
});
Expand All @@ -58,10 +58,10 @@ suite('ReadStream', () => {
test('ReadStream close method can be called multiple times', (_, done) => {
const readStream = new fs.ReadStream();
readStream.close(err => {
assert.strictEqual(err, undefined);
assert.equal(err, null);
// Call close again
readStream.close(err2 => {
assert.strictEqual(err2, undefined);
assert.equal(err2, null);
done();
});
});
Expand Down Expand Up @@ -94,7 +94,7 @@ suite('WriteStream', () => {
closed = true;
});
writeStream.close(err => {
assert.strictEqual(err, undefined);
assert.equal(err, null);
assert(closed);
done();
});
Expand All @@ -119,10 +119,10 @@ suite('WriteStream', () => {
test('WriteStream close method can be called multiple times', (_, done) => {
const writeStream = new fs.WriteStream();
writeStream.close(err => {
assert.strictEqual(err, undefined);
assert.equal(err, null);
// Call close again
writeStream.close(err2 => {
assert.strictEqual(err2, undefined);
assert.equal(err2, null);
done();
});
});
Expand Down

0 comments on commit 65551e6

Please sign in to comment.