plugin.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import { MiddlewareConsumer, NestModule } from '@nestjs/common';
  2. import { Type } from '@vendure/common/lib/shared-types';
  3. import {
  4. ConfigService,
  5. Logger,
  6. PluginCommonModule,
  7. ProcessContext,
  8. registerPluginStartupMessage,
  9. VendurePlugin,
  10. } from '@vendure/core';
  11. import express from 'express';
  12. import fs from 'fs';
  13. import path from 'path';
  14. import { loggerCtx, PLUGIN_INIT_OPTIONS } from './constants';
  15. import { GraphiQLService } from './graphiql.service';
  16. import { GraphiqlPluginOptions } from './types';
  17. /**
  18. * @description
  19. * This plugin provides a GraphiQL UI for exploring and testing the Vendure GraphQL APIs.
  20. *
  21. * It adds routes `/graphiql/admin` and `/graphiql/shop` which serve the GraphiQL interface
  22. * for the respective APIs.
  23. *
  24. * ## Installation
  25. *
  26. * ```ts
  27. * import { GraphiqlPlugin } from '\@vendure/graphiql-plugin';
  28. *
  29. * const config: VendureConfig = {
  30. * // Add an instance of the plugin to the plugins array
  31. * plugins: [
  32. * GraphiqlPlugin.init({
  33. * route: 'graphiql', // Optional, defaults to 'graphiql'
  34. * }),
  35. * ],
  36. * };
  37. * ```
  38. *
  39. * ## Custom API paths
  40. *
  41. * By default, the plugin automatically reads the Admin API and Shop API paths from your Vendure configuration.
  42. *
  43. * If you need to override these paths, you can specify them explicitly:
  44. *
  45. * ```typescript
  46. * GraphiQLPlugin.init({
  47. * route: 'my-custom-route', // defaults to `graphiql`
  48. * });
  49. * ```
  50. *
  51. * ## Query parameters
  52. *
  53. * You can add the following query parameters to the GraphiQL URL:
  54. *
  55. * - `?query=...` - Pre-populate the query editor with a GraphQL query.
  56. * - `?embeddedMode=true` - This renders the editor in embedded mode, which hides the header and
  57. * the API switcher. This is useful for embedding GraphiQL in other applications such as documentation.
  58. * In this mode, the editor also does not persist changes across reloads.
  59. *
  60. * @docsCategory core plugins/GraphiqlPlugin
  61. */
  62. @VendurePlugin({
  63. imports: [PluginCommonModule],
  64. providers: [
  65. GraphiQLService,
  66. {
  67. provide: PLUGIN_INIT_OPTIONS,
  68. useFactory: () => GraphiqlPlugin.options,
  69. },
  70. ],
  71. configuration: config => {
  72. // disable GraphQL playground in config
  73. config.apiOptions.adminApiPlayground = false;
  74. config.apiOptions.shopApiPlayground = false;
  75. return config;
  76. },
  77. exports: [GraphiQLService],
  78. compatibility: '^3.0.0',
  79. })
  80. export class GraphiqlPlugin implements NestModule {
  81. static options: Required<GraphiqlPluginOptions>;
  82. constructor(
  83. private readonly processContext: ProcessContext,
  84. private readonly configService: ConfigService,
  85. private readonly graphiQLService: GraphiQLService,
  86. ) {}
  87. /**
  88. * Initialize the plugin with the given options.
  89. */
  90. static init(options: GraphiqlPluginOptions = {}): Type<GraphiqlPlugin> {
  91. this.options = {
  92. ...options,
  93. route: options.route || 'graphiql',
  94. };
  95. return GraphiqlPlugin;
  96. }
  97. configure(consumer: MiddlewareConsumer) {
  98. if (!this.processContext.isServer) {
  99. return;
  100. }
  101. const adminRoute = GraphiqlPlugin.options.route + '/admin';
  102. const shopRoute = GraphiqlPlugin.options.route + '/shop';
  103. consumer.apply(this.createStaticServer()).forRoutes(adminRoute);
  104. consumer.apply(this.createStaticServer()).forRoutes(shopRoute);
  105. // Add middleware for serving assets
  106. consumer.apply(this.createAssetsServer()).forRoutes('/graphiql/assets/*path');
  107. registerPluginStartupMessage('GraphiQL Admin', adminRoute);
  108. registerPluginStartupMessage('GraphiQL Shop', shopRoute);
  109. }
  110. private createStaticServer() {
  111. const distDir = path.join(__dirname, '../dist/graphiql');
  112. const adminApiUrl = this.graphiQLService.getAdminApiUrl();
  113. const shopApiUrl = this.graphiQLService.getShopApiUrl();
  114. return (req: express.Request, res: express.Response) => {
  115. try {
  116. const indexHtmlPath = path.join(distDir, 'index.html');
  117. if (fs.existsSync(indexHtmlPath)) {
  118. // Read the HTML file
  119. let html = fs.readFileSync(indexHtmlPath, 'utf-8');
  120. // Inject API URLs
  121. html = html.replace(
  122. '</head>',
  123. `<script>
  124. window.GRAPHIQL_SETTINGS = {
  125. adminApiUrl: "${adminApiUrl}",
  126. shopApiUrl: "${shopApiUrl}"
  127. };
  128. </script>
  129. </head>`,
  130. );
  131. return res.send(html);
  132. }
  133. throw new Error(`GraphiQL UI not found: ${indexHtmlPath}`);
  134. } catch (e) {
  135. const errorMessage = e instanceof Error ? e.message : 'Unknown error';
  136. Logger.error(`Error serving GraphiQL: ${errorMessage}`, 'GraphiQLPlugin');
  137. return res.status(500).send('An error occurred while rendering GraphiQL');
  138. }
  139. };
  140. }
  141. private createAssetsServer() {
  142. const distDir = path.join(__dirname, '../dist/graphiql');
  143. return (req: express.Request, res: express.Response, next: express.NextFunction) => {
  144. try {
  145. // Extract asset path from URL
  146. const assetPath = req.params[0] || req.params.path || '';
  147. const filePath = path.join(distDir, 'assets', assetPath.toString());
  148. if (fs.existsSync(filePath)) {
  149. return res.sendFile(assetPath.toString(), { root: path.join(distDir, 'assets') });
  150. } else {
  151. return res.status(404).send('Asset not found');
  152. }
  153. } catch (e: unknown) {
  154. const errorMessage = e instanceof Error ? e.message : 'Unknown error';
  155. Logger.error(`Error serving static asset: ${errorMessage}`, loggerCtx);
  156. return res.status(500).send('An error occurred while serving static asset');
  157. }
  158. };
  159. }
  160. }