1
0

plugin.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
  2. import { AdminUiApp, AdminUiAppDevMode, AdminUiConfig, Type } from '@vendure/common/lib/shared-types';
  3. import {
  4. AuthOptions,
  5. ConfigService,
  6. createProxyHandler,
  7. Logger,
  8. OnVendureBootstrap,
  9. OnVendureClose,
  10. PluginCommonModule,
  11. RuntimeVendureConfig,
  12. VendurePlugin,
  13. } from '@vendure/core';
  14. import express from 'express';
  15. import fs from 'fs-extra';
  16. import { Server } from 'http';
  17. import path from 'path';
  18. import { DEFAULT_APP_PATH, loggerCtx } from './constants';
  19. /**
  20. * @description
  21. * Configuration options for the {@link AdminUiPlugin}.
  22. *
  23. * @docsCategory AdminUiPlugin
  24. */
  25. export interface AdminUiOptions {
  26. /**
  27. * @description
  28. * The port on which the server will listen. If not
  29. */
  30. port: number;
  31. /**
  32. * @description
  33. * The hostname of the server serving the static admin ui files.
  34. *
  35. * @default 'localhost'
  36. */
  37. hostname?: string;
  38. /**
  39. * @description
  40. * By default, the AdminUiPlugin comes bundles with a pre-built version of the
  41. * Admin UI. This option can be used to override this default build with a different
  42. * version, e.g. one pre-compiled with one or more ui extensions.
  43. */
  44. app?: AdminUiApp | AdminUiAppDevMode;
  45. /**
  46. * @description
  47. * The hostname of the Vendure server which the admin ui will be making API calls
  48. * to. If set to "auto", the admin ui app will determine the hostname from the
  49. * current location (i.e. `window.location.hostname`).
  50. *
  51. * @default 'auto'
  52. */
  53. apiHost?: string | 'auto';
  54. /**
  55. * @description
  56. * The port of the Vendure server which the admin ui will be making API calls
  57. * to. If set to "auto", the admin ui app will determine the port from the
  58. * current location (i.e. `window.location.port`).
  59. *
  60. * @default 'auto'
  61. */
  62. apiPort?: number | 'auto';
  63. }
  64. /**
  65. * @description
  66. * This plugin starts a static server for the Admin UI app, and proxies it via the `/admin/` path of the main Vendure server.
  67. *
  68. * The Admin UI allows you to administer all aspects of your store, from inventory management to order tracking. It is the tool used by
  69. * store administrators on a day-to-day basis for the management of the store.
  70. *
  71. * ## Installation
  72. *
  73. * `yarn add \@vendure/admin-ui-plugin`
  74. *
  75. * or
  76. *
  77. * `npm install \@vendure/admin-ui-plugin`
  78. *
  79. * @example
  80. * ```ts
  81. * import { AdminUiPlugin } from '\@vendure/admin-ui-plugin';
  82. *
  83. * const config: VendureConfig = {
  84. * // Add an instance of the plugin to the plugins array
  85. * plugins: [
  86. * AdminUiPlugin.init({ port: 3002 }),
  87. * ],
  88. * };
  89. * ```
  90. *
  91. * @docsCategory AdminUiPlugin
  92. */
  93. @VendurePlugin({
  94. imports: [PluginCommonModule],
  95. providers: [],
  96. configuration: config => AdminUiPlugin.configure(config),
  97. })
  98. export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
  99. private static options: AdminUiOptions;
  100. private server: Server;
  101. private devServerClose: () => void | Promise<void> | undefined;
  102. constructor(private configService: ConfigService) {}
  103. /**
  104. * @description
  105. * Set the plugin options
  106. */
  107. static init(options: AdminUiOptions): Type<AdminUiPlugin> {
  108. this.options = options;
  109. return AdminUiPlugin;
  110. }
  111. /** @internal */
  112. static async configure(config: RuntimeVendureConfig): Promise<RuntimeVendureConfig> {
  113. const route = 'admin';
  114. const { app } = this.options;
  115. const appWatchMode = this.isDevModeApp(app);
  116. let port: number;
  117. if (this.isDevModeApp(app)) {
  118. port = app.port;
  119. } else {
  120. port = this.options.port;
  121. }
  122. config.middleware.push({
  123. handler: createProxyHandler({
  124. hostname: this.options.hostname,
  125. port,
  126. route: 'admin',
  127. label: 'Admin UI',
  128. basePath: appWatchMode ? 'admin' : undefined,
  129. }),
  130. route,
  131. });
  132. if (this.isDevModeApp(app)) {
  133. config.middleware.push({
  134. handler: createProxyHandler({
  135. hostname: this.options.hostname,
  136. port,
  137. route: 'sockjs-node',
  138. label: 'Admin UI live reload',
  139. basePath: 'sockjs-node',
  140. }),
  141. route: 'sockjs-node',
  142. });
  143. }
  144. return config;
  145. }
  146. /** @internal */
  147. async onVendureBootstrap() {
  148. const { adminApiPath, authOptions } = this.configService;
  149. const { apiHost, apiPort, port, app } = AdminUiPlugin.options;
  150. const adminUiAppPath = AdminUiPlugin.isDevModeApp(app)
  151. ? app.sourcePath
  152. : (app && app.path) || DEFAULT_APP_PATH;
  153. const adminUiConfigPath = path.join(adminUiAppPath, 'vendure-ui-config.json');
  154. const overwriteConfig = () =>
  155. this.overwriteAdminUiConfig({
  156. host: apiHost || 'auto',
  157. port: apiPort || 'auto',
  158. adminApiPath,
  159. authOptions,
  160. adminUiConfigPath,
  161. });
  162. if (!AdminUiPlugin.isDevModeApp(app)) {
  163. // If not in dev mode, start a static server for the compiled app
  164. const adminUiServer = express();
  165. adminUiServer.use(express.static(adminUiAppPath));
  166. adminUiServer.use((req, res) => {
  167. res.sendFile(path.join(adminUiAppPath, 'index.html'));
  168. });
  169. this.server = adminUiServer.listen(AdminUiPlugin.options.port);
  170. if (app && typeof app.compile === 'function') {
  171. Logger.info(`Compiling Admin UI app in production mode`, loggerCtx);
  172. app.compile()
  173. .then(overwriteConfig)
  174. .then(() => {
  175. Logger.info(`Admin UI successfully compiled`);
  176. });
  177. } else {
  178. await overwriteConfig();
  179. }
  180. } else {
  181. Logger.info(`Compiling Admin UI app in development mode`, loggerCtx);
  182. app.compile()
  183. .then(overwriteConfig)
  184. .then(() => {
  185. Logger.info(`Admin UI successfully compiled and watching for changes...`);
  186. });
  187. }
  188. }
  189. /** @internal */
  190. async onVendureClose(): Promise<void> {
  191. if (this.devServerClose) {
  192. await this.devServerClose();
  193. }
  194. if (this.server) {
  195. await new Promise(resolve => this.server.close(() => resolve()));
  196. }
  197. }
  198. /**
  199. * Overwrites the parts of the admin-ui app's `vendure-ui-config.json` file relating to connecting to
  200. * the server admin API.
  201. */
  202. private async overwriteAdminUiConfig(options: {
  203. host: string | 'auto';
  204. port: number | 'auto';
  205. adminApiPath: string;
  206. authOptions: AuthOptions;
  207. adminUiConfigPath: string;
  208. }) {
  209. const { host, port, adminApiPath, authOptions, adminUiConfigPath } = options;
  210. const adminUiConfig = await fs.readFile(adminUiConfigPath, 'utf-8');
  211. let config: AdminUiConfig;
  212. try {
  213. config = JSON.parse(adminUiConfig);
  214. } catch (e) {
  215. throw new Error('[AdminUiPlugin] Could not parse vendure-ui-config.json file:\n' + e.message);
  216. }
  217. config.apiHost = host || 'http://localhost';
  218. config.apiPort = port;
  219. config.adminApiPath = adminApiPath;
  220. config.tokenMethod = authOptions.tokenMethod || 'cookie';
  221. config.authTokenHeaderKey = authOptions.authTokenHeaderKey || DEFAULT_AUTH_TOKEN_HEADER_KEY;
  222. Logger.verbose(`Applying configuration to vendure-ui-config.json file`, loggerCtx);
  223. await fs.writeFile(adminUiConfigPath, JSON.stringify(config, null, 2));
  224. }
  225. private static isDevModeApp(app?: AdminUiApp | AdminUiAppDevMode): app is AdminUiAppDevMode {
  226. if (!app) {
  227. return false;
  228. }
  229. return typeof (app as any).close === 'function' && typeof (app as any).sourcePath === 'string';
  230. }
  231. }