dashboard.plugin.ts 6.7 KB

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