Skip to content

Commit

Permalink
refactor: move walk to ILoaderStrategy (#352)
Browse files Browse the repository at this point in the history
* refactor: move `walk` to `ILoaderStrategy`

* fix: unbound methods

---------

Co-authored-by: Jeroen Claassens <support@favware.tech>
  • Loading branch information
kyranet and favna authored Nov 19, 2023
1 parent ef03b83 commit 540ac88
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 29 deletions.
10 changes: 9 additions & 1 deletion src/lib/strategies/ILoaderStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Awaitable, Constructor, Ctor } from '@sapphire/utilities';
import type { Piece } from '../structures/Piece';
import type { Store } from '../structures/Store';
import type { Store, StoreLogger } from '../structures/Store';

/**
* The module data information.
Expand Down Expand Up @@ -147,4 +147,12 @@ export interface ILoaderStrategy<T extends Piece> {
* @param path The path of the file that caused the error to be thrown.
*/
onError(error: Error, path: string): void;

/**
* Walks the specified path and returns an async iterator of all the files' paths.
* @param store The store that is walking the path.
* @param path The path to recursively walk.
* @param logger The logger to use when walking the path, if any.
*/
walk?(store: Store<T>, path: string, logger?: StoreLogger | null): AsyncIterableIterator<string>;
}
23 changes: 21 additions & 2 deletions src/lib/strategies/LoaderStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { isNullish, type Awaitable } from '@sapphire/utilities';
import { basename, extname } from 'path';
import { opendir } from 'fs/promises';
import { basename, extname, join } from 'path';
import { pathToFileURL } from 'url';
import { MissingExportsError } from '../errors/MissingExportsError';
import { getRootData } from '../internal/RootScan';
import { mjsImport } from '../internal/internal';
import type { Piece } from '../structures/Piece';
import type { Store } from '../structures/Store';
import type { Store, StoreLogger } from '../structures/Store';
import type {
AsyncPreloadResult,
FilterResult,
Expand Down Expand Up @@ -118,4 +119,22 @@ export class LoaderStrategy<T extends Piece> implements ILoaderStrategy<T> {
public onError(error: Error, path: string): void {
console.error(`Error when loading '${path}':`, error);
}

public async *walk(store: Store<T>, path: string, logger?: StoreLogger | null): AsyncIterableIterator<string> {
logger?.(`[STORE => ${store.name}] [WALK] Loading all pieces from '${path}'.`);
try {
const dir = await opendir(path);
for await (const item of dir) {
if (item.isFile()) yield join(dir.path, item.name);
else if (item.isDirectory()) yield* this.walk(store, join(dir.path, item.name), logger);
}
} catch (error) {
// Specifically ignore ENOENT, which is commonly raised by fs operations
// to indicate that a component of the specified pathname does not exist.
// No entity (file or directory) could be found by the given path.
if ((error as ErrorWithCode).code !== 'ENOENT') this.onError(error as Error, path);
}
}
}

type ErrorWithCode = Error & { code: string };
40 changes: 14 additions & 26 deletions src/lib/structures/Store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Collection } from '@discordjs/collection';
import { Constructor, classExtends, isClass, type AbstractConstructor } from '@sapphire/utilities';
import { promises as fsp } from 'fs';
import { join } from 'path';
import { LoaderError, LoaderErrorType } from '../errors/LoaderError';
import { resolvePath, type Path } from '../internal/Path';
Expand Down Expand Up @@ -48,6 +47,8 @@ export interface StoreLogger {
(value: string): void;
}

const defaultStrategy = new LoaderStrategy();

/**
* The store class which contains {@link Piece}s.
*/
Expand All @@ -67,6 +68,11 @@ export class Store<T extends Piece, StoreName extends StoreRegistryKey = StoreRe
*/
#calledLoadAll = false;

/**
* The walk function for the store.
*/
#walk: LoaderStrategy<T>['walk'];

/**
* @param constructor The piece constructor this store loads.
* @param options The options for the store.
Expand All @@ -77,6 +83,11 @@ export class Store<T extends Piece, StoreName extends StoreRegistryKey = StoreRe
this.name = options.name as StoreRegistryKey;
this.paths = new Set(options.paths ?? []);
this.strategy = options.strategy ?? Store.defaultStrategy;

this.#walk =
typeof this.strategy.walk === 'function' //
? this.strategy.walk.bind(this.strategy)
: defaultStrategy.walk.bind(defaultStrategy);
}

/**
Expand Down Expand Up @@ -342,7 +353,7 @@ export class Store<T extends Piece, StoreName extends StoreRegistryKey = StoreRe
*/
private async *loadPath(root: string): AsyncIterableIterator<T> {
Store.logger?.(`[STORE => ${this.name}] [WALK] Loading all pieces from '${root}'.`);
for await (const child of this.walk(root)) {
for await (const child of this.#walk(this, root, Store.logger)) {
const data = this.strategy.filter(child);
if (data === null) {
Store.logger?.(`[STORE => ${this.name}] [LOAD] Skipped piece '${child}' as 'LoaderStrategy#filter' returned 'null'.`);
Expand All @@ -359,32 +370,11 @@ export class Store<T extends Piece, StoreName extends StoreRegistryKey = StoreRe
}
}

/**
* Retrieves all possible pieces.
* @param path The directory to load the pieces from.
* @return An async iterator that yields the modules to be processed and loaded into the store.
*/
private async *walk(path: string): AsyncIterableIterator<string> {
Store.logger?.(`[STORE => ${this.name}] [WALK] Loading all pieces from '${path}'.`);
try {
const dir = await fsp.opendir(path);
for await (const item of dir) {
if (item.isFile()) yield join(dir.path, item.name);
else if (item.isDirectory()) yield* this.walk(join(dir.path, item.name));
}
} catch (error) {
// Specifically ignore ENOENT, which is commonly raised by fs operations
// to indicate that a component of the specified pathname does not exist.
// No entity (file or directory) could be found by the given path.
if ((error as ErrorWithCode).code !== 'ENOENT') this.strategy.onError(error as Error, path);
}
}

/**
* The default strategy, defaults to {@link LoaderStrategy}, which is constructed on demand when a store is constructed,
* when none was set beforehand.
*/
public static defaultStrategy: ILoaderStrategy<any> = new LoaderStrategy();
public static defaultStrategy: ILoaderStrategy<any> = defaultStrategy;

/**
* The default logger, defaults to `null`.
Expand All @@ -401,8 +391,6 @@ export interface StoreManuallyRegisteredPiece<StoreName extends StoreRegistryKey
piece: StoreRegistryEntries[StoreName] extends Store<infer Piece> ? Constructor<Piece> : never;
}

type ErrorWithCode = Error & { code: string };

export namespace Store {
export const Registry = StoreRegistry;
export type Options<T extends Piece> = StoreOptions<T>;
Expand Down

0 comments on commit 540ac88

Please sign in to comment.