Просмотр исходного кода

feat(core): Add static lifecycle hooks to run before bootstrap

Michael Bromley 5 лет назад
Родитель
Сommit
c92c21b0f2

+ 24 - 1
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<void> {
+        TestPluginWithAllLifecycleHooks.onBeforeBootstrapFn(app);
+    }
+
+    static beforeVendureWorkerBootstrap(app: INestMicroservice): void | Promise<void> {
+        TestPluginWithAllLifecycleHooks.onBeforeWorkerBootstrapFn(app);
+    }
+
     onVendureBootstrap(): void | Promise<void> {
         TestPluginWithAllLifecycleHooks.onBootstrapFn();
     }

+ 15 - 1
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);
     });

+ 41 - 10
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<INestApplication>;
 
@@ -51,6 +52,7 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
     });
     DefaultLogger.restoreOriginalLogLevel();
     app.useLogger(new Logger());
+    await runBeforeBootstrapHooks(config, app);
     await app.listen(port, hostname || '');
     app.enableShutdownHooks();
     if (config.workerOptions.runInMainProcess) {
@@ -101,7 +103,7 @@ export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promi
 }
 
 async function bootstrapWorkerInternal(
-    vendureConfig: ReadOnlyRequired<VendureConfig>,
+    vendureConfig: Readonly<RuntimeVendureConfig>,
 ): Promise<INestMicroservice> {
     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<VendureConfig>): 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<RuntimeVendureConfig>) {
             } 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<RuntimeVendureConfig>) {
     }
 }
 
+export async function runBeforeBootstrapHooks(config: Readonly<RuntimeVendureConfig>, 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<RuntimeVendureConfig>,
+    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<VendureConfig>): ReadOnlyRequired<VendureConfig> {
+function disableSynchronize(userConfig: Readonly<RuntimeVendureConfig>): Readonly<RuntimeVendureConfig> {
     const config = { ...userConfig };
     config.dbConnectionOptions = {
         ...userConfig.dbConnectionOptions,
@@ -311,7 +342,7 @@ function checkForDeprecatedOptions(config: Partial<VendureConfig>) {
         '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` +

+ 23 - 1
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<void>;
+
+/**
+ * @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<void>;
+
 /**
  * @description
  * A plugin which implements this interface can define logic to run when the Vendure server is initialized.

+ 8 - 2
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();