import { AbstractConstructor, Constructor, Awaitable, Ctor } from '@flowr/types'; import { ManuallyRegisteredPiecesSymbol, Path } from './lib/utils.js'; import { Collection } from '@flowr/utilities/collection'; import { PieceLocation, PieceLocationJSON } from './lib/structures/PieceLocation.js'; /** * The options for the store, this features both hooks (changes the behaviour) and handlers (similar to event listeners). */ interface StoreOptions { /** * The name for this store. */ readonly name: StoreName; /** * The paths to load pieces from, should be absolute. * @default [] */ readonly paths?: readonly string[]; /** * The strategy to be used for the loader. * @default Store.defaultStrategy */ readonly strategy?: ILoaderStrategy; } /** * An interface representing a logger function. */ interface StoreLogger { /** * @param value The string to print. All strings will be formatted with the format `[STORE => ${name}] [${type}] ${content}`, * where the content may have identifiers (values or names of methods) surrounded by `'`. For example: * * - `[STORE => commands] [LOAD] Skipped piece '/home/user/bot/src/commands/foo.js' as 'LoaderStrategy#filter' returned 'null'.` * - `[STORE => commands] [INSERT] Unloaded new piece 'foo' due to 'enabled' being 'false'.` * - `[STORE => commands] [UNLOAD] Unloaded piece 'foo'.` */ (value: string): void; } /** * The store class which contains {@link Piece}s. */ declare class Store extends Collection { #private; readonly Constructor: AbstractConstructor; readonly name: StoreName; readonly paths: Set; readonly strategy: ILoaderStrategy; /** * A reference to the {@link Container} object for ease of use. * * @see container */ static readonly container: Container; /** * The queue of manually registered pieces to load. */ private readonly [ManuallyRegisteredPiecesSymbol]; /** * @param constructor The piece constructor this store loads. * @param options The options for the store. */ constructor(constructor: AbstractConstructor, options: StoreOptions); /** * Registers a directory into the store. * @param path The path to be added. * @example * ```typescript * store * .registerPath(resolve('commands')) * .registerPath(resolve('third-party', 'commands')); * ``` */ registerPath(path: Path): this; /** * Adds a piece into the store's list of manually registered pieces. If {@linkcode Store.loadAll()} was called, the * piece will be loaded immediately, otherwise it will be queued until {@linkcode Store.loadAll()} is called. * * All manually registered pieces will be kept even after they are loaded to ensure they can be loaded again if * {@linkcode Store.loadAll()} is called again. * * @remarks * * - Pieces loaded this way will have their {@linkcode LoaderPieceContexts.root root} and * {@linkcode LoaderPieceContext.path path} set to {@linkcode VirtualPath}, and as such, cannot be reloaded. * - This method is useful in environments where file system access is limited or unavailable, such as when using * {@linkplain https://en.wikipedia.org/wiki/Serverless_computing Serverless Computing}. * - This method will always throw a {@link TypeError} if `entry.piece` is not a class. * - This method will always throw a {@linkcode LoaderError} if the piece does not extend the * {@linkcode Store#Constructor store's piece constructor}. * - This operation is atomic, if any of the above errors are thrown, the piece will not be loaded. * * @seealso {@linkcode StoreRegistry.loadPiece()} * @param entry The entry to load. * @example * ```typescript * import { container } from '@flowr/loader'; * * class PingCommand extends Command { * // ... * } * * container.stores.get('commands').loadPiece({ * name: 'ping', * piece: PingCommand * }); * ``` */ loadPiece(entry: StoreManuallyRegisteredPiece): Promise; /** * Loads one or more pieces from a path. * @param root The root directory the file is from. * @param path The path of the file to load, relative to the `root`. * @return All the loaded pieces. */ load(root: string, path: string): Promise; /** * Unloads a piece given its instance or its name. * @param name The name of the file to load. * @return Returns the piece that was unloaded. */ unload(name: string | T): Promise; /** * Unloads all pieces from the store. */ unloadAll(): Promise; /** * Loads all pieces from all directories specified by {@link paths}. */ loadAll(): Promise; /** * Resolves a piece by its name or its instance. * @param name The name of the piece or the instance itself. * @return The resolved piece. */ resolve(name: string | T): T; /** * Inserts a piece into the store. * @param piece The piece to be inserted into the store. * @return The inserted piece. */ insert(piece: T): Promise; /** * Constructs a {@link Piece} instance. * @param Ctor The {@link Piece}'s constructor used to build the instance. * @param data The module's information * @return An instance of the constructed piece. */ construct(Ctor: ILoaderResultEntry, data: HydratedModuleData): T; /** * Loads a directory into the store. * @param root The directory to load the pieces from. * @return An async iterator that yields the pieces to be loaded into the store. */ private loadPath; /** * The default strategy, defaults to {@link LoaderStrategy}, which is constructed on demand when a store is constructed, * when none was set beforehand. */ static defaultStrategy: ILoaderStrategy; /** * The default logger, defaults to `null`. */ static logger: StoreLogger | null; } /** * An entry for a manually registered piece using {@linkcode Store.loadPiece()}. */ interface StoreManuallyRegisteredPiece { name: string; piece: StoreRegistryEntries[StoreName] extends Store ? Constructor : never; } /** * A strict-typed store registry. This is available in {@link container}. * * @example * ```typescript * // Adding new stores * * // Register the store: * container.stores.register(new RouteStore()); * * // Augment Petal to add the new store, in case of a JavaScript * // project, this can be moved to an `Augments.d.ts` (or any other name) * // file somewhere: * declare module '@flowr/loader' { * export interface StoreRegistryEntries { * routes: RouteStore; * } * } * ``` */ declare class StoreRegistry extends Collection { #private; /** * Loads all the registered stores. */ load(): Promise; /** * Registers all user directories from the process working directory, the default value is obtained by assuming * CommonJS (high accuracy) but with fallback for ECMAScript Modules (reads package.json's `main` entry, fallbacks * to `process.cwd()`). * * By default, if you have this folder structure: * ``` * /home/me/my-bot * ├─ src * │ ├─ commands * │ ├─ events * │ └─ main.js * └─ package.json * ``` * * And you run `node src/main.js`, the directories `/home/me/my-bot/src/commands` and `/home/me/my-bot/src/events` will * be registered for the commands and events stores respectively, since both directories are located in the same * directory as your main file. * * **Note**: this also registers directories for all other stores, even if they don't have a folder, this allows you * to create new pieces and hot-load them later anytime. * @param rootDirectory The root directory to register pieces at. */ registerPath(rootDirectory?: Path): void; /** * Registers a store. * * @remarks * * - This method will allow {@linkcode StoreRegistry} to manage the store, meaning: * - {@linkcode StoreRegistry.registerPath()} will call the store's * {@linkcode Store.registerPath() registerPath()} method on call. * - {@linkcode StoreRegistry.load()} will call the store's {@linkcode Store.load() load()} method on call. * - {@linkcode StoreRegistry.loadPiece()} will call the store's {@linkcode Store.loadPiece() loadPiece()} method * on call. * - This will also add all the manually registered pieces by {@linkcode StoreRegistry.loadPiece()} in the store. * * It is generally recommended to register a store as early as possible, before any of the aforementioned methods * are called, otherwise you will have to manually call the aforementioned methods for the store to work properly. * * If there were manually registered pieces for this store with {@linkcode StoreRegistry.loadPiece()}, this method * will add them to the store and delete the queue. Note, however, that this method will not call the store's * {@linkcode Store.loadPiece() loadPiece()} method, and as such, the pieces will not be loaded until * {@linkcode Store.loadAll()} is called. * * * @param store The store to register. */ register(store: Store): this; /** * Deregisters a store. * * @param store The store to deregister. */ deregister(store: Store): this; /** * If the store was {@link StoreRegistry.register registered}, this method will call the store's * {@linkcode Store.loadPiece() loadPiece()} method. * * If it was called, the entry will be loaded immediately without queueing. * * @remarks * * - Pieces loaded this way will have their {@linkcode LoaderPieceContext.root root} and * {@linkcode LoaderPieceContext#path path} set to {@linkcode VirtualPath}, and as such, cannot be reloaded. * - This method is useful in environments where file system access is limited or unavailable, such as when using * {@linkplain https://en.wikipedia.org/wiki/Serverless_computing Serverless Computing}. * - This method will not throw an error if a store with the given name does not exist, it will simply be queued * until it's registered. * - This method will always throw a {@link TypeError} if `entry.piece` is not a class. * - If the store is registered, this method will always throw a {@linkcode LoaderError} if the piece does not * extend the registered {@linkcode Store.Constructor store's piece constructor}. * - This operation is atomic, if any of the above errors are thrown, the piece will not be loaded. * * @seealso {@linkcode Store.loadPiece()} * * @param entry The entry to load. * @example * ```typescript * import { container } from '@flowr/loader'; * * class PingCommand extends Command { * // ... * } * * container.stores.loadPiece({ * store: 'commands', * name: 'ping', * piece: PingCommand * }); * ``` */ loadPiece(entry: StoreManagerManuallyRegisteredPiece): Promise; } interface StoreRegistry { get: ((key: K) => StoreRegistryEntries[K]) & ((key: string) => undefined); has: ((key: StoreRegistryKey) => true) & ((key: string) => false); } /** * A type utility to get the keys of {@linkcode StoreRegistryEntries}. * */ type StoreRegistryKey = keyof StoreRegistryEntries; /** * A type utility to get the values of {@linkcode StoreRegistryEntries}. * */ type StoreRegistryValue = StoreRegistryEntries[StoreRegistryKey]; /** * The {@link StoreRegistry}'s registry, use module augmentation against this interface when adding new stores. * */ interface StoreRegistryEntries { } /** * An entry for a manually registered piece using {@linkcode StoreRegistry.loadPiece()}. * @seealso {@linkcode StoreRegistry.loadPiece()} * */ interface StoreManagerManuallyRegisteredPiece extends StoreManuallyRegisteredPiece { store: StoreName; } /** * Type utility to get the {@linkcode Store} given its name. * */ type StoreOf = StoreRegistryKey extends never ? Store> : StoreRegistryEntries[StoreName]; /** * Type utility to get the {@linkcode Piece} given its {@linkcode Store}'s name. * */ type PieceOf = StoreRegistryKey extends never ? Piece : StoreRegistryEntries[StoreName] extends Store ? PieceType : Piece; /** * Represents the type of the properties injected into the container, which is available at {@link container}. * * Plugins may use this structure to add properties referencing to the plugin's objects so they can be * accessed by both the user and the plugin at any moment and at any place. * * Finally, both library developers and bot developers should augment the Container interface from this module using * [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation). */ interface Container { stores: StoreRegistry; } /** * The injected variables that will be accessible to any place. To add an extra property, simply add a property with a * regular assignment, and it will be available in all places simultaneously. * * @example * ```typescript * // Add a reference for the version: * import { container } from '@flowr/loader'; * * container.version = '1.0.0'; * * // Can be placed anywhere in a TypeScript file, for JavaScript projects, * // you can create an `augments.d.ts` and place the code there. * declare module '@flowr/loader' { * interface Container { * version: string; * } * } * * // In any piece, core, plugin, or custom: * export class UserCommand extends Command { * public messageRun(message, args) { * // The injected version is available here: * const { version } = this.container; * * // ... * } * } * ``` * * @example * ```typescript * // In a plugin's context, e.g. API: * class Api extends Plugin { * static [postInitialization]() { * const server = new Server(this); * container.server = server; * * // ... * } * } * * declare module '@flowr/loader' { * interface Container { * server: Server; * } * } * * // In any piece, even those that aren't routes nor middlewares: * export class UserRoute extends Route { * public [methods.POST](message, args) { * // The injected server is available here: * const { server } = this.container; * * // ... * } * } * ``` */ declare const container: Container; /** * The context for the piece, contains extra information from the store, * the piece's path, and the store that loaded it. */ interface LoaderPieceContext { /** * The root directory the piece was loaded from. */ readonly root: string; /** * The path the module was loaded from, relative to {@link root}. */ readonly path: string; /** * The module's name extracted from the path. */ readonly name: string; /** * The store that loaded the piece. */ readonly store: StoreOf; } /** * The options for the {@link Piece}. */ interface PieceOptions { /** * The name for the piece. * @default '' */ readonly name?: string; /** * Whether or not the piece should be enabled. If set to false, the piece will be unloaded. * @default true */ readonly enabled?: boolean; } /** * The piece to be stored in {@link Store} instances. */ declare class Piece { /** * The store that contains the piece. */ readonly store: StoreOf; /** * The location metadata for the piece's file. */ readonly location: PieceLocation; /** * The name of the piece. */ readonly name: string; /** * Whether or not the piece is enabled. */ enabled: boolean; /** * The raw options passed to this {@link Piece} */ readonly options: Options; /** * A reference to the {@link Container} object for ease of use. * @see container */ static readonly container: Container; constructor(context: LoaderPieceContext, options?: PieceOptions); /** * Per-piece listener that is called when the piece is loaded into the store. * Useful to set-up asynchronous initialization tasks. */ onLoad(): Awaitable; /** * Per-piece listener that is called when the piece is unloaded from the store. * Useful to set-up clean-up tasks. */ onUnload(): Awaitable; /** * Unloads and disables the piece. */ unload(): Promise; /** * Reloads the piece by loading the same path in the store. */ reload(): Promise; /** * Defines the `JSON.stringify` behavior of this piece. */ toJSON(): PieceJSON; } /** * The return type of {@link Piece.toJSON}. */ interface PieceJSON { location: PieceLocationJSON; name: string; enabled: boolean; options: PieceOptions; } /** * The module data information. */ interface ModuleData { /** * The name of the module. */ name: string; /** * The absolute path to the module. */ path: string; /** * The extension of the module. */ extension: string; } /** * The hydrated module data. */ interface HydratedModuleData extends ModuleData { /** * The directory the module was loaded from. */ root: string; } /** * Adds the final module data properties. * @param root The root directory to add. * @param data The module data returned from {@link ILoaderStrategy.filter}. * @returns The finished module data. * @private */ declare const hydrateModuleData: (root: string, data: ModuleData) => HydratedModuleData; /** * The result from the filter. */ type FilterResult = ModuleData | null; /** * Represents the return data from {@link ILoaderStrategy.preload} */ type PreloadResult = Awaitable & Record>; /** * Represents the return data from {@link ILoaderStrategy.preload} */ type AsyncPreloadResult = Promise & Record>; /** * Represents an entry from {@link ILoaderResult}. */ type ILoaderResultEntry = Ctor, T>; /** * Represents the return data from {@link ILoaderStrategy.load}. */ type ILoaderResult = AsyncIterableIterator>; /** * An abstracted loader strategy interface. */ interface ILoaderStrategy { /** * Retrieves the name and the extension of the specified file path. * @param path The path of the file to be processed. * @return A {@link ModuleData} on success, otherwise `null` to stop the store from processing the path. * @example * ```typescript * // ts-node support * class MyStrategy extends LoaderStrategy { * filter(path) { * const extension = extname(path); * if (!['.js', '.ts'].includes(extension)) return null; * const name = basename(path, extension); * return { extension, name }; * } * } * ``` */ filter: (path: string) => FilterResult; /** * The pre-load hook, use this to override the loader. * @example * ```typescript * class MyStrategy extends LoaderStrategy { * preload(file) { * return import(file.path); * } * } * ``` */ preload: (file: ModuleData) => PreloadResult; /** * The load hook, use this to override the loader. * @example * ```typescript * class MyStrategy extends LoaderStrategy { * load(store, file) { * // ... * } * } * ``` */ load: (store: Store, file: HydratedModuleData) => ILoaderResult; /** * Called after a piece has been loaded, but before {@link Piece.onLoad} and {@link Store.set}. * @param store The store that holds the piece. * @param piece The piece that was loaded. */ onLoad(store: Store, piece: T): Awaitable; /** * Called after all pieces have been loaded. * @param store The store that loaded all pieces. */ onLoadAll: (store: Store) => Awaitable; /** * Called after a piece has been unloaded or overwritten by a newly loaded piece. * @param store The store that held the piece. * @param piece The piece that was unloaded. */ onUnload(store: Store, piece: T): Awaitable; /** * Called after all pieces have been unloaded. * @param store The store that unloaded all pieces. */ onUnloadAll: (store: Store) => Awaitable; /** * @param error The error that was thrown. * @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, path: string, logger?: StoreLogger | null) => AsyncIterableIterator; } export { type AsyncPreloadResult as A, type Container as C, type FilterResult as F, type HydratedModuleData as H, type ILoaderResultEntry as I, type LoaderPieceContext as L, type ModuleData as M, type PreloadResult as P, type StoreOptions as S, type ILoaderResult as a, type ILoaderStrategy as b, container as c, type PieceOptions as d, Piece as e, type PieceJSON as f, type StoreLogger as g, hydrateModuleData as h, Store as i, type StoreManuallyRegisteredPiece as j, StoreRegistry as k, type StoreRegistryKey as l, type StoreRegistryValue as m, type StoreRegistryEntries as n, type StoreManagerManuallyRegisteredPiece as o, type StoreOf as p, type PieceOf as q };