diff --git a/packages/core/src/dependency-injector.ts b/packages/core/src/dependency-injector.ts index b695f05..31cf059 100644 --- a/packages/core/src/dependency-injector.ts +++ b/packages/core/src/dependency-injector.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import { awaitAllEntries, isPlainObject, mapObject } from '@stilt/util'; +import { awaitMapAllEntries, isPlainObject, FORCE_SEQUENTIAL_MODULE_IMPORT } from '@stilt/util'; import type { Factory } from './factory'; import { isFactory } from './factory.js'; import type { TOptionalLazy } from './lazy'; @@ -65,6 +65,7 @@ export default class DependencyInjector { // dependencies: { myService: lazy(() => MyService) } | { [key: string]: TOptionalLazy> }, ): Promise { + // @ts-expect-error return this._getInstances(moduleFactory, []); } @@ -88,21 +89,17 @@ export default class DependencyInjector { return this._getInstance(moduleFactory, dependencyChain); } - if (Array.isArray(moduleFactory)) { - return Promise.all( - moduleFactory.map(async ClassItem => this._getInstance(ClassItem, dependencyChain)), - ); - } - - if (typeof moduleFactory === 'object' && isPlainObject(moduleFactory)) { - return awaitAllEntries(mapObject(moduleFactory, async (aClass: TOptionalLazy>, key: string) => { + if (Array.isArray(moduleFactory) || typeof moduleFactory === 'object' && isPlainObject(moduleFactory)) { + // @ts-expect-error + return awaitMapAllEntries(moduleFactory, async (ClassItem, key) => { try { - return await this._getInstance(aClass, dependencyChain); + // @ts-expect-error + return await this._getInstance(ClassItem, dependencyChain); } catch (e) { - // TODO: use .causedBy + // TODO: use { cause: e } throw new Error(`Failed to build ${key}: \n ${e.message}`); } - })); + }, FORCE_SEQUENTIAL_MODULE_IMPORT); } assert(typeof moduleFactory === 'function'); diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index f0f8c00..83e0274 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -3,7 +3,7 @@ import path from 'path'; import type { InjectableIdentifier, TRunnable } from '@stilt/core'; import { App, factory, isRunnable, runnable } from '@stilt/core'; import { StiltHttp } from '@stilt/http'; -import { asyncGlob, coalesce } from '@stilt/util'; +import { asyncGlob, awaitMapAllEntries, coalesce, FORCE_SEQUENTIAL_MODULE_IMPORT } from '@stilt/util'; import type { GraphQLNamedType, Source, DocumentNode, @@ -207,7 +207,7 @@ export class StiltGraphQl { * - A GraphQL type (eg. a GraphQL enum, any export) */ - const resolverExports = await Promise.all(resolverFiles.map(readOrRequireFile)); + const resolverExports = await awaitMapAllEntries(resolverFiles, readOrRequireFile, FORCE_SEQUENTIAL_MODULE_IMPORT); const resolverInstancePromises = []; for (const resolverExport of resolverExports) { diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index b5124a4..115dcc3 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -3,7 +3,7 @@ import { App, factory, isRunnable, runnable } from '@stilt/core'; import type { Class } from '@stilt/core/types/typing'; import { StiltHttp } from '@stilt/http'; import { wrapControllerWithInjectors } from '@stilt/http/dist/controllerInjectors.js'; -import { asyncGlob } from '@stilt/util'; +import { asyncGlob, awaitMapAllEntries, FORCE_SEQUENTIAL_MODULE_IMPORT } from '@stilt/util'; import { getRoutingMetadata } from './HttpMethodsDecorators.js'; import { IsRestError } from './RestError.js'; @@ -145,28 +145,24 @@ export class StiltRest { private static async loadControllers(app: App, schemaGlob: string) { const controllers = await asyncGlob(schemaGlob); - const apiClasses = (await Promise.all( - Object.values(controllers).map(async controllerPath => { - const controllerModule = await import(controllerPath); - const controllerClass = controllerModule.default; + const apiClasses = (await awaitMapAllEntries(Object.values(controllers), async controllerPath => { + const controllerModule = await import(controllerPath); + const controllerClass = controllerModule.default; - if (controllerClass == null || (typeof controllerClass !== 'function' && typeof controllerClass !== 'object')) { - return null; - } + if (controllerClass == null || (typeof controllerClass !== 'function' && typeof controllerClass !== 'object')) { + return null; + } - return controllerClass; - }), - )).filter(controllerClass => controllerClass != null); + return controllerClass; + }, FORCE_SEQUENTIAL_MODULE_IMPORT)).filter(controllerClass => controllerClass != null); - const apiInstances = await Promise.all( - apiClasses.map(async resolverClass => { - if (typeof resolverClass === 'function') { - return app.instantiate(resolverClass); - } + const apiInstances = (await awaitMapAllEntries(apiClasses, async resolverClass => { + if (typeof resolverClass === 'function') { + return app.instantiate(resolverClass); + } - return null; - }), - ); + return null; + }, FORCE_SEQUENTIAL_MODULE_IMPORT)).filter(controllerInstance => controllerInstance != null); const routeHandlers = [...apiClasses, ...apiInstances]; diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 2532c39..014a35f 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -10,7 +10,7 @@ export function hasOwnProperty( return Object.prototype.hasOwnProperty.call(obj, propertyKey); } -export function isPlainObject(obj: any): obj is Object { +export function isPlainObject(obj: any): obj is object { const proto = Object.getPrototypeOf(obj); return proto == null || proto === Object.prototype; @@ -56,9 +56,16 @@ export function coalesce(...args: T[]): T { } type UnwrapPromise = T extends Promise ? U : T; - -export async function awaitAllEntries(obj: T): Promise<{ [P in keyof T]: UnwrapPromise }> { +type MaybePromise = Promise | T; + +// eslint-disable-next-line max-len +export async function awaitAllEntries }>(obj: T): Promise<{ [P in keyof T]: UnwrapPromise }>; +export async function awaitAllEntries(obj: Array>): Promise; +// eslint-disable-next-line max-len +export async function awaitAllEntries })>(obj: T | Array>): Promise<{ [P in keyof T]: UnwrapPromise } | In[]> { + if (Array.isArray(obj)) { + return Promise.all(obj); + } const values = await Promise.all(Object.values(obj)); const keys = Object.keys(obj); @@ -73,7 +80,7 @@ export async function awaitAllEntries( +export function mapObject( obj: T, callback: (value: In, key: string) => Out, ): { [P in keyof T]: Out } { @@ -88,6 +95,71 @@ export function mapObject( return newObject; } +export function mapEntries( + obj: T, + callback: (value: In, key: string | number) => Out +): { [P in keyof T]: Out }; + +export function mapEntries( + obj: In[], + callback: (value: In, key: string | number) => Out +): Out[]; + +export function mapEntries( + obj: T | In[], + callback: (value: In, key: string | number) => Out): { [P in keyof T]: Out } | Out[] { + // process.env.JEST_WORKER_ID + if (Array.isArray(obj)) { + return obj.map((value, key) => callback(value, key)); + } + + return mapObject(obj, callback); +} + +// eslint-disable-next-line max-len +export async function awaitMapAllEntries(obj: T, callback: (value: In, key: string | number) => MaybePromise, sequential?: boolean): Promise<{ [P in keyof T]: Out }>; +// eslint-disable-next-line max-len +export async function awaitMapAllEntries(obj: In[], callback: (value: In, key: string | number) => MaybePromise, sequential?: boolean): Promise; +// eslint-disable-next-line max-len +export async function awaitMapAllEntries( + obj: T | In[], + callback: (value: In, key: string | number) => MaybePromise, + sequential?: boolean): Promise<{ [P in keyof T]: Out } | Out[]> { + + // `sequential` option is a workaround due to a bug when using Jest with native ES Modules + // See https://github.com/facebook/jest/issues/11434 + if (sequential) { + if (Array.isArray(obj)) { + const out = []; + + for (let i = 0; i < obj.length; i++){ + // eslint-disable-next-line no-await-in-loop + out.push(await callback(obj[i], i)); + } + + return out; + } + + const out = {}; + for (const key of Object.keys(obj)) { + // eslint-disable-next-line no-await-in-loop + out[key] = await callback(obj[key], key); + } + + // @ts-expect-error + return out; + } + + // @ts-expect-error + const mapped = mapEntries(obj, callback); + const out = awaitAllEntries(mapped); + + // @ts-expect-error + return out; +} + export function assertIsFunction(item: any): asserts item is Function { assert(typeof item === 'function'); } + +export const FORCE_SEQUENTIAL_MODULE_IMPORT = process.env.JEST_WORKER_ID != null;