|
@@ -1,6 +1,10 @@
|
|
|
-import { Watcher } from '@vendure/admin-ui/compiler/watch';
|
|
|
|
|
import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
|
|
import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
|
|
|
-import { AdminUiConfig, AdminUiExtension, Type } from '@vendure/common/lib/shared-types';
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ AdminUiAppConfig,
|
|
|
|
|
+ AdminUiAppDevModeConfig,
|
|
|
|
|
+ AdminUiConfig,
|
|
|
|
|
+ Type,
|
|
|
|
|
+} from '@vendure/common/lib/shared-types';
|
|
|
import {
|
|
import {
|
|
|
AuthOptions,
|
|
AuthOptions,
|
|
|
ConfigService,
|
|
ConfigService,
|
|
@@ -17,8 +21,7 @@ import fs from 'fs-extra';
|
|
|
import { Server } from 'http';
|
|
import { Server } from 'http';
|
|
|
import path from 'path';
|
|
import path from 'path';
|
|
|
|
|
|
|
|
-import { UI_PATH } from './constants';
|
|
|
|
|
-import { UiAppCompiler } from './ui-app-compiler.service';
|
|
|
|
|
|
|
+import { DEFAULT_APP_PATH, loggerCtx } from './constants';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* @description
|
|
* @description
|
|
@@ -27,6 +30,11 @@ import { UiAppCompiler } from './ui-app-compiler.service';
|
|
|
* @docsCategory AdminUiPlugin
|
|
* @docsCategory AdminUiPlugin
|
|
|
*/
|
|
*/
|
|
|
export interface AdminUiOptions {
|
|
export interface AdminUiOptions {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @description
|
|
|
|
|
+ * The port on which the server will listen. If not
|
|
|
|
|
+ */
|
|
|
|
|
+ port: number;
|
|
|
/**
|
|
/**
|
|
|
* @description
|
|
* @description
|
|
|
* The hostname of the server serving the static admin ui files.
|
|
* The hostname of the server serving the static admin ui files.
|
|
@@ -36,9 +44,11 @@ export interface AdminUiOptions {
|
|
|
hostname?: string;
|
|
hostname?: string;
|
|
|
/**
|
|
/**
|
|
|
* @description
|
|
* @description
|
|
|
- * The port on which the server will listen.
|
|
|
|
|
|
|
+ * By default, the AdminUiPlugin comes bundles with a pre-built version of the
|
|
|
|
|
+ * Admin UI. This option can be used to override this default build with a different
|
|
|
|
|
+ * version, e.g. one pre-compiled with one or more ui extensions.
|
|
|
*/
|
|
*/
|
|
|
- port: number;
|
|
|
|
|
|
|
+ app?: AdminUiAppConfig | AdminUiAppDevModeConfig;
|
|
|
/**
|
|
/**
|
|
|
* @description
|
|
* @description
|
|
|
* The hostname of the Vendure server which the admin ui will be making API calls
|
|
* The hostname of the Vendure server which the admin ui will be making API calls
|
|
@@ -57,22 +67,6 @@ export interface AdminUiOptions {
|
|
|
* @default 'auto'
|
|
* @default 'auto'
|
|
|
*/
|
|
*/
|
|
|
apiPort?: number | 'auto';
|
|
apiPort?: number | 'auto';
|
|
|
- /**
|
|
|
|
|
- * @description
|
|
|
|
|
- * An optional array of objects which configure extension Angular modules
|
|
|
|
|
- * to be compiled into and made available by the AdminUi application.
|
|
|
|
|
- */
|
|
|
|
|
- extensions?: AdminUiExtension[];
|
|
|
|
|
- /**
|
|
|
|
|
- * @description
|
|
|
|
|
- * Set to `true` in order to run the Admin UI in development mode (using the Angular CLI
|
|
|
|
|
- * [ng serve](https://angular.io/cli/serve) command). When in watch mode, any changes to
|
|
|
|
|
- * UI extension files will be watched and trigger a rebuild of the Admin UI with live
|
|
|
|
|
- * reloading.
|
|
|
|
|
- *
|
|
|
|
|
- * @default false
|
|
|
|
|
- */
|
|
|
|
|
- watch?: boolean;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -106,15 +100,14 @@ export interface AdminUiOptions {
|
|
|
*/
|
|
*/
|
|
|
@VendurePlugin({
|
|
@VendurePlugin({
|
|
|
imports: [PluginCommonModule],
|
|
imports: [PluginCommonModule],
|
|
|
- providers: [UiAppCompiler],
|
|
|
|
|
|
|
+ providers: [],
|
|
|
configuration: config => AdminUiPlugin.configure(config),
|
|
configuration: config => AdminUiPlugin.configure(config),
|
|
|
})
|
|
})
|
|
|
export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
|
|
export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
|
|
|
private static options: AdminUiOptions;
|
|
private static options: AdminUiOptions;
|
|
|
private server: Server;
|
|
private server: Server;
|
|
|
- private watcher: Watcher | undefined;
|
|
|
|
|
|
|
|
|
|
- constructor(private configService: ConfigService, private appCompiler: UiAppCompiler) {}
|
|
|
|
|
|
|
+ constructor(private configService: ConfigService) {}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* @description
|
|
* @description
|
|
@@ -128,19 +121,29 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
|
|
|
/** @internal */
|
|
/** @internal */
|
|
|
static async configure(config: RuntimeVendureConfig): Promise<RuntimeVendureConfig> {
|
|
static async configure(config: RuntimeVendureConfig): Promise<RuntimeVendureConfig> {
|
|
|
const route = 'admin';
|
|
const route = 'admin';
|
|
|
|
|
+ const { app } = this.options;
|
|
|
|
|
+ const appWatchMode = this.isDevModeApp(app);
|
|
|
|
|
+ let port: number;
|
|
|
|
|
+ if (this.isDevModeApp(app)) {
|
|
|
|
|
+ port = app.port;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ port = this.options.port;
|
|
|
|
|
+ }
|
|
|
config.middleware.push({
|
|
config.middleware.push({
|
|
|
handler: createProxyHandler({
|
|
handler: createProxyHandler({
|
|
|
- ...this.options,
|
|
|
|
|
|
|
+ hostname: this.options.hostname,
|
|
|
|
|
+ port,
|
|
|
route: 'admin',
|
|
route: 'admin',
|
|
|
label: 'Admin UI',
|
|
label: 'Admin UI',
|
|
|
- basePath: this.options.watch ? 'admin' : undefined,
|
|
|
|
|
|
|
+ basePath: appWatchMode ? 'admin' : undefined,
|
|
|
}),
|
|
}),
|
|
|
route,
|
|
route,
|
|
|
});
|
|
});
|
|
|
- if (this.options.watch) {
|
|
|
|
|
|
|
+ if (this.isDevModeApp(app)) {
|
|
|
config.middleware.push({
|
|
config.middleware.push({
|
|
|
handler: createProxyHandler({
|
|
handler: createProxyHandler({
|
|
|
- ...this.options,
|
|
|
|
|
|
|
+ hostname: this.options.hostname,
|
|
|
|
|
+ port,
|
|
|
route: 'sockjs-node',
|
|
route: 'sockjs-node',
|
|
|
label: 'Admin UI live reload',
|
|
label: 'Admin UI live reload',
|
|
|
basePath: 'sockjs-node',
|
|
basePath: 'sockjs-node',
|
|
@@ -154,36 +157,59 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
|
|
|
/** @internal */
|
|
/** @internal */
|
|
|
async onVendureBootstrap() {
|
|
async onVendureBootstrap() {
|
|
|
const { adminApiPath, authOptions } = this.configService;
|
|
const { adminApiPath, authOptions } = this.configService;
|
|
|
- const { apiHost, apiPort, extensions, watch, port } = AdminUiPlugin.options;
|
|
|
|
|
- let adminUiConfigPath: string;
|
|
|
|
|
|
|
+ const { apiHost, apiPort, port, app } = AdminUiPlugin.options;
|
|
|
|
|
+ const adminUiAppPath = AdminUiPlugin.isDevModeApp(app)
|
|
|
|
|
+ ? path.join(app.sourcePath, 'src')
|
|
|
|
|
+ : (app && app.path) || DEFAULT_APP_PATH;
|
|
|
|
|
+ const adminUiConfigPath = path.join(adminUiAppPath, 'vendure-ui-config.json');
|
|
|
|
|
+ const overwriteConfig = () =>
|
|
|
|
|
+ this.overwriteAdminUiConfig({
|
|
|
|
|
+ host: apiHost || 'auto',
|
|
|
|
|
+ port: apiPort || 'auto',
|
|
|
|
|
+ adminApiPath,
|
|
|
|
|
+ authOptions,
|
|
|
|
|
+ adminUiConfigPath,
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- if (watch) {
|
|
|
|
|
- this.watcher = this.appCompiler.watchAdminUiApp(extensions, port);
|
|
|
|
|
- adminUiConfigPath = path.join(__dirname, '../../../admin-ui/src', 'vendure-ui-config.json');
|
|
|
|
|
- } else {
|
|
|
|
|
- const adminUiPath = await this.appCompiler.compileAdminUiApp(extensions);
|
|
|
|
|
|
|
+ if (!AdminUiPlugin.isDevModeApp(app)) {
|
|
|
|
|
+ // If not in dev mode, start a static server for the compiled app
|
|
|
const adminUiServer = express();
|
|
const adminUiServer = express();
|
|
|
- adminUiServer.use(express.static(UI_PATH));
|
|
|
|
|
|
|
+ adminUiServer.use(express.static(adminUiAppPath));
|
|
|
adminUiServer.use((req, res) => {
|
|
adminUiServer.use((req, res) => {
|
|
|
- res.sendFile(path.join(UI_PATH, 'index.html'));
|
|
|
|
|
|
|
+ res.sendFile(path.join(adminUiAppPath, 'index.html'));
|
|
|
});
|
|
});
|
|
|
this.server = adminUiServer.listen(AdminUiPlugin.options.port);
|
|
this.server = adminUiServer.listen(AdminUiPlugin.options.port);
|
|
|
- adminUiConfigPath = path.join(UI_PATH, 'vendure-ui-config.json');
|
|
|
|
|
|
|
+ if (app && typeof app.compile === 'function') {
|
|
|
|
|
+ Logger.info(`Compiling Admin UI app in production mode...`, loggerCtx);
|
|
|
|
|
+ app.compile()
|
|
|
|
|
+ .then(overwriteConfig)
|
|
|
|
|
+ .then(
|
|
|
|
|
+ () => {
|
|
|
|
|
+ Logger.info(`Admin UI successfully compiled`, loggerCtx);
|
|
|
|
|
+ },
|
|
|
|
|
+ (err: any) => {
|
|
|
|
|
+ Logger.error(`Failed to compile: ${err}`, loggerCtx, err.stack);
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await overwriteConfig();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Logger.info(`Compiling Admin UI app in development mode`, loggerCtx);
|
|
|
|
|
+ app.compile().then(
|
|
|
|
|
+ () => {
|
|
|
|
|
+ Logger.info(`Admin UI compiling and watching for changes...`, loggerCtx);
|
|
|
|
|
+ },
|
|
|
|
|
+ (err: any) => {
|
|
|
|
|
+ Logger.error(`Failed to compile: ${err}`, loggerCtx, err.stack);
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ await overwriteConfig();
|
|
|
}
|
|
}
|
|
|
- await this.overwriteAdminUiConfig({
|
|
|
|
|
- host: apiHost || 'auto',
|
|
|
|
|
- port: apiPort || 'auto',
|
|
|
|
|
- adminApiPath,
|
|
|
|
|
- authOptions,
|
|
|
|
|
- adminUiConfigPath,
|
|
|
|
|
- });
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** @internal */
|
|
/** @internal */
|
|
|
async onVendureClose(): Promise<void> {
|
|
async onVendureClose(): Promise<void> {
|
|
|
- if (this.watcher) {
|
|
|
|
|
- this.watcher.close();
|
|
|
|
|
- }
|
|
|
|
|
if (this.server) {
|
|
if (this.server) {
|
|
|
await new Promise(resolve => this.server.close(() => resolve()));
|
|
await new Promise(resolve => this.server.close(() => resolve()));
|
|
|
}
|
|
}
|
|
@@ -201,10 +227,40 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
|
|
|
adminUiConfigPath: string;
|
|
adminUiConfigPath: string;
|
|
|
}) {
|
|
}) {
|
|
|
const { host, port, adminApiPath, authOptions, adminUiConfigPath } = options;
|
|
const { host, port, adminApiPath, authOptions, adminUiConfigPath } = options;
|
|
|
- const adminUiConfig = await fs.readFile(adminUiConfigPath, 'utf-8');
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * It might be that the ui-devkit compiler has not yet copied the config
|
|
|
|
|
+ * file to the expected location (perticularly when running in watch mode),
|
|
|
|
|
+ * so polling is used to check multiple times with a delay.
|
|
|
|
|
+ */
|
|
|
|
|
+ async function pollForConfigFile() {
|
|
|
|
|
+ let configFileContent: string;
|
|
|
|
|
+ const maxRetries = 5;
|
|
|
|
|
+ const retryDelay = 200;
|
|
|
|
|
+ let attempts = 0;
|
|
|
|
|
+ return new Promise<string>(async function checkForFile(resolve, reject) {
|
|
|
|
|
+ if (attempts >= maxRetries) {
|
|
|
|
|
+ reject();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ Logger.verbose(`Checking for config file: ${adminUiConfigPath}`, loggerCtx);
|
|
|
|
|
+ configFileContent = await fs.readFile(adminUiConfigPath, 'utf-8');
|
|
|
|
|
+ resolve(configFileContent);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ attempts++;
|
|
|
|
|
+ Logger.verbose(
|
|
|
|
|
+ `Unable to locate config file: ${adminUiConfigPath} (attempt ${attempts})`,
|
|
|
|
|
+ loggerCtx,
|
|
|
|
|
+ );
|
|
|
|
|
+ setTimeout(pollForConfigFile, retryDelay, resolve, reject);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const content = await pollForConfigFile();
|
|
|
let config: AdminUiConfig;
|
|
let config: AdminUiConfig;
|
|
|
try {
|
|
try {
|
|
|
- config = JSON.parse(adminUiConfig);
|
|
|
|
|
|
|
+ config = JSON.parse(content);
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
throw new Error('[AdminUiPlugin] Could not parse vendure-ui-config.json file:\n' + e.message);
|
|
throw new Error('[AdminUiPlugin] Could not parse vendure-ui-config.json file:\n' + e.message);
|
|
|
}
|
|
}
|
|
@@ -214,5 +270,15 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
|
|
|
config.tokenMethod = authOptions.tokenMethod || 'cookie';
|
|
config.tokenMethod = authOptions.tokenMethod || 'cookie';
|
|
|
config.authTokenHeaderKey = authOptions.authTokenHeaderKey || DEFAULT_AUTH_TOKEN_HEADER_KEY;
|
|
config.authTokenHeaderKey = authOptions.authTokenHeaderKey || DEFAULT_AUTH_TOKEN_HEADER_KEY;
|
|
|
await fs.writeFile(adminUiConfigPath, JSON.stringify(config, null, 2));
|
|
await fs.writeFile(adminUiConfigPath, JSON.stringify(config, null, 2));
|
|
|
|
|
+ Logger.verbose(`Applied configuration to vendure-ui-config.json file`, loggerCtx);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static isDevModeApp(
|
|
|
|
|
+ app?: AdminUiAppConfig | AdminUiAppDevModeConfig,
|
|
|
|
|
+ ): app is AdminUiAppDevModeConfig {
|
|
|
|
|
+ if (!app) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return !!(app as AdminUiAppDevModeConfig).sourcePath;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|