import { createInterface } from './di'; import { resourceBaseName } from './resource'; import { getMetadata, isFunction } from './utilities'; import { ErrorNames, createMappedError } from './errors'; import type { IRegistry } from './di'; import type { Constructable, IDisposable, IIndexable } from './interfaces'; import type { ResourceDefinition } from './resource'; export interface IModule { [key: string]: unknown; default?: unknown; } export interface IModuleLoader extends ModuleLoader {} export const IModuleLoader = /*@__PURE__*/createInterface(x => x.singleton(ModuleLoader)); const noTransform = (m: AnalyzedModule): TRet => m as unknown as TRet; type TransformFn> = (m: AnalyzedModule) => TRet | Promise; class ModuleTransformer> { private readonly _promiseCache: Map, unknown> = new Map, unknown>(); private readonly _objectCache: Map = new Map(); private readonly _transform: TransformFn; public constructor( transform: TransformFn, ) { this._transform = transform; } public transform(objOrPromise: TMod | Promise): Promise | TRet { if (objOrPromise instanceof Promise) { return this._transformPromise(objOrPromise); } else if (typeof objOrPromise === 'object' && objOrPromise !== null) { return this._transformObject(objOrPromise); } else { throw createMappedError(ErrorNames.invalid_module_transform_input, objOrPromise); } } /** @internal */ private _transformPromise(promise: Promise): TRet | Promise { if (this._promiseCache.has(promise)) { return this._promiseCache.get(promise) as TRet | Promise; } const ret = promise.then(obj => { return this._transformObject(obj); }); this._promiseCache.set(promise, ret); void ret.then(value => { // make it synchronous for future requests this._promiseCache.set(promise, value); }); return ret; } /** @internal */ private _transformObject(obj: TMod): TRet | Promise { if (this._objectCache.has(obj)) { return this._objectCache.get(obj) as TRet | Promise; } const ret = this._transform(this._analyze(obj)); this._objectCache.set(obj, ret); if (ret instanceof Promise) { void ret.then(value => { // make it synchronous for future requests this._objectCache.set(obj, value); }); } return ret; } /** @internal */ private _analyze(m: TMod): AnalyzedModule { if (m == null) throw createMappedError(ErrorNames.invalid_module_transform_input, m); if (typeof m !== 'object') return new AnalyzedModule(m, []); let value: unknown; let isRegistry: boolean; let isConstructable: boolean; let definition: ResourceDefinition | null; const items: ModuleItem[] = []; for (const key in m) { switch (typeof (value = m[key])) { case 'object': if (value === null) { continue; } isRegistry = isFunction((value as IIndexable).register); isConstructable = false; definition = null; break; case 'function': isRegistry = isFunction((value as Constructable & IIndexable).register); isConstructable = (value as Constructable).prototype !== void 0; definition = getMetadata(resourceBaseName, value) ?? null; break; default: continue; } items.push(new ModuleItem( key, value, isRegistry, isConstructable, definition, )); } return new AnalyzedModule(m, items as ITypedModuleItem_T[]); } } export class ModuleLoader implements IDisposable { private readonly transformers: Map = new Map(); /** * Await a module promise and then analyzes and transforms it. The result is cached, using the transform function + promise as the cache key. * * @param promise - A promise (returning a module, or an object resembling a module), e.g. the return value of a dynamic `import()` or `require()` call. * @param transform - A transform function, e.g. to select the appropriate default or first non-default resource export. * Note: The return value of `transform` is cached, so it is recommended to perform any processing here that is intended to happen only once per unique module promise. * * @returns The (cached) transformed result. On subsequent calls, if the original promise resolved, the resolved result will be returned (rather than a promise). */ public load>(promise: Promise, transform: (m: AnalyzedModule) => TRet | Promise): Promise | TRet; /** * Await a module promise and then analyzes it. The result is cached, using the transform function + promise as the cache key. * * @param promise - A promise (returning a module, or an object resembling a module), e.g. the return value of a dynamic `import()` or `require()` call. * * @returns The analyzed module. On subsequent calls, if the original promise resolved, the resolved result will be returned (rather than a promise). */ public load(promise: Promise): Promise> | AnalyzedModule; /** * Analyzes and transforms a module-like object. The result is cached, using the transform function + object as the cache key. * * @param promise - A module-like object, e.g. the awaited return value of a dynamic `import()` or `require()` call, or a statically imported module such as `import * as Module from './my-module';`. * @param transform - A transform function, e.g. to select the appropriate default or first non-default resource export. * Note: The return value of `transform` is cached, so it is recommended to perform any processing here that is intended to happen only once per unique module promise. * * @returns The (cached) transformed result. On subsequent calls, if the original promise resolved, the resolved result will be returned (rather than a promise). */ public load>(obj: TMod, transform: (m: AnalyzedModule) => TRet | Promise): Promise | TRet; /** * Analyzes a module-like object. The result is cached, using the transform function + object as the cache key. * * @param promise - A module-like object, e.g. the awaited return value of a dynamic `import()` or `require()` call, or a statically imported module such as `import * as Module from './my-module';`. * * @returns The analyzed module. On subsequent calls, if the original promise resolved, the resolved result will be returned (rather than a promise). */ public load(obj: TMod): AnalyzedModule; /** * Analyzes and transforms a module-like object or a promise thereof. The result is cached, using the transform function + object as the cache key. * * @param promise - A module-like object or a promise thereof, e.g. the (awaited) return value of a dynamic `import()` or `require()` call, or a statically imported module such as `import * as Module from './my-module';`. * @param transform - A transform function, e.g. to select the appropriate default or first non-default resource export. * Note: The return value of `transform` is cached, so it is recommended to perform any processing here that is intended to happen only once per unique module promise. * * @returns The (cached) transformed result. On subsequent calls, if the original promise resolved, the resolved result will be returned (rather than a promise). */ public load>(objOrPromise: TMod | Promise, transform?: (m: AnalyzedModule) => TRet | Promise): Promise | TRet; public load>(objOrPromise: TMod | Promise, transform: (m: AnalyzedModule) => TRet | Promise = noTransform): Promise | TRet { const transformers = this.transformers as Map & Map, ModuleTransformer>; let transformer = transformers.get(transform); if (transformer === void 0) { transformers.set(transform, transformer = new ModuleTransformer(transform)); } return transformer.transform(objOrPromise); } public dispose(): void { this.transformers.clear(); } } export class AnalyzedModule { public constructor( public readonly raw: TMod, public readonly items: readonly ITypedModuleItem_T[], ) {} } export interface ITypedModuleItem< TisRegistry extends boolean, TisConstructable extends boolean, TValue > { readonly key: string; readonly value: TValue; readonly isRegistry: TisRegistry; readonly isConstructable: TisConstructable; readonly definition: ResourceDefinition | null; } export interface ITypedModuleItem_Unknown extends ITypedModuleItem {} export interface ITypedModuleItem_Registry extends ITypedModuleItem {} export interface ITypedModuleItem_Constructable extends ITypedModuleItem {} export interface ITypedModuleItem_ConstructableRegistry extends ITypedModuleItem {} export type ITypedModuleItem_T = ( ITypedModuleItem_Unknown | ITypedModuleItem_Registry | ITypedModuleItem_Constructable | ITypedModuleItem_ConstructableRegistry ); export class ModuleItem { public constructor( public readonly key: string, public readonly value: unknown, public readonly isRegistry: boolean, public readonly isConstructable: boolean, public readonly definition: ResourceDefinition | null, ) {} } /** * Iterate through the exports of a module and register aliases for resources respectively */ export const aliasedResourcesRegistry = (mod: IModule, mainKeyAlias: string | null | undefined, aliases: Record = {}): IRegistry => { return { register(container) { const analyzedModule = container.get(IModuleLoader).load(mod); let mainAliasRegistered = false; analyzedModule.items.forEach((item) => { const definition = item.definition; if (definition == null) { container.register(item.value); return; } if (!mainAliasRegistered && mainKeyAlias != null) { mainAliasRegistered = true; definition.register(container, mainKeyAlias); return; } // cannot use item.key, since it could contain an uppercase letter // while if import as is used in html, then it'll be lowercase letters only // using definition name, however, comes with an issue, which is that it's not guaranteed to be unique // // for example: a module can export both an element and an attribute with the name "foo" // but if that's the case, devs can always split the exports into two modules const alias = aliases[definition.name]; definition.register(container, alias); }); }, }; }; // or extract the registry part into a class? // // class AliasModuleKeysRegistry implements IRegistry { // /** @internal */ private readonly _mod: IModule; // /** @internal */ private readonly _mainKeyAlias: string | null; // /** @internal */ private readonly _otherAliases: Record; // public constructor( // mod: IModule, // mainKeyAlias: string | null, // aliases: Record, // ) { // this._mod = mod; // this._mainKeyAlias = mainKeyAlias; // this._otherAliases = aliases; // } // /** @internal */ // private _getAliasedKeyForName(key: string, name: string): string { // // replace the part after the last : with the name // const parts = key.split(':'); // parts[parts.length - 1] = name; // return parts.join(':'); // } // public register(container: IContainer) { // const analyzedModule = container.get(IModuleLoader).load(this._mod); // let mainAliasRegistered = false; // analyzedModule.items.forEach((item) => { // const definition = item.definition; // if (definition == null) { // container.register(item.value); // return; // } // if (!mainAliasRegistered && this._mainKeyAlias != null) { // mainAliasRegistered = true; // aliasToRegistration(definition.key, this._mainKeyAlias).register(container); // return; // } // for (const aliasedExport in this._otherAliases) { // const aliasName = this._otherAliases[aliasedExport]; // const aliasKey = this._getAliasedKeyForName(definition.key, aliasName); // if (item.key === aliasedExport) { // aliasToRegistration(definition.key, aliasKey).register(container); // } // } // }); // } // }