feat!: new module resolution algorithm

This commit is contained in:
Jacob Nguyen
2023-05-04 18:45:54 -05:00
parent e2fd44516a
commit faa0b4882e
9 changed files with 213 additions and 303 deletions

View File

@@ -1,35 +1,45 @@
import { readdirSync, statSync } from 'fs';
import { join, basename } from 'path';
import { readdir, stat } from 'fs/promises';
import { join, basename, resolve } from 'path';
import { type Observable, from, mergeMap } from 'rxjs';
import { SernError } from './structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { ImportPayload } from '../types/handler';
import { pathToFileURL } from 'node:url';
import { Processed } from '../types/handler';
import { Module } from '../types/module';
import * as assert from 'node:assert'
import * as util from 'node:util'
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
async function* readPath(dir: string): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const fullPath = join(dir, file);
const fileStats = await stat(fullPath);
if (fileStats.isDirectory()) {
yield* readPath(fullPath);
} else {
/// #if MODE === 'esm'
yield 'file:///'+fullPath;
/// #elif MODE === 'cjs'
yield fullPath;
/// #endif
}
}
return arrayOfFiles;
} catch (err) {
throw err;
}
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
export async function defaultModuleLoader<T>(
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
export async function defaultModuleLoader<T extends Module>(
absPath: string,
): Promise<Result<ImportPayload<T>, SernError>> {
): Promise<Result< Processed<T>, SernError>> {
// prettier-ignore
let module: T | undefined
/// #if MODE === 'esm'
= (await import(pathToFileURL(absPath).toString())).default
= (await import(absPath)).default
/// #elif MODE === 'cjs'
= require(absPath).default; // eslint-disable-line
/// #endif
@@ -39,7 +49,12 @@ export async function defaultModuleLoader<T>(
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
checkIsProcessed(module)
return Ok(module);
}
function checkIsProcessed<T extends Module>(m: T): asserts m is Processed<T> {
assert.ok(m.name !== undefined, `name is not defined for ${util.format(m)}`)
}
/**
@@ -48,15 +63,15 @@ export async function defaultModuleLoader<T>(
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildModuleStream<T>(
export function buildModuleStream<T extends Module >(
commandDir: string,
): Observable<Result<ImportPayload<T>, SernError>> {
): Observable<Result<Processed<T>, SernError>> {
const commands = getCommands(commandDir);
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
}
export function getCommands(dir: string): string[] {
return readPath(join(process.cwd(), dir));
export function getCommands(dir: string) {
return readPath(resolve(dir));
}
export function filename(path: string) {

View File

@@ -1,7 +1,3 @@
import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler';
import { ApplicationCommandType, ComponentType } from './enums';
/**
* @since 2.0.0
@@ -9,20 +5,7 @@ import { ApplicationCommandType, ComponentType } from './enums';
* This dependency is usually injected into ModuleManager
*/
export class ModuleStore {
readonly BothCommands = new Map<string, Processed<CommandModule>>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.Message]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.ChatInput]: new Map<string, Processed<CommandModule>>(),
};
readonly ModalSubmit = new Map<string, Processed<CommandModule>>();
readonly TextCommands = new Map<string, Processed<CommandModule>>();
readonly InteractionHandlers = {
[ComponentType.Button]: new Map<string, Processed<CommandModule>>(),
[ComponentType.StringSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.ChannelSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.MentionableSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.RoleSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.UserSelect]: new Map<string, Processed<CommandModule>>(),
};
readonly Commands = new Map<string, string>();
}

View File

