bootstrap.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { INestApplication, INestMicroservice } from '@nestjs/common';
  2. import { NestFactory } from '@nestjs/core';
  3. import { TcpClientOptions, Transport } from '@nestjs/microservices';
  4. import { Type } from '@vendure/common/lib/shared-types';
  5. import { EntitySubscriberInterface } from 'typeorm';
  6. import { InternalServerError } from './common/error/errors';
  7. import { ReadOnlyRequired } from './common/types/common-types';
  8. import { getConfig, setConfig } from './config/config-helpers';
  9. import { DefaultLogger } from './config/logger/default-logger';
  10. import { Logger } from './config/logger/vendure-logger';
  11. import { VendureConfig } from './config/vendure-config';
  12. import { registerCustomEntityFields } from './entity/register-custom-entity-fields';
  13. import { logProxyMiddlewares } from './plugin/plugin-utils';
  14. export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestApplication>;
  15. /**
  16. * @description
  17. * Bootstraps the Vendure server. This is the entry point to the application.
  18. *
  19. * @example
  20. * ```TypeScript
  21. * import { bootstrap } from '\@vendure/core';
  22. * import { config } from './vendure-config';
  23. *
  24. * bootstrap(config).catch(err => {
  25. * console.log(err);
  26. * });
  27. * ```
  28. * @docsCategory
  29. * @docsWeight 0
  30. */
  31. export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INestApplication> {
  32. const config = await preBootstrapConfig(userConfig);
  33. Logger.useLogger(config.logger);
  34. Logger.info(`Bootstrapping Vendure Server (pid: ${process.pid})...`);
  35. // The AppModule *must* be loaded only after the entities have been set in the
  36. // config, so that they are available when the AppModule decorator is evaluated.
  37. // tslint:disable-next-line:whitespace
  38. const appModule = await import('./app.module');
  39. DefaultLogger.hideNestBoostrapLogs();
  40. const app = await NestFactory.create(appModule.AppModule, {
  41. cors: config.cors,
  42. logger: new Logger(),
  43. });
  44. DefaultLogger.restoreOriginalLogLevel();
  45. app.useLogger(new Logger());
  46. await runPluginOnBootstrapMethods(config, app);
  47. await app.listen(config.port, config.hostname);
  48. app.enableShutdownHooks();
  49. if (config.workerOptions.runInMainProcess) {
  50. const worker = await bootstrapWorkerInternal(config);
  51. Logger.warn(`Worker is running in main process. This is not recommended for production.`);
  52. Logger.warn(`[VendureConfig.workerOptions.runInMainProcess = true]`);
  53. closeWorkerOnAppClose(app, worker);
  54. }
  55. logWelcomeMessage(config);
  56. return app;
  57. }
  58. /**
  59. * @description
  60. * Bootstraps the Vendure worker. Read more about the [Vendure Worker]({{< relref "vendure-worker" >}}) or see the worker-specific options
  61. * defined in {@link WorkerOptions}.
  62. *
  63. * @example
  64. * ```TypeScript
  65. * import { bootstrapWorker } from '\@vendure/core';
  66. * import { config } from './vendure-config';
  67. *
  68. * bootstrapWorker(config).catch(err => {
  69. * console.log(err);
  70. * });
  71. * ```
  72. * @docsCategory worker
  73. * @docsWeight 0
  74. */
  75. export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<INestMicroservice> {
  76. if (userConfig.workerOptions && userConfig.workerOptions.runInMainProcess === true) {
  77. Logger.useLogger(userConfig.logger || new DefaultLogger());
  78. const errorMessage = `Cannot bootstrap worker when "runInMainProcess" is set to true`;
  79. Logger.error(errorMessage, 'Vendure Worker');
  80. throw new Error(errorMessage);
  81. } else {
  82. return bootstrapWorkerInternal(userConfig);
  83. }
  84. }
  85. async function bootstrapWorkerInternal(userConfig: Partial<VendureConfig>): Promise<INestMicroservice> {
  86. const config = await preBootstrapConfig(userConfig);
  87. if (!config.workerOptions.runInMainProcess && (config.logger as any).setDefaultContext) {
  88. (config.logger as any).setDefaultContext('Vendure Worker');
  89. }
  90. Logger.useLogger(config.logger);
  91. Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
  92. const workerModule = await import('./worker/worker.module');
  93. DefaultLogger.hideNestBoostrapLogs();
  94. const workerApp = await NestFactory.createMicroservice(workerModule.WorkerModule, {
  95. transport: config.workerOptions.transport,
  96. logger: new Logger(),
  97. options: config.workerOptions.options,
  98. });
  99. DefaultLogger.restoreOriginalLogLevel();
  100. workerApp.useLogger(new Logger());
  101. workerApp.enableShutdownHooks();
  102. await workerApp.listenAsync();
  103. workerWelcomeMessage(config);
  104. return workerApp;
  105. }
  106. /**
  107. * Setting the global config must be done prior to loading the AppModule.
  108. */
  109. export async function preBootstrapConfig(
  110. userConfig: Partial<VendureConfig>,
  111. ): Promise<ReadOnlyRequired<VendureConfig>> {
  112. if (userConfig) {
  113. setConfig(userConfig);
  114. }
  115. // Entities *must* be loaded after the user config is set in order for the
  116. // base VendureEntity to be correctly configured with the primary key type
  117. // specified in the EntityIdStrategy.
  118. // tslint:disable-next-line:whitespace
  119. const pluginEntities = getEntitiesFromPlugins(userConfig);
  120. const entities = await getAllEntities(userConfig);
  121. const { coreSubscribersMap } = await import('./entity/subscribers');
  122. setConfig({
  123. dbConnectionOptions: {
  124. entities,
  125. subscribers: Object.values(coreSubscribersMap) as Array<Type<EntitySubscriberInterface>>,
  126. },
  127. });
  128. let config = getConfig();
  129. config = await runPluginConfigurations(config);
  130. registerCustomEntityFields(config);
  131. return config;
  132. }
  133. /**
  134. * Initialize any configured plugins.
  135. */
  136. async function runPluginConfigurations(
  137. config: ReadOnlyRequired<VendureConfig>,
  138. ): Promise<ReadOnlyRequired<VendureConfig>> {
  139. for (const plugin of config.plugins) {
  140. if (plugin.configure) {
  141. config = (await plugin.configure(config)) as ReadOnlyRequired<VendureConfig>;
  142. }
  143. }
  144. return config;
  145. }
  146. /**
  147. * Run the onBootstrap() method of any configured plugins.
  148. */
  149. export async function runPluginOnBootstrapMethods(
  150. config: ReadOnlyRequired<VendureConfig>,
  151. app: INestApplication,
  152. ): Promise<void> {
  153. function inject<T>(type: Type<T>): T {
  154. return app.get(type);
  155. }
  156. for (const plugin of config.plugins) {
  157. if (plugin.onBootstrap) {
  158. await plugin.onBootstrap(inject);
  159. const pluginName = plugin.constructor && plugin.constructor.name || '(anonymous plugin)';
  160. Logger.verbose(`Bootstrapped plugin ${pluginName}`);
  161. }
  162. }
  163. }
  164. /**
  165. * Returns an array of core entities and any additional entities defined in plugins.
  166. */
  167. async function getAllEntities(userConfig: Partial<VendureConfig>): Promise<Array<Type<any>>> {
  168. const { coreEntitiesMap } = await import('./entity/entities');
  169. const coreEntities = Object.values(coreEntitiesMap) as Array<Type<any>>;
  170. const pluginEntities = getEntitiesFromPlugins(userConfig);
  171. const allEntities: Array<Type<any>> = coreEntities;
  172. // Check to ensure that no plugins are defining entities with names
  173. // which conflict with existing entities.
  174. for (const pluginEntity of pluginEntities) {
  175. if (allEntities.find(e => e.name === pluginEntity.name)) {
  176. throw new InternalServerError(`error.entity-name-conflict`, { entityName: pluginEntity.name });
  177. } else {
  178. allEntities.push(pluginEntity);
  179. }
  180. }
  181. return [...coreEntities, ...pluginEntities];
  182. }
  183. /**
  184. * Collects all entities defined in plugins into a single array.
  185. */
  186. function getEntitiesFromPlugins(userConfig: Partial<VendureConfig>): Array<Type<any>> {
  187. if (!userConfig.plugins) {
  188. return [];
  189. }
  190. return userConfig.plugins
  191. .map(p => (p.defineEntities ? p.defineEntities() : []))
  192. .reduce((all, entities) => [...all, ...entities], []);
  193. }
  194. /**
  195. * Monkey-patches the app's .close() method to also close the worker microservice
  196. * instance too.
  197. */
  198. function closeWorkerOnAppClose(app: INestApplication, worker: INestMicroservice) {
  199. // A Nest app is a nested Proxy. By getting the prototype we are
  200. // able to access and override the actual close() method.
  201. const appPrototype = Object.getPrototypeOf(app);
  202. const appClose = appPrototype.close.bind(app);
  203. appPrototype.close = async () => {
  204. await worker.close();
  205. await appClose();
  206. };
  207. }
  208. function workerWelcomeMessage(config: VendureConfig) {
  209. let transportString = '';
  210. let connectionString = '';
  211. const transport = (config.workerOptions && config.workerOptions.transport) || Transport.TCP;
  212. transportString = ` with ${Transport[transport]} transport`;
  213. const options = (config.workerOptions as TcpClientOptions).options;
  214. if (options) {
  215. const { host, port } = options;
  216. connectionString = ` at ${host || 'localhost'}:${port}`;
  217. }
  218. Logger.info(`Vendure Worker started${transportString}${connectionString}`);
  219. }
  220. function logWelcomeMessage(config: VendureConfig) {
  221. let version: string;
  222. try {
  223. version = require('../package.json').version;
  224. } catch (e) {
  225. version = ' unknown';
  226. }
  227. Logger.info(`=================================================`);
  228. Logger.info(`Vendure server (v${version}) now running on port ${config.port}`);
  229. Logger.info(`Shop API: http://localhost:${config.port}/${config.shopApiPath}`);
  230. Logger.info(`Admin API: http://localhost:${config.port}/${config.adminApiPath}`);
  231. logProxyMiddlewares(config);
  232. Logger.info(`=================================================`);
  233. }