diff --git a/packages/core/e2e/fixtures/test-plugins/with-all-lifecycle-hooks.ts b/packages/core/e2e/fixtures/test-plugins/with-all-lifecycle-hooks.ts index f2da9e7bf1..35a79fc471 100644 --- a/packages/core/e2e/fixtures/test-plugins/with-all-lifecycle-hooks.ts +++ b/packages/core/e2e/fixtures/test-plugins/with-all-lifecycle-hooks.ts @@ -1,4 +1,7 @@ +import { INestApplication, INestMicroservice } from '@nestjs/common'; import { + BeforeVendureBootstrap, + BeforeVendureWorkerBootstrap, OnVendureBootstrap, OnVendureClose, OnVendureWorkerBootstrap, @@ -6,8 +9,16 @@ import { } from '@vendure/core'; export class TestPluginWithAllLifecycleHooks - implements OnVendureBootstrap, OnVendureWorkerBootstrap, OnVendureClose, OnVendureWorkerClose { + implements + BeforeVendureBootstrap, + BeforeVendureWorkerBootstrap, + OnVendureBootstrap, + OnVendureWorkerBootstrap, + OnVendureClose, + OnVendureWorkerClose { private static onConstructorFn: any; + private static onBeforeBootstrapFn: any; + private static onBeforeWorkerBootstrapFn: any; private static onBootstrapFn: any; private static onWorkerBootstrapFn: any; private static onCloseFn: any; @@ -15,12 +26,16 @@ export class TestPluginWithAllLifecycleHooks static init( constructorFn: any, + beforeBootstrapFn: any, + beforeWorkerBootstrapFn: any, bootstrapFn: any, workerBootstrapFn: any, closeFn: any, workerCloseFn: any, ) { this.onConstructorFn = constructorFn; + this.onBeforeBootstrapFn = beforeBootstrapFn; + this.onBeforeWorkerBootstrapFn = beforeWorkerBootstrapFn; this.onBootstrapFn = bootstrapFn; this.onWorkerBootstrapFn = workerBootstrapFn; this.onCloseFn = closeFn; @@ -32,6 +47,14 @@ export class TestPluginWithAllLifecycleHooks TestPluginWithAllLifecycleHooks.onConstructorFn(); } + static beforeVendureBootstrap(app: INestApplication): void | Promise { + TestPluginWithAllLifecycleHooks.onBeforeBootstrapFn(app); + } + + static beforeVendureWorkerBootstrap(app: INestMicroservice): void | Promise { + TestPluginWithAllLifecycleHooks.onBeforeWorkerBootstrapFn(app); + } + onVendureBootstrap(): void | Promise { TestPluginWithAllLifecycleHooks.onBootstrapFn(); } diff --git a/packages/core/e2e/plugin.e2e-spec.ts b/packages/core/e2e/plugin.e2e-spec.ts index c59cd8de5e..ccbccaaebf 100644 --- a/packages/core/e2e/plugin.e2e-spec.ts +++ b/packages/core/e2e/plugin.e2e-spec.ts @@ -5,7 +5,7 @@ import gql from 'graphql-tag'; import path from 'path'; import { initialData } from '../../../e2e-common/e2e-initial-data'; -import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config'; +import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; import { TestPluginWithAllLifecycleHooks } from './fixtures/test-plugins/with-all-lifecycle-hooks'; import { TestAPIExtensionPlugin } from './fixtures/test-plugins/with-api-extensions'; @@ -18,6 +18,8 @@ import { TestProcessContextPlugin } from './fixtures/test-plugins/with-worker-co describe('Plugins', () => { const bootstrapMockFn = jest.fn(); const onConstructorFn = jest.fn(); + const beforeBootstrapFn = jest.fn(); + const beforeWorkerBootstrapFn = jest.fn(); const onBootstrapFn = jest.fn(); const onWorkerBootstrapFn = jest.fn(); const onCloseFn = jest.fn(); @@ -28,6 +30,8 @@ describe('Plugins', () => { plugins: [ TestPluginWithAllLifecycleHooks.init( onConstructorFn, + beforeBootstrapFn, + beforeWorkerBootstrapFn, onBootstrapFn, onWorkerBootstrapFn, onCloseFn, @@ -59,6 +63,16 @@ describe('Plugins', () => { expect(onConstructorFn).toHaveBeenCalledTimes(2); }); + it('calls beforeVendureBootstrap', () => { + expect(beforeBootstrapFn).toHaveBeenCalledTimes(1); + expect(beforeBootstrapFn).toHaveBeenCalledWith(server.app); + }); + + it('calls beforeVendureWorkerBootstrap', () => { + expect(beforeWorkerBootstrapFn).toHaveBeenCalledTimes(1); + expect(beforeWorkerBootstrapFn).toHaveBeenCalledWith(server.worker); + }); + it('calls onVendureBootstrap', () => { expect(onBootstrapFn).toHaveBeenCalledTimes(1); }); diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index 1e47383fbc..ed62a2cf7a 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -16,6 +16,7 @@ import { setEntityIdStrategy } from './entity/set-entity-id-strategy'; import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config'; import { getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata'; import { getProxyMiddlewareCliGreetings } from './plugin/plugin-utils'; +import { BeforeVendureBootstrap, BeforeVendureWorkerBootstrap } from './plugin/vendure-plugin'; export type VendureBootstrapFunction = (config: VendureConfig) => Promise; @@ -51,6 +52,7 @@ export async function bootstrap(userConfig: Partial): Promise): Promi } async function bootstrapWorkerInternal( - vendureConfig: ReadOnlyRequired, + vendureConfig: Readonly, ): Promise { const config = disableSynchronize(vendureConfig); if (!config.workerOptions.runInMainProcess && (config.logger as any).setDefaultContext) { @@ -120,7 +122,7 @@ async function bootstrapWorkerInternal( DefaultLogger.restoreOriginalLogLevel(); workerApp.useLogger(new Logger()); workerApp.enableShutdownHooks(); - + await runBeforeWorkerBootstrapHooks(config, workerApp); // A work-around to correctly handle errors when attempting to start the // microservice server listening. // See https://github.com/nestjs/nest/issues/2777 @@ -196,7 +198,7 @@ export async function getAllEntities(userConfig: Partial): Promis // Check to ensure that no plugins are defining entities with names // which conflict with existing entities. for (const pluginEntity of pluginEntities) { - if (allEntities.find(e => e.name === pluginEntity.name)) { + if (allEntities.find((e) => e.name === pluginEntity.name)) { throw new InternalServerError(`error.entity-name-conflict`, { entityName: pluginEntity.name }); } else { allEntities.push(pluginEntity); @@ -221,7 +223,7 @@ function setExposedHeaders(config: Readonly) { } else if (typeof exposedHeaders === 'string') { exposedHeadersWithAuthKey = exposedHeaders .split(',') - .map(x => x.trim()) + .map((x) => x.trim()) .concat(authTokenHeaderKey); } else { exposedHeadersWithAuthKey = exposedHeaders.concat(authTokenHeaderKey); @@ -231,6 +233,35 @@ function setExposedHeaders(config: Readonly) { } } +export async function runBeforeBootstrapHooks(config: Readonly, app: INestApplication) { + function hasBeforeBootstrapHook( + plugin: any, + ): plugin is { beforeVendureBootstrap: BeforeVendureBootstrap } { + return typeof plugin.beforeVendureBootstrap === 'function'; + } + for (const plugin of config.plugins) { + if (hasBeforeBootstrapHook(plugin)) { + await plugin.beforeVendureBootstrap(app); + } + } +} + +export async function runBeforeWorkerBootstrapHooks( + config: Readonly, + worker: INestMicroservice, +) { + function hasBeforeBootstrapHook( + plugin: any, + ): plugin is { beforeVendureWorkerBootstrap: BeforeVendureWorkerBootstrap } { + return typeof plugin.beforeVendureWorkerBootstrap === 'function'; + } + for (const plugin of config.plugins) { + if (hasBeforeBootstrapHook(plugin)) { + await plugin.beforeVendureWorkerBootstrap(worker); + } + } +} + /** * Monkey-patches the app's .close() method to also close the worker microservice * instance too. @@ -273,25 +304,25 @@ function logWelcomeMessage(config: RuntimeVendureConfig) { apiCliGreetings.push(...getProxyMiddlewareCliGreetings(config)); const columnarGreetings = arrangeCliGreetingsInColumns(apiCliGreetings); const title = `Vendure server (v${version}) now running on port ${port}`; - const maxLineLength = Math.max(title.length, ...columnarGreetings.map(l => l.length)); + const maxLineLength = Math.max(title.length, ...columnarGreetings.map((l) => l.length)); const titlePadLength = title.length < maxLineLength ? Math.floor((maxLineLength - title.length) / 2) : 0; Logger.info(`=`.repeat(maxLineLength)); Logger.info(title.padStart(title.length + titlePadLength)); Logger.info('-'.repeat(maxLineLength).padStart(titlePadLength)); - columnarGreetings.forEach(line => Logger.info(line)); + columnarGreetings.forEach((line) => Logger.info(line)); Logger.info(`=`.repeat(maxLineLength)); } function arrangeCliGreetingsInColumns(lines: Array<[string, string]>): string[] { - const columnWidth = Math.max(...lines.map(l => l[0].length)) + 2; - return lines.map(l => `${(l[0] + ':').padEnd(columnWidth)}${l[1]}`); + const columnWidth = Math.max(...lines.map((l) => l[0].length)) + 2; + return lines.map((l) => `${(l[0] + ':').padEnd(columnWidth)}${l[1]}`); } /** * Fix race condition when modifying DB * See: https://github.com/vendure-ecommerce/vendure/issues/152 */ -function disableSynchronize(userConfig: ReadOnlyRequired): ReadOnlyRequired { +function disableSynchronize(userConfig: Readonly): Readonly { const config = { ...userConfig }; config.dbConnectionOptions = { ...userConfig.dbConnectionOptions, @@ -311,7 +342,7 @@ function checkForDeprecatedOptions(config: Partial) { 'middleware', 'apolloServerPlugins', ]; - const deprecatedOptionsUsed = deprecatedApiOptions.filter(option => config.hasOwnProperty(option)); + const deprecatedOptionsUsed = deprecatedApiOptions.filter((option) => config.hasOwnProperty(option)); if (deprecatedOptionsUsed.length) { throw new Error( `The following VendureConfig options are deprecated: ${deprecatedOptionsUsed.join(', ')}\n` + diff --git a/packages/core/src/plugin/vendure-plugin.ts b/packages/core/src/plugin/vendure-plugin.ts index 4cd33f2077..4e3bf0844f 100644 --- a/packages/core/src/plugin/vendure-plugin.ts +++ b/packages/core/src/plugin/vendure-plugin.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { INestApplication, INestMicroservice, Module } from '@nestjs/common'; import { MODULE_METADATA } from '@nestjs/common/constants'; import { ModuleMetadata } from '@nestjs/common/interfaces'; import { pick } from '@vendure/common/lib/pick'; @@ -139,6 +139,28 @@ export function VendurePlugin(pluginMetadata: VendurePluginMetadata): ClassDecor }; } +/** + * @description + * A plugin which implements a static `beforeVendureBootstrap` method with this type can define logic to run + * before the Vendure server (and the underlying Nestjs application) is bootstrapped. This is called + * _after_ the Nestjs application has been created, but _before_ the `app.listen()` method is invoked. + * + * @docsCategory plugin + * @docsPage Plugin Lifecycle Methods + */ +export type BeforeVendureBootstrap = (app: INestApplication) => void | Promise; + +/** + * @description + * A plugin which implements a static `beforeVendureWorkerBootstrap` method with this type can define logic to run + * before the Vendure worker (and the underlying Nestjs microservice) is bootstrapped. This is called + * _after_ the Nestjs microservice has been created, but _before_ the `microservice.listen()` method is invoked. + * + * @docsCategory plugin + * @docsPage Plugin Lifecycle Methods + */ +export type BeforeVendureWorkerBootstrap = (app: INestMicroservice) => void | Promise; + /** * @description * A plugin which implements this interface can define logic to run when the Vendure server is initialized. diff --git a/packages/testing/src/test-server.ts b/packages/testing/src/test-server.ts index 8f01328bbc..d1fad46948 100644 --- a/packages/testing/src/test-server.ts +++ b/packages/testing/src/test-server.ts @@ -1,7 +1,11 @@ import { INestApplication, INestMicroservice } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DefaultLogger, Logger, VendureConfig } from '@vendure/core'; -import { preBootstrapConfig } from '@vendure/core/dist/bootstrap'; +import { + preBootstrapConfig, + runBeforeBootstrapHooks, + runBeforeWorkerBootstrapHooks, +} from '@vendure/core/dist/bootstrap'; import { populateForTesting } from './data-population/populate-for-testing'; import { getInitializerFor } from './initializers/initializers'; @@ -71,7 +75,7 @@ export class TestServer { */ async destroy() { // allow a grace period of any outstanding async tasks to complete - await new Promise(resolve => global.setTimeout(resolve, 500)); + await new Promise((resolve) => global.setTimeout(resolve, 500)); await this.app.close(); if (this.worker) { await this.worker.close(); @@ -134,6 +138,7 @@ export class TestServer { logger: new Logger(), }); let worker: INestMicroservice | undefined; + await runBeforeBootstrapHooks(config, app); await app.listen(config.apiOptions.port); if (config.workerOptions.runInMainProcess) { const workerModule = await import('@vendure/core/dist/worker/worker.module'); @@ -142,6 +147,7 @@ export class TestServer { logger: new Logger(), options: config.workerOptions.options, }); + await runBeforeWorkerBootstrapHooks(config, worker); await worker.listenAsync(); } DefaultLogger.restoreOriginalLogLevel();