plugin.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
  2. import { AdminUiConfig, Type } from '@vendure/common/lib/shared-types';
  3. import {
  4. AuthOptions,
  5. createProxyHandler,
  6. OnVendureBootstrap,
  7. OnVendureClose,
  8. RuntimeVendureConfig,
  9. VendurePlugin,
  10. } from '@vendure/core';
  11. import express from 'express';
  12. import fs from 'fs-extra';
  13. import { Server } from 'http';
  14. import path from 'path';
  15. /**
  16. * @description
  17. * Configuration options for the {@link AdminUiPlugin}.
  18. *
  19. * @docsCategory AdminUiPlugin
  20. */
  21. export interface AdminUiOptions {
  22. /**
  23. * @description
  24. * The hostname of the server serving the static admin ui files.
  25. *
  26. * @default 'localhost'
  27. */
  28. hostname?: string;
  29. /**
  30. * @description
  31. * The port on which the server will listen.
  32. */
  33. port: number;
  34. /**
  35. * @description
  36. * The hostname of the Vendure server which the admin ui will be making API calls
  37. * to. If set to "auto", the admin ui app will determine the hostname from the
  38. * current location (i.e. `window.location.hostname`).
  39. *
  40. * @default 'auto'
  41. */
  42. apiHost?: string | 'auto';
  43. /**
  44. * @description
  45. * The port of the Vendure server which the admin ui will be making API calls
  46. * to. If set to "auto", the admin ui app will determine the port from the
  47. * current location (i.e. `window.location.port`).
  48. *
  49. * @default 'auto'
  50. */
  51. apiPort?: number | 'auto';
  52. }
  53. /**
  54. * @description
  55. * This plugin starts a static server for the Admin UI app, and proxies it via the `/admin/` path of the main Vendure server.
  56. *
  57. * The Admin UI allows you to administer all aspects of your store, from inventory management to order tracking. It is the tool used by
  58. * store administrators on a day-to-day basis for the management of the store.
  59. *
  60. * ## Installation
  61. *
  62. * `yarn add \@vendure/admin-ui-plugin`
  63. *
  64. * or
  65. *
  66. * `npm install \@vendure/admin-ui-plugin`
  67. *
  68. * @example
  69. * ```ts
  70. * import { AdminUiPlugin } from '\@vendure/admin-ui-plugin';
  71. *
  72. * const config: VendureConfig = {
  73. * // Add an instance of the plugin to the plugins array
  74. * plugins: [
  75. * AdminUiPlugin.init({ port: 3002 }),
  76. * ],
  77. * };
  78. * ```
  79. *
  80. * @docsCategory AdminUiPlugin
  81. */
  82. @VendurePlugin({
  83. configuration: config => AdminUiPlugin.configure(config),
  84. })
  85. export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
  86. private static options: AdminUiOptions;
  87. private server: Server;
  88. /**
  89. * @description
  90. * Set the plugin options
  91. */
  92. static init(options: AdminUiOptions): Type<AdminUiPlugin> {
  93. this.options = options;
  94. return AdminUiPlugin;
  95. }
  96. /** @internal */
  97. static async configure(config: RuntimeVendureConfig): Promise<RuntimeVendureConfig> {
  98. const route = 'admin';
  99. config.middleware.push({
  100. handler: createProxyHandler({ ...this.options, route, label: 'Admin UI' }),
  101. route,
  102. });
  103. const { adminApiPath, authOptions } = config;
  104. const { apiHost, apiPort } = this.options;
  105. await this.overwriteAdminUiConfig(apiHost || 'auto', apiPort || 'auto', adminApiPath, authOptions);
  106. return config;
  107. }
  108. /** @internal */
  109. onVendureBootstrap() {
  110. const adminUiPath = AdminUiPlugin.getAdminUiPath();
  111. const assetServer = express();
  112. assetServer.use(express.static(adminUiPath));
  113. assetServer.use((req, res) => {
  114. res.sendFile(path.join(adminUiPath, 'index.html'));
  115. });
  116. this.server = assetServer.listen(AdminUiPlugin.options.port);
  117. }
  118. /** @internal */
  119. onVendureClose(): Promise<void> {
  120. return new Promise(resolve => this.server.close(() => resolve()));
  121. }
  122. /**
  123. * Overwrites the parts of the admin-ui app's `vendure-ui-config.json` file relating to connecting to
  124. * the server admin API.
  125. */
  126. private static async overwriteAdminUiConfig(
  127. host: string | 'auto',
  128. port: number | 'auto',
  129. adminApiPath: string,
  130. authOptions: AuthOptions,
  131. ) {
  132. const adminUiConfigPath = path.join(this.getAdminUiPath(), 'vendure-ui-config.json');
  133. const adminUiConfig = await fs.readFile(adminUiConfigPath, 'utf-8');
  134. let config: AdminUiConfig;
  135. try {
  136. config = JSON.parse(adminUiConfig);
  137. } catch (e) {
  138. throw new Error('[AdminUiPlugin] Could not parse vendure-ui-config.json file:\n' + e.message);
  139. }
  140. config.apiHost = host || 'http://localhost';
  141. config.apiPort = port;
  142. config.adminApiPath = adminApiPath;
  143. config.tokenMethod = authOptions.tokenMethod || 'cookie';
  144. config.authTokenHeaderKey = authOptions.authTokenHeaderKey || DEFAULT_AUTH_TOKEN_HEADER_KEY;
  145. await fs.writeFile(adminUiConfigPath, JSON.stringify(config, null, 2));
  146. }
  147. private static getAdminUiPath(): string {
  148. // attempt to read from the path location on a production npm install
  149. const prodPath = path.join(__dirname, '../admin-ui');
  150. if (fs.existsSync(path.join(prodPath, 'index.html'))) {
  151. return prodPath;
  152. }
  153. // attempt to read from the path on a development install
  154. const devPath = path.join(__dirname, '../lib/admin-ui');
  155. if (fs.existsSync(path.join(devPath, 'index.html'))) {
  156. return devPath;
  157. }
  158. throw new Error(`AdminUiPlugin: admin-ui app not found`);
  159. }
  160. }