@@ -2,7 +2,7 @@ import type { Processed } from '../../../types/handler';
import type { AutocompleteInteraction } from 'discord.js';
import { SernError } from '../../../core/structures';
import { treeSearch } from '../../../core/functions';
import type { CommandModule, Module } from '../../../types/module';
import type { BothCommand, CommandModule, Module } from '../../../types/module';
import { EventEmitter } from 'events';
import * as assert from 'assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
@@ -51,7 +51,7 @@ export function eventDispatcher(module: Processed<Module>, source: unknown) {
}
export function dispatchAutocomplete(
module: Processed<Module>,
module: Processed<BothCommand>,
interaction: AutocompleteInteraction,
) {
const option = treeSearch(interaction, module.options);

View File

@@ -0,0 +1,95 @@
import { BaseInteraction, ChatInputCommandInteraction, Interaction, InteractionType } from "discord.js";
import { Observable, filter, map, pipe, switchMap} from "rxjs";
import { CommandType, ModuleManager, SernEmitter, SernError } from "../../core";
import { filterMap, errTap } from '../../core/operators';
import { defaultModuleLoader } from "../../core/module-loading";
import { Processed } from "../../types/handler";
import { AnyModule, BothCommand, CommandModule } from "../../types/module";
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from "./dispatchers";
import { isAutocomplete } from "../../core/predicates";
import { err } from "../../core/functions";
import * as Files from '../../core/module-loading';
import { sernMeta } from "../../commands";
/**
* Creates an RxJS observable that filters and maps incoming interactions to their respective modules.
* @param i An RxJS observable of interactions.
* @param mg The module manager instance used to retrieve the module path for each interaction.
* @returns A handler to create a RxJS observable of dispatchers that take incoming interactions and execute their corresponding modules.
*/
export function createHandler<T extends BaseInteraction>(
i: Observable<Interaction>,
mg: ModuleManager,
) {
return (pred: (i: BaseInteraction) => i is T) =>
i.pipe(
filter(pred),
filterMap(event => {
const fullPath = mg.get(createId(event as unknown as Interaction))
if(!fullPath) return err();
return defaultModuleLoader<CommandModule>(fullPath)
.then(res => res.map(module => ({ module, event }) ))
}),
map(createDispatcher)
)
}
/**
* Creates a unique ID for a given interaction object.
* @param event The interaction object for which to create an ID.
* @returns A unique string ID based on the type and properties of the interaction object.
*/
function createId<T extends Interaction>(event: T) {
let id: string;
switch(event.type) {
case InteractionType.MessageComponent: {
id = `${event.customId}__C${event.componentType}`;
} break;
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete: {
id = `${event.commandName}__A${event.commandType}`;
console.log(id)
} break;
case InteractionType.ModalSubmit: {
id = `${event.customId}__C1`;
} break;
}
return id;
}
function createDispatcher({
module,
event,
}: {
module: Processed<CommandModule>;
event: BaseInteraction;
}) {
switch (module.type) {
case CommandType.Text:
throw Error(SernError.MismatchEvent+ " Found a text module in interaction stream.");
case CommandType.Slash:
case CommandType.Both: {
if (isAutocomplete(event)) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
*/
return dispatchAutocomplete(module as Processed<BothCommand>, event);
}
return dispatchCommand(module, contextArgs(event as ChatInputCommandInteraction));
}
default:
return dispatchCommand(module, interactionArg(event));
}
}
export function buildModules<T extends AnyModule>(path: string, sernEmitter: SernEmitter) {
return pipe(
switchMap(() => Files.buildModuleStream<T>(path)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
map(module => ({ module, absPath: module[sernMeta].fullPath }))
);
}

View File

@@ -1,101 +1,22 @@
import { ChatInputCommandInteraction, Interaction, InteractionType } from 'discord.js';
import { Interaction } from 'discord.js';
import {
catchError,
concatMap,
EMPTY,
filter,
finalize,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
merge,
} from 'rxjs';
import { CommandType,SernError } from '../../core/structures';
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
import { SernError } from '../../core/structures';
import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module';
import { ErrorHandling, handleError } from '../../core/contracts/errorHandling';
import { SernEmitter, WebsocketStrategy } from '../../core';
import type { Processed } from '../../types/handler';
import { sharedObservable } from '../../core/operators'
import { useContainerRaw } from '../../core/dependencies';
import type { Logging, ModuleManager } from '../../core/contracts';
import type { EventEmitter } from 'node:events';
import { ModuleGetter, createModuleGetter } from '../../core/contracts/moduleManager';
import { isAutocomplete, isCommand, isMessageComponent, isModal } from '../../core/predicates';
import { createHandler } from './generic';
function handleMessageComponents(i: Observable<Interaction>, mg: ModuleGetter) {
return i.pipe(
filter(e => e.isMessageComponent()),
map(event => ({ module: mg('' as any), event }) )
)
}
function handleAutocomplete(i: Observable<Interaction>, mg: ModuleGetter) {
return i.pipe(
filter(e => e.isAutocomplete()),
map(event => ({ module: mg('' as any), event }) )
)
}
function handleApplicationCommands(i: Observable<Interaction>, mg: ModuleGetter) {
return i.pipe(
filter(e => e.isCommand()),
map(event => ({ module: mg('' as any), event }) )
)
}
function handleModal(i: Observable<Interaction>, mg: ModuleGetter) {
return i.pipe(
filter(e => e.isModalSubmit()),
map(event => ({ module: mg('' as any), event }) )
)
}
function makeInteractionProcessor(
modules: ModuleManager,
): OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }> {
const get = createModuleGetter(modules);
return pipe(
concatMap(event => {
switch(event.type) {
case InteractionType.MessageComponent:
case InteractionType.ModalSubmit: {
const id = `${event.customId}__M${event.componentType}`
} break;
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete: {
}
}
if (event.isMessageComponent()) {
const customId = event.customId;
const module = get(ms => {
return ms.InteractionHandlers[event.componentType].get(customId);
});
return of({ module, event });
} else if (event.isCommand() || event.isAutocomplete()) {
const commandName = event.commandName;
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(commandName) ??
ms.BothCommands.get(commandName),
);
return of({ module, event });
} else if (event.isModalSubmit()) {
const module = get(ms => ms.ModalSubmit.get(event.customId));
return of({ module, event });
} else return EMPTY;
}),
filter(m => m.module !== undefined),
) as OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }>;
}
export function makeInteractionCreate([s, err, log, modules, client]: [
SernEmitter,
ErrorHandling,
@@ -105,13 +26,16 @@ export function makeInteractionCreate([s, err, log, modules, client]: [
],
platform: WebsocketStrategy
) {
//map. If nothing again,this means a slash command
const interactionStream$ = fromEvent(client, platform.eventNames[0]) as Observable<Interaction>;
const interactionProcessor = makeInteractionProcessor(modules);
return interactionStream$
const interactionStream$ = sharedObservable<Interaction>(client, platform.eventNames[0]);
const handle = createHandler(interactionStream$, modules);
const interactionHandler$ = merge(
handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal)
);
return interactionHandler$
.pipe(
interactionProcessor,
map(createDispatcher),
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
@@ -119,7 +43,7 @@ export function makeInteractionCreate([s, err, log, modules, client]: [
catchError(handleError(err, log)),
finalize(() => {
log?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
message: 'interaction stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
@@ -128,31 +52,3 @@ export function makeInteractionCreate([s, err, log, modules, client]: [
)
.subscribe();
}
function createDispatcher({
module,
event,
}: {
event: Interaction;
module: Processed<CommandModule>;
}) {
switch (module.type) {
case CommandType.Text:
throw Error(SernError.MismatchEvent);
case CommandType.Slash:
case CommandType.Both: {
if (event.isAutocomplete()) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
*/
return dispatchAutocomplete(module, event);
} else {
return dispatchCommand(module, contextArgs(event as ChatInputCommandInteraction));
}
}
default:
return dispatchCommand(module, interactionArg(event));
}
}

View File

@@ -1,5 +1,5 @@
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of, pipe } from 'rxjs';
import { type ModuleStore, SernError } from '../../core/structures';
import { catchError, concatMap, EMPTY, finalize, map, of, pipe } from 'rxjs';
import { SernError } from '../../core/structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module';
@@ -11,7 +11,9 @@ import { useContainerRaw } from '../../core/dependencies';
import type { Logging, ModuleManager } from '../../core/contracts';
import type { EventEmitter } from 'node:events';
import { WebsocketStrategy } from '../../core';
import { createModuleGetter } from '../../core/contracts/moduleManager';
import { err } from '../../core/functions';
import { defaultModuleLoader } from '../../core/module-loading';
import { sharedObservable, filterMap } from '../../core/operators';
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
@@ -34,25 +36,21 @@ export function fmt(msg: string, prefix: string): string[] {
*/
const createMessageProcessor = (
defaultPrefix: string,
get: (
cb: (ms: ModuleStore) => Processed<CommandModule> | undefined,
) => CommandModule | undefined,
moduleManager: ModuleManager
) =>
pipe(
ignoreNonBot(defaultPrefix),
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
filterMap(message => {
const [prefix, ...rest] = fmt(message.content, defaultPrefix);
const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
if (module === undefined) {
return EMPTY;
const fullPath = moduleManager.get(`${prefix}__A0`);
if (fullPath === undefined) {
return err();
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
return defaultModuleLoader<CommandModule>(fullPath).then(
result => {
const args = contextArgs(message, rest);
return result.map(module => ({ module, args }))
})
}),
map(({ args, module }) => dispatchCommand(module as Processed<CommandModule>, args)),
);
@@ -70,9 +68,8 @@ export function makeMessageCreate(
if(!platform.defaultPrefix) {
return EMPTY.subscribe()
}
const get = createModuleGetter(modules);
const messageStream$ = fromEvent(client, platform.eventNames[1]) as Observable<Message>;
const messageProcessor = createMessageProcessor(platform.defaultPrefix, get);
const messageStream$ = sharedObservable<Message>(client, platform.eventNames[1]);
const messageProcessor = createMessageProcessor(platform.defaultPrefix, modules);
return messageStream$
.pipe(
messageProcessor,

View File

@@ -1,50 +1,27 @@
import { Subscription, fromEvent, map, of, pipe, switchMap, take } from 'rxjs';
import * as Files from '../../core/module-loading';
import { Subscription, fromEvent, of, take } from 'rxjs';
import { callInitPlugins } from './observableHandling';
import { CommandType, type ModuleStore, SernError } from '../../core/structures';
import { CommandType, SernError } from '../../core/structures';
import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler';
import type { Processed, ServerlessDependencyList, WebsocketDependencyList } from '../../types/handler';
import type { ErrorHandling, Logging, ModuleManager } from '../../core/contracts';
import { err, ok } from '../../core/functions';
import { errTap, fillDefaults } from '../../core/operators';
import SernEmitter from '../../core/sernEmitter';
import type { EventEmitter } from 'node:events';
import { DispatchType, PlatformStrategy, ServerlessStrategy, WebsocketStrategy } from '../../core';
function buildCommandModules(commandDir: string, sernEmitter: SernEmitter) {
return pipe(
switchMap(() => Files.buildModuleStream<CommandModule>(commandDir)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
map(fillDefaults),
);
}
import { sernMeta } from '../../commands';
import { buildModules } from './generic';
/**
* @overload
*/
export function makeReadyEvent(
dependencies: [
SernEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
],
dependencies: ServerlessDependencyList,
commandDir: string,
platform: ServerlessStrategy
): Subscription
export function makeReadyEvent(
dependencies: [
SernEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
EventEmitter
],
dependencies: WebsocketDependencyList,
commandDir: string,
platform: WebsocketStrategy
@@ -66,7 +43,7 @@ export function makeReadyEvent(
: fromEvent(client!, platform.eventNames[2]).pipe(take(1));
return ready$
.pipe(
buildCommandModules(commandDir, sEmitter),
buildModules(commandDir, sEmitter),
callInitPlugins({
onStop: module => {
sEmitter.emit(
@@ -90,49 +67,11 @@ export function makeReadyEvent(
function registerModule<T extends Processed<CommandModule>>(
manager: ModuleManager,
mod: T,
module: T,
): Result<void, void> {
const name = mod.name;
const insert = (cb: (ms: ModuleStore) => void) => {
const set = Result.wrap(() => manager.set(cb));
return set.ok ? ok() : err();
};
switch (mod.type) {
case CommandType.Text: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));
}
case CommandType.Slash:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod),
);
case CommandType.Both: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.BothCommands.set(name, mod));
}
case CommandType.CtxUser:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
case CommandType.CtxMsg:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod),
);
case CommandType.Button:
return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
case CommandType.StringSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
case CommandType.MentionableSelect:
return insert(ms =>
ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod),
);
case CommandType.UserSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
case CommandType.ChannelSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
case CommandType.RoleSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
case CommandType.Modal:
return insert(ms => ms.ModalSubmit.set(name, mod));
default:
return err();
const { id, fullPath } = module[sernMeta];
if(module.type === CommandType.Both || module.type === CommandType.Text) {
module.alias?.forEach(a => manager.set(`${a}__A0` , fullPath))
}
return Result.wrap(() => manager.set(id, fullPath))
}

View File

@@ -1,5 +1,4 @@
import { catchError, finalize, map, mergeAll } from 'rxjs';
import * as Files from '../../core/module-loading';
import { catchError, finalize, map, mergeAll, of } from 'rxjs';
import type { Processed, WebsocketDependencies } from '../../types/handler';
import { callInitPlugins } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module';
@@ -9,9 +8,9 @@ import type { ErrorHandling, Logging } from '../../core/contracts';
import { SernError, EventType } from '../../core/structures';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../../core/contracts/errorHandling';
import { errTap, fillDefaults } from '../../core/operators';
import { useContainerRaw } from '../../core/dependencies';
import { AnyWrapper } from '../../core/structures/wrapper';
import { buildModules } from './generic';
export function makeEventsHandler(
[s, err, log, client]: [SernEmitter, ErrorHandling, Logging | undefined, EventEmitter],
@@ -19,19 +18,6 @@ export function makeEventsHandler(
containerGetter: AnyWrapper['containerConfig'],
) {
const lazy = (k: string) => containerGetter.get(k as keyof WebsocketDependencies)[0];
const eventStream$ = eventObservable(eventsPath, s);
const eventCreation$ = eventStream$.pipe(
map(fillDefaults),
callInitPlugins({
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
);
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch (e.type) {
case EventType.Sern:
@@ -46,30 +32,29 @@ export function makeEventsHandler(
);
}
};
eventCreation$
.pipe(
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
log?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
function eventObservable(events: string, emitter: SernEmitter) {
return Files.buildModuleStream<EventModule>(events).pipe(
errTap(reason => {
emitter.emit('module.register', SernEmitter.failure(undefined, reason));
of(null).pipe(
buildModules(eventsPath, s),
callInitPlugins({
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
);
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
log?.info({ message: 'Cleaning container and crashing' });
});
}),
).subscribe();
}

View File

@@ -24,8 +24,8 @@ import { discordjs } from '../core';
* ```
*/
export function init(wrapper: DefaultWrapper) {
const dependenciesAnd = makeFetcher(wrapper.containerConfig);
const startTime = performance.now();
const dependenciesAnd = makeFetcher(wrapper.containerConfig);
const dependencies = dependenciesAnd(['@sern/modules', '@sern/client']);
if (wrapper.events !== undefined) {
makeEventsHandler(