dashboard.plugin.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { MiddlewareConsumer, NestModule } from '@nestjs/common';
  2. import { Type } from '@vendure/common/lib/shared-types';
  3. import {
  4. Logger,
  5. PluginCommonModule,
  6. ProcessContext,
  7. registerPluginStartupMessage,
  8. SettingsStoreScopes,
  9. VendurePlugin,
  10. } from '@vendure/core';
  11. import express from 'express';
  12. import { rateLimit } from 'express-rate-limit';
  13. import fs from 'fs-extra';
  14. import path from 'path';
  15. import { adminApiExtensions } from './api/api-extensions.js';
  16. import { MetricsResolver } from './api/metrics.resolver.js';
  17. import { DASHBOARD_PLUGIN_OPTIONS, DEFAULT_APP_PATH, loggerCtx } from './constants.js';
  18. import { ProductDataMapper } from './entity-data-mapper/product.data-mapper';
  19. import { DbIndexingStrategy } from './search-index/db-indexing.strategy';
  20. import { MetricsService } from './service/metrics.service.js';
  21. import { DashboardPluginOptions } from './types';
  22. /**
  23. * @description
  24. * This plugin serves the static files of the Vendure Dashboard and provides the
  25. * GraphQL extensions needed for the order metrics on the dashboard index page.
  26. *
  27. * ## Installation
  28. *
  29. * `npm install \@vendure/dashboard`
  30. *
  31. * ## Usage
  32. *
  33. * First you need to set up compilation of the Dashboard, using the Vite configuration
  34. * described in the [Dashboard Getting Started Guide](/guides/extending-the-dashboard/getting-started/)
  35. *
  36. * Once set up, you run `npx vite build` to build the production version of the dashboard app.
  37. *
  38. * The built app files will be output to the location specified by `build.outDir` in your Vite
  39. * config file. This should then be passed to the `appDir` init option, as in the example below:
  40. *
  41. * @example
  42. * ```ts
  43. * import { DashboardPlugin } from '\@vendure/dashboard/plugin';
  44. *
  45. * const config: VendureConfig = {
  46. * // Add an instance of the plugin to the plugins array
  47. * plugins: [
  48. * DashboardPlugin.init({
  49. * route: 'dashboard',
  50. * appDir: './dist/dashboard',
  51. * }),
  52. * ],
  53. * };
  54. * ```
  55. *
  56. * ## Metrics
  57. *
  58. * This plugin defines a `metricSummary` query which is used by the Dashboard UI to
  59. * display the order metrics on the dashboard.
  60. *
  61. * If you are building a stand-alone version of the Dashboard UI app, and therefore
  62. * don't need this plugin to serve the Dashboard UI, you can still use the
  63. * `metricSummary` query by adding the `DashboardPlugin` to the `plugins` array,
  64. * but without calling the `init()` method:
  65. *
  66. * @example
  67. * ```ts
  68. * import { DashboardPlugin } from '\@vendure/dashboard-plugin';
  69. *
  70. * const config: VendureConfig = {
  71. * plugins: [
  72. * DashboardPlugin, // <-- no call to .init()
  73. * ],
  74. * // ...
  75. * };
  76. * ```
  77. *
  78. * @docsCategory core plugins/DashboardPlugin
  79. */
  80. @VendurePlugin({
  81. imports: [PluginCommonModule],
  82. adminApiExtensions: {
  83. schema: adminApiExtensions,
  84. resolvers: [MetricsResolver],
  85. },
  86. providers: [
  87. { provide: DASHBOARD_PLUGIN_OPTIONS, useFactory: () => DashboardPlugin.options },
  88. MetricsService,
  89. ],
  90. configuration: config => {
  91. config.settingsStoreFields['vendure.dashboard'] = [
  92. {
  93. name: 'userSettings',
  94. scope: SettingsStoreScopes.user,
  95. },
  96. ];
  97. return config;
  98. },
  99. compatibility: '^3.0.0',
  100. })
  101. export class DashboardPlugin implements NestModule {
  102. private static options: DashboardPluginOptions | undefined;
  103. constructor(private readonly processContext: ProcessContext) {}
  104. /**
  105. * @description
  106. * Set the plugin options
  107. */
  108. static init(options: DashboardPluginOptions): Type<DashboardPlugin> {
  109. this.options = {
  110. ...options,
  111. globalSearch: {
  112. indexingStrategy: options.globalSearch?.indexingStrategy ?? new DbIndexingStrategy(),
  113. entityDataMappers: {
  114. Product: new ProductDataMapper(),
  115. ...options.globalSearch?.entityDataMappers,
  116. },
  117. },
  118. };
  119. return DashboardPlugin;
  120. }
  121. configure(consumer: MiddlewareConsumer) {
  122. if (this.processContext.isWorker) {
  123. return;
  124. }
  125. if (!DashboardPlugin.options) {
  126. Logger.info(
  127. `DashboardPlugin's init() method was not called. The Dashboard UI will not be served.`,
  128. loggerCtx,
  129. );
  130. return;
  131. }
  132. const { route, appDir } = DashboardPlugin.options;
  133. const dashboardPath = appDir || this.getDashboardPath();
  134. Logger.info('Creating dashboard middleware', loggerCtx);
  135. consumer.apply(this.createStaticServer(dashboardPath)).forRoutes(route);
  136. registerPluginStartupMessage('Dashboard UI', route);
  137. }
  138. private createStaticServer(dashboardPath: string) {
  139. const limiter = rateLimit({
  140. windowMs: 60 * 1000,
  141. limit: process.env.NODE_ENV === 'production' ? 500 : 2000,
  142. standardHeaders: true,
  143. legacyHeaders: false,
  144. });
  145. const dashboardServer = express.Router();
  146. // This is a workaround for a type mismatch between express v5 (Vendure core)
  147. // and express v4 (several transitive dependencies). Can be removed once the
  148. // ecosystem has more significantly shifted to v5.
  149. dashboardServer.use(limiter as any);
  150. dashboardServer.use(express.static(dashboardPath));
  151. dashboardServer.use((req, res) => {
  152. res.sendFile('index.html', { root: dashboardPath });
  153. });
  154. return dashboardServer;
  155. }
  156. private getDashboardPath(): string {
  157. // First, try to find the dashboard dist directory in the @vendure/dashboard package
  158. try {
  159. const dashboardPackageJson = require.resolve('@vendure/dashboard/package.json');
  160. const dashboardPackageRoot = path.dirname(dashboardPackageJson);
  161. const dashboardDistPath = path.join(dashboardPackageRoot, 'dist');
  162. if (fs.existsSync(dashboardDistPath)) {
  163. Logger.info(`Found dashboard UI at ${dashboardDistPath}`, loggerCtx);
  164. return dashboardDistPath;
  165. }
  166. } catch (e) {
  167. // Dashboard package not found, continue to fallback
  168. }
  169. // Fallback to default path
  170. Logger.warn(
  171. `Could not find @vendure/dashboard dist directory. Falling back to default path: ${DEFAULT_APP_PATH}`,
  172. loggerCtx,
  173. );
  174. return DEFAULT_APP_PATH;
  175. }
  176. }