Преглед изворни кода

refactor(admin-ui-plugin): Move compilation out of plugin into ui-devkit

The AdminUiPlugin now is responsible for serving a compiled Admin UI app and proxying that server.
Michael Bromley пре 5 година
родитељ
комит
3e4cfbfd91
34 измењених фајлова са 571 додато и 565 уклоњено
  1. 19 2
      packages/admin-ui-plugin/build.ts
  2. 1 1
      packages/admin-ui-plugin/src/constants.ts
  3. 72 48
      packages/admin-ui-plugin/src/plugin.ts
  4. 0 105
      packages/admin-ui-plugin/src/ui-app-compiler.service.ts
  5. 78 6
      packages/common/src/shared-types.ts
  6. 2 1
      packages/ui-devkit/.gitignore
  7. 8 6
      packages/ui-devkit/package.json
  8. 2 2
      packages/ui-devkit/rollup.config.js
  9. 13 3
      packages/ui-devkit/scaffold/angular.json
  10. 14 0
      packages/ui-devkit/scaffold/ngcc.config.js
  11. 11 10
      packages/ui-devkit/scaffold/src/app.module.ts
  12. 10 21
      packages/ui-devkit/scaffold/src/app.routes.ts
  13. 3 0
      packages/ui-devkit/scaffold/src/environment.prod.ts
  14. 6 0
      packages/ui-devkit/scaffold/src/environment.ts
  15. 1 0
      packages/ui-devkit/scaffold/src/extension.routes.ts
  16. 3 2
      packages/ui-devkit/scaffold/src/main.ts
  17. 0 8
      packages/ui-devkit/scaffold/src/routing/catalog-wrapper.module.ts
  18. 0 8
      packages/ui-devkit/scaffold/src/routing/customer-wrapper.module.ts
  19. 0 8
      packages/ui-devkit/scaffold/src/routing/dashboard-wrapper.module.ts
  20. 0 8
      packages/ui-devkit/scaffold/src/routing/login-wrapper.module.ts
  21. 0 8
      packages/ui-devkit/scaffold/src/routing/marketing-wrapper.module.ts
  22. 0 8
      packages/ui-devkit/scaffold/src/routing/order-wrapper.module.ts
  23. 0 8
      packages/ui-devkit/scaffold/src/routing/settings-wrapper.module.ts
  24. 7 0
      packages/ui-devkit/scaffold/src/shared-extensions.module.ts
  25. 18 0
      packages/ui-devkit/scaffold/src/tsconfig.app.json
  26. 0 0
      packages/ui-devkit/src/client/devkit-client-api.ts
  27. 1 0
      packages/ui-devkit/src/client/index.ts
  28. 0 135
      packages/ui-devkit/src/compiler/common.ts
  29. 299 68
      packages/ui-devkit/src/compiler/compile.ts
  30. 0 1
      packages/ui-devkit/src/compiler/index.ts
  31. 0 94
      packages/ui-devkit/src/compiler/watch.ts
  32. 0 1
      packages/ui-devkit/src/index.ts
  33. 1 1
      packages/ui-devkit/tsconfig.compiler.json
  34. 2 2
      packages/ui-devkit/tsconfig.json

+ 19 - 2
packages/admin-ui-plugin/build.ts

@@ -1,6 +1,23 @@
 /* tslint:disable:no-console */
-import { compileAdminUiApp } from '@vendure/admin-ui/compiler/compile';
+import { execSync, spawn } from 'child_process';
+import fs from 'fs-extra';
 import path from 'path';
 
+const compiledUiDir = path.join(__dirname, 'lib/admin-ui');
 console.log('Building admin-ui from source...');
-compileAdminUiApp(path.join(__dirname, 'lib/admin-ui'), []);
+
+fs.remove(compiledUiDir);
+
+const buildProcess = spawn('yarn', ['run', 'build:app'], {
+    cwd: path.join(__dirname, '../admin-ui'),
+    shell: true,
+    stdio: 'inherit',
+});
+
+buildProcess.on('close', code => {
+    if (code === 0) {
+        fs.copySync(path.join(__dirname, '../admin-ui/dist'), compiledUiDir);
+    } else {
+        console.log('Could not build!');
+    }
+});

+ 1 - 1
packages/admin-ui-plugin/src/constants.ts

@@ -1,4 +1,4 @@
 import path from 'path';
 
-export const UI_PATH = path.join(__dirname, '../admin-ui');
+export const DEFAULT_APP_PATH = path.join(__dirname, '../admin-ui');
 export const loggerCtx = 'AdminUiPlugin';

+ 72 - 48
packages/admin-ui-plugin/src/plugin.ts

@@ -1,6 +1,5 @@
-import { Watcher } from '@vendure/admin-ui/compiler/watch';
 import { DEFAULT_AUTH_TOKEN_HEADER_KEY } from '@vendure/common/lib/shared-constants';
-import { AdminUiConfig, AdminUiExtension, Type } from '@vendure/common/lib/shared-types';
+import { AdminUiApp, AdminUiAppDevMode, AdminUiConfig, Type } from '@vendure/common/lib/shared-types';
 import {
     AuthOptions,
     ConfigService,
@@ -17,8 +16,7 @@ import fs from 'fs-extra';
 import { Server } from 'http';
 import path from 'path';
 
-import { UI_PATH } from './constants';
-import { UiAppCompiler } from './ui-app-compiler.service';
+import { DEFAULT_APP_PATH, loggerCtx } from './constants';
 
 /**
  * @description
@@ -27,6 +25,11 @@ import { UiAppCompiler } from './ui-app-compiler.service';
  * @docsCategory AdminUiPlugin
  */
 export interface AdminUiOptions {
+    /**
+     * @description
+     * The port on which the server will listen. If not
+     */
+    port: number;
     /**
      * @description
      * The hostname of the server serving the static admin ui files.
@@ -36,9 +39,11 @@ export interface AdminUiOptions {
     hostname?: string;
     /**
      * @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?: AdminUiApp | AdminUiAppDevMode;
     /**
      * @description
      * The hostname of the Vendure server which the admin ui will be making API calls
@@ -57,22 +62,6 @@ export interface AdminUiOptions {
      * @default '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 +95,15 @@ export interface AdminUiOptions {
  */
 @VendurePlugin({
     imports: [PluginCommonModule],
-    providers: [UiAppCompiler],
+    providers: [],
     configuration: config => AdminUiPlugin.configure(config),
 })
 export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
     private static options: AdminUiOptions;
     private server: Server;
-    private watcher: Watcher | undefined;
+    private devServerClose: () => void | Promise<void> | undefined;
 
-    constructor(private configService: ConfigService, private appCompiler: UiAppCompiler) {}
+    constructor(private configService: ConfigService) {}
 
     /**
      * @description
@@ -128,19 +117,29 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
     /** @internal */
     static async configure(config: RuntimeVendureConfig): Promise<RuntimeVendureConfig> {
         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({
             handler: createProxyHandler({
-                ...this.options,
+                hostname: this.options.hostname,
+                port,
                 route: 'admin',
                 label: 'Admin UI',
-                basePath: this.options.watch ? 'admin' : undefined,
+                basePath: appWatchMode ? 'admin' : undefined,
             }),
             route,
         });
-        if (this.options.watch) {
+        if (this.isDevModeApp(app)) {
             config.middleware.push({
                 handler: createProxyHandler({
-                    ...this.options,
+                    hostname: this.options.hostname,
+                    port,
                     route: 'sockjs-node',
                     label: 'Admin UI live reload',
                     basePath: 'sockjs-node',
@@ -154,35 +153,52 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
     /** @internal */
     async onVendureBootstrap() {
         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)
+            ? app.sourcePath
+            : (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();
-            adminUiServer.use(express.static(UI_PATH));
+            adminUiServer.use(express.static(adminUiAppPath));
             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);
-            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`);
+                    });
+            } else {
+                await overwriteConfig();
+            }
+        } else {
+            Logger.info(`Compiling Admin UI app in development mode`, loggerCtx);
+            app.compile()
+                .then(overwriteConfig)
+                .then(() => {
+                    Logger.info(`Admin UI successfully compiled and watching for changes...`);
+                });
         }
-        await this.overwriteAdminUiConfig({
-            host: apiHost || 'auto',
-            port: apiPort || 'auto',
-            adminApiPath,
-            authOptions,
-            adminUiConfigPath,
-        });
     }
 
     /** @internal */
     async onVendureClose(): Promise<void> {
-        if (this.watcher) {
-            this.watcher.close();
+        if (this.devServerClose) {
+            await this.devServerClose();
         }
         if (this.server) {
             await new Promise(resolve => this.server.close(() => resolve()));
@@ -213,6 +229,14 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
         config.adminApiPath = adminApiPath;
         config.tokenMethod = authOptions.tokenMethod || 'cookie';
         config.authTokenHeaderKey = authOptions.authTokenHeaderKey || DEFAULT_AUTH_TOKEN_HEADER_KEY;
+        Logger.verbose(`Applying configuration to vendure-ui-config.json file`, loggerCtx);
         await fs.writeFile(adminUiConfigPath, JSON.stringify(config, null, 2));
     }
+
+    private static isDevModeApp(app?: AdminUiApp | AdminUiAppDevMode): app is AdminUiAppDevMode {
+        if (!app) {
+            return false;
+        }
+        return typeof (app as any).close === 'function' && typeof (app as any).sourcePath === 'string';
+    }
 }

+ 0 - 105
packages/admin-ui-plugin/src/ui-app-compiler.service.ts

@@ -1,105 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { compileAdminUiApp, watchAdminUiApp, Watcher } from '@vendure/admin-ui/compiler';
-import { AdminUiExtension } from '@vendure/common/lib/shared-types';
-import { Logger } from '@vendure/core';
-import crypto from 'crypto';
-import fs from 'fs-extra';
-import path from 'path';
-
-import { loggerCtx, UI_PATH } from './constants';
-
-@Injectable()
-export class UiAppCompiler {
-    private readonly hashfile = path.join(__dirname, 'modules-hash.txt');
-
-    watchAdminUiApp(extensions: AdminUiExtension[] | undefined, port: number): Watcher {
-        const extensionsWithId = this.normalizeExtensions(extensions);
-        Logger.info(`Starting Admin UI in Angular dev server on port ${port}`, loggerCtx);
-        return watchAdminUiApp(extensionsWithId, port);
-    }
-
-    async compileAdminUiApp(extensions: AdminUiExtension[] | undefined): Promise<void> {
-        const compiledAppExists = fs.existsSync(path.join(UI_PATH, 'index.html'));
-        const extensionsWithId = this.normalizeExtensions(extensions);
-
-        if (!compiledAppExists || this.extensionModulesHaveChanged(extensionsWithId)) {
-            Logger.info('Compiling Admin UI with extensions...', loggerCtx);
-            await compileAdminUiApp(path.join(__dirname, '../admin-ui'), extensionsWithId);
-            Logger.info('Completed compilation!', loggerCtx);
-        } else {
-            Logger.verbose('Extensions not changed since last run', loggerCtx);
-        }
-    }
-
-    /**
-     * Ensures each extension has an ID. If not defined by the user, a deterministic ID is generated
-     * from a hash of the extension config.
-     */
-    private normalizeExtensions(extensions?: AdminUiExtension[]): Array<Required<AdminUiExtension>> {
-        return (extensions || []).map(e => {
-            if (e.id) {
-                return e as Required<AdminUiExtension>;
-            }
-            const hash = crypto.createHash('sha256');
-            hash.update(JSON.stringify(e));
-            const id = hash.digest('hex');
-            return { staticAssets: [], ...e, id };
-        });
-    }
-
-    /**
-     * Checks whether the extensions configuration or any of the extension module files have been
-     * changed since the last run.
-     */
-    private extensionModulesHaveChanged(extensions: Array<Required<AdminUiExtension>>): boolean {
-        fs.ensureFileSync(this.hashfile);
-        const previousHash = fs.readFileSync(this.hashfile, 'utf-8');
-        if (!previousHash && (!extensions || extensions.length === 0)) {
-            // No extensions are configured and there is no last has,
-            // as when the plugin is newly installed. In this case,
-            // it would be unnecessary to recompile.
-            return false;
-        }
-        const currentHash = this.getExtensionModulesHash(extensions);
-
-        if (currentHash === previousHash) {
-            return false;
-        }
-        fs.writeFileSync(this.hashfile, currentHash, 'utf-8');
-        return true;
-    }
-
-    /**
-     * Generates a hash based on the extensions array as well as the modified time of each file
-     * in the ngModulesPaths.
-     */
-    private getExtensionModulesHash(extensions: Array<Required<AdminUiExtension>>): string {
-        let modifiedDates: string[] = [];
-        for (const extension of extensions) {
-            modifiedDates = [...modifiedDates, ...this.getAllModifiedDates(extension.extensionPath)];
-        }
-        const hash = crypto.createHash('sha256');
-        hash.update(modifiedDates.join('') + JSON.stringify(extensions));
-        return hash.digest('hex');
-    }
-
-    private getAllModifiedDates(dirPath: string): string[] {
-        const modifiedDates: string[] = [];
-        this.visitRecursive(dirPath, filePath => {
-            modifiedDates.push(fs.statSync(filePath).mtimeMs.toString());
-        });
-        return modifiedDates;
-    }
-
-    private visitRecursive(dirPath: string, visitor: (filePath: string) => void) {
-        const files = fs.readdirSync(dirPath);
-        for (const file of files) {
-            const fullPath = path.join(dirPath, file);
-            if (fs.statSync(fullPath).isDirectory()) {
-                this.visitRecursive(fullPath, visitor);
-            } else {
-                visitor(fullPath);
-            }
-        }
-    }
-}

+ 78 - 6
packages/common/src/shared-types.ts

@@ -90,6 +90,49 @@ export interface AdminUiConfig {
     authTokenHeaderKey: string;
 }
 
+/**
+ * @description
+ * Configures the path to a custom-build of the Admin UI app.
+ *
+ * @docsCategory common
+ */
+export interface AdminUiApp {
+    /**
+     * @description
+     * The path to the compiled admin ui app files. If not specified, an internal
+     * default build is used. This path should contain the `vendure-ui-config.json` file,
+     * index.html, the compiled js bundles etc.
+     */
+    path: string;
+    compile?: () => Promise<void>;
+}
+
+/**
+ * @description
+ * Information about the Admin UI app dev server.
+ *
+ * @docsCategory common
+ */
+export interface AdminUiAppDevMode {
+    /**
+     * @description
+     * The path to the uncompiled ui app source files. This path should contain the `vendure-ui-config.json` file.
+     */
+    sourcePath: string;
+    /**
+     * @description
+     * The port on which the dev server is listening. Overrides the value set by `AdminUiOptions.port`.
+     */
+    port: number;
+    compile: () => Promise<void>;
+    /**
+     * @description
+     * If this function is specified, it will be invoked when the plugin closes. Intended for
+     * ensuring the dev server is shut down as part of the AdminUiPlugin lifecycle.
+     */
+    onClose?: () => void | Promise<void>;
+}
+
 /**
  * @description
  * Defines extensions to the Admin UI application by specifying additional
@@ -120,7 +163,7 @@ export interface AdminUiExtension {
      * @description
      * One or more Angular modules which extend the default Admin UI.
      */
-    ngModules: AdminUiExtensionModule[];
+    ngModules: Array<AdminUiExtensionSharedModule | AdminUiExtensionLazyModule>;
 
     /**
      * @description
@@ -136,17 +179,46 @@ export interface AdminUiExtension {
  *
  * @docsCategory AdminUiPlugin
  */
-export interface AdminUiExtensionModule {
+export interface AdminUiExtensionSharedModule {
     /**
      * @description
-     * Lazy modules are lazy-loaded at the `/extensions/` route and should be used for
-     * modules which define new views for the Admin UI.
-     *
      * Shared modules are directly imported into the main AppModule of the Admin UI
      * and should be used to declare custom form components and define custom
      * navigation items.
      */
-    type: 'shared' | 'lazy';
+    type: 'shared';
+    /**
+     * @description
+     * The name of the file containing the extension module class.
+     */
+    ngModuleFileName: string;
+    /**
+     * @description
+     * The name of the extension module class.
+     */
+    ngModuleName: string;
+}
+
+/**
+ * @description
+ * Configuration defining a single NgModule with which to extend the Admin UI.
+ *
+ * @docsCategory AdminUiPlugin
+ */
+export interface AdminUiExtensionLazyModule {
+    /**
+     * @description
+     * Lazy modules are lazy-loaded at the `/extensions/` route and should be used for
+     * modules which define new views for the Admin UI.
+     */
+    type: 'lazy';
+    /**
+     * @description
+     * The route specifies the route at which the module will be lazy-loaded. E.g. a value
+     * of `'foo'` will cause the module to lazy-load when the `/extensions/foo` route
+     * is activated.
+     */
+    route: string;
     /**
      * @description
      * The name of the file containing the extension module class.

+ 2 - 1
packages/ui-devkit/.gitignore

@@ -1,2 +1,3 @@
-lib
+/client
+/compiler
 node_modules

+ 8 - 6
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "0.9.8",
+  "version": "0.9.0",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -14,18 +14,20 @@
     "lib": "lib"
   },
   "files": [
-    "lib",
+    "client",
+    "compiler",
     "scaffold"
   ],
-  "main": "lib/index.js",
-  "types": "lib/index.d.ts",
+  "main": "client/index.js",
+  "types": "client/index.d.ts",
   "repository": {
     "type": "git",
     "url": "git+https://github.com/vendure-ecommerce/vendure.git"
   },
   "scripts": {
-    "build": "rimraf ./lib && rollup -c rollup.config.js --configProduction",
-    "build:compiler": "tsc -p tsconfig.compiler.json",
+    "build:client": "rimraf ./client && rollup -c rollup.config.js --configProduction",
+    "build:compiler": "rimraf ./compiler && tsc -p tsconfig.compiler.json",
+    "build": "yarn build:client && yarn build:compiler",
     "watch": "rimraf ./lib && rollup -c rollup.config.js -w",
     "lint": "tslint --fix --project ./"
   },

+ 2 - 2
packages/ui-devkit/rollup.config.js

@@ -6,9 +6,9 @@ import resolve from '@rollup/plugin-node-resolve';
 export default commandLineArgs => {
     const isProd = commandLineArgs.configProduction === true;
     return {
-        input: 'src/index.ts',
+        input: 'src/client/index.ts',
         output: {
-            dir: 'lib',
+            dir: 'client',
             format: 'umd',
             name: 'VendureUiDevkit',
         },

+ 13 - 3
packages/ui-devkit/scaffold/angular.json

@@ -28,10 +28,14 @@
               "src/favicon.ico",
               "src/vendure-ui-config.json",
               "src/assets",
-              "src/i18n-messages"
+              "src/i18n-messages",
+              {
+                "glob": "**/*.*",
+                "input": "src/extensions/static-assets",
+                "output": "assets"
+              }
             ],
             "styles": [
-              "../../node_modules/@clr/icons/clr-icons.min.css",
               "src/styles/styles.scss"
             ],
             "stylePreprocessorOptions": {
@@ -49,6 +53,12 @@
                   "maximumWarning": "6kb"
                 }
               ],
+              "fileReplacements": [
+                {
+                  "replace": "src/environment.ts",
+                  "with": "src/environment.prod.ts"
+                }
+              ],
               "optimization": true,
               "outputHashing": "all",
               "sourceMap": true,
@@ -57,7 +67,7 @@
               "aot": true,
               "extractLicenses": true,
               "vendorChunk": false,
-              "buildOptimizer": true
+              "buildOptimizer": false
             }
           }
         },

+ 14 - 0
packages/ui-devkit/scaffold/ngcc.config.js

@@ -0,0 +1,14 @@
+// Prevents false positive warnings from the ng compatibility compiler.
+// See https://github.com/angular/angular/pull/35683
+module.exports = {
+    packages: {
+        '@vendure/admin-ui': {
+            ignorableDeepImportMatchers: [
+                /@vendure\/common\//,
+                /@clr\/icons\//,
+                /@webcomponents\//,
+                /graphql\//,
+            ]
+        },
+    }
+};

+ 11 - 10
packages/ui-devkit/scaffold/src/app.module.ts

@@ -1,18 +1,19 @@
 import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
+import { Route, RouterModule } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { AppComponent, AppComponentModule, CoreModule } from '@vendure/admin-ui/core';
 
 import { routes } from './app.routes';
-
-// Using TS "import" results in the following error when building with the Angular CLI:
-// "Error: <path>\node_modules\@vendure\admin-ui\library\app\app.module.d.ts is missing from the
-// TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property."
-// tslint:disable:no-var-requires
-declare const require: any;
-const { AppComponent, CoreModule } = require('@vendure/admin-ui');
+import { SharedExtensionsModule } from './shared-extensions.module';
 
 @NgModule({
-    declarations: [AppComponent],
-    imports: [RouterModule.forRoot(routes, { useHash: false }), CoreModule],
+    declarations: [],
+    imports: [
+        AppComponentModule,
+        RouterModule.forRoot(routes, { useHash: false }),
+        CoreModule,
+        SharedExtensionsModule,
+    ],
     bootstrap: [AppComponent],
 })
 export class AppModule {}

+ 10 - 21
packages/ui-devkit/scaffold/src/app.routes.ts

@@ -1,18 +1,11 @@
 import { Route } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { AppShellComponent, AuthGuard } from '@vendure/admin-ui/core';
 
-// Using TS "import" results in the following error when building with the Angular CLI:
-// "Error: <path>\node_modules\@vendure\admin-ui\library\app\app.module.d.ts is missing from the
-// TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property."
-// tslint:disable:no-var-requires
-declare const require: any;
-const { AppShellComponent, AuthGuard } = require('@vendure/admin-ui');
+import { extensionRoutes } from './extension.routes';
 
 export const routes: Route[] = [
-    {
-        path: 'login',
-        loadChildren: () => import('./routing/login-wrapper.module').then(m => m.LoginWrapperModule),
-    },
+    { path: 'login', loadChildren: () => import('@vendure/admin-ui/login').then(m => m.LoginModule) },
     {
         path: '',
         canActivate: [AuthGuard],
@@ -24,33 +17,29 @@ export const routes: Route[] = [
             {
                 path: '',
                 pathMatch: 'full',
-                loadChildren: () =>
-                    import('./routing/dashboard-wrapper.module').then(m => m.DashboardWrapperModule),
+                loadChildren: () => import('@vendure/admin-ui/dashboard').then(m => m.DashboardModule),
             },
             {
                 path: 'catalog',
-                loadChildren: () =>
-                    import('./routing/catalog-wrapper.module').then(m => m.CatalogWrapperModule),
+                loadChildren: () => import('@vendure/admin-ui/catalog').then(m => m.CatalogModule),
             },
             {
                 path: 'customer',
-                loadChildren: () =>
-                    import('./routing/customer-wrapper.module').then(m => m.CustomerWrapperModule),
+                loadChildren: () => import('@vendure/admin-ui/customer').then(m => m.CustomerModule),
             },
             {
                 path: 'orders',
-                loadChildren: () => import('./routing/order-wrapper.module').then(m => m.OrderWrapperModule),
+                loadChildren: () => import('@vendure/admin-ui/order').then(m => m.OrderModule),
             },
             {
                 path: 'marketing',
-                loadChildren: () =>
-                    import('./routing/marketing-wrapper.module').then(m => m.MarketingWrapperModule),
+                loadChildren: () => import('@vendure/admin-ui/marketing').then(m => m.MarketingModule),
             },
             {
                 path: 'settings',
-                loadChildren: () =>
-                    import('./routing/settings-wrapper.module').then(m => m.SettingsWrapperModule),
+                loadChildren: () => import('@vendure/admin-ui/settings').then(m => m.SettingsModule),
             },
         ],
+        ...extensionRoutes,
     },
 ];

+ 3 - 0
packages/ui-devkit/scaffold/src/environment.prod.ts

@@ -0,0 +1,3 @@
+export const environment = {
+    production: true,
+};

+ 6 - 0
packages/ui-devkit/scaffold/src/environment.ts

@@ -0,0 +1,6 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+export const environment = {
+    production: false,
+};

+ 1 - 0
packages/ui-devkit/scaffold/src/extension.routes.ts

@@ -0,0 +1 @@
+export const extensionRoutes = [];

+ 3 - 2
packages/ui-devkit/scaffold/src/main.ts

@@ -1,16 +1,17 @@
 import { enableProdMode } from '@angular/core';
 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { loadAppConfig } from '@vendure/admin-ui/core';
 
 import { AppModule } from './app.module';
+import { environment } from './environment';
 
 // Using TS "import" results in the following error when building with the Angular CLI:
 // "Error: <path>\node_modules\@vendure\admin-ui\library\app\app.module.d.ts is missing from the
 // TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property."
 // tslint:disable:no-var-requires
 declare const require: any;
-const { loadAppConfig } = require('@vendure/admin-ui');
 
-if (false) {
+if (environment.production) {
     enableProdMode();
 }
 

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/catalog-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { CatalogModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [CatalogModule],
-})
-export class CatalogWrapperModule {}

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/customer-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { CustomerModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [CustomerModule],
-})
-export class CustomerWrapperModule {}

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/dashboard-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { DashboardModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [DashboardModule],
-})
-export class DashboardWrapperModule {}

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/login-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { LoginModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [LoginModule],
-})
-export class LoginWrapperModule {}

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/marketing-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { MarketingModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [MarketingModule],
-})
-export class MarketingWrapperModule {}

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/order-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { OrderModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [OrderModule],
-})
-export class OrderWrapperModule {}

+ 0 - 8
packages/ui-devkit/scaffold/src/routing/settings-wrapper.module.ts

@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { SettingsModule } from '@vendure/admin-ui';
-
-@NgModule({
-    imports: [SettingsModule],
-})
-export class SettingsWrapperModule {}

+ 7 - 0
packages/ui-devkit/scaffold/src/shared-extensions.module.ts

@@ -0,0 +1,7 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+@NgModule({
+    imports: [CommonModule],
+})
+export class SharedExtensionsModule {}

+ 18 - 0
packages/ui-devkit/scaffold/src/tsconfig.app.json

@@ -0,0 +1,18 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/app"
+  },
+  "files": [
+    "main.ts",
+    "polyfills.ts"
+  ],
+  "include": [
+    "**/*.d.ts"
+  ],
+  "angularCompilerOptions": {
+    "strictMetadataEmit": true,
+    "fullTemplateTypeCheck": true,
+    "strictInjectionParameters": true
+  }
+}

+ 0 - 0
packages/ui-devkit/src/devkit-api.ts → packages/ui-devkit/src/client/devkit-client-api.ts


+ 1 - 0
packages/ui-devkit/src/client/index.ts

@@ -0,0 +1 @@
+export * from './devkit-client-api';

+ 0 - 135
packages/ui-devkit/src/compiler/common.ts

@@ -1,135 +0,0 @@
-import { AdminUiExtension, AdminUiExtensionModule } from '@vendure/common/lib/shared-types';
-import * as fs from 'fs-extra';
-import * as path from 'path';
-
-const EXTENSIONS_DIR = path.join(__dirname, '../src/app/extensions');
-const EXTENSIONS_MODULES_DIR = 'modules';
-const STATIC_ASSETS_OUTPUT_DIR = path.join(EXTENSIONS_DIR, '__static-assets__');
-const lazyExtensionsModuleFile = path.join(EXTENSIONS_DIR, 'lazy-extensions.module.ts');
-const sharedExtensionsModuleFile = path.join(EXTENSIONS_DIR, 'shared-extensions.module.ts');
-
-/**
- * Returns true if currently being executed from inside the Vendure monorepo.
- */
-export function isInVendureMonorepo(): boolean {
-    return fs.existsSync(path.join(__dirname, '../../dev-server'));
-}
-
-/**
- * Deletes the contents of the /modules directory, which contains the plugin
- * extension modules copied over during the last compilation.
- */
-export function deleteExistingExtensionModules() {
-    fs.removeSync(path.join(EXTENSIONS_DIR, EXTENSIONS_MODULES_DIR));
-    fs.removeSync(STATIC_ASSETS_OUTPUT_DIR);
-}
-
-/**
- * Copies all files from the extensionPaths of the configured extensions into the
- * admin-ui source tree.
- */
-export function copyExtensionModules(extensions: Array<Required<AdminUiExtension>>) {
-    for (const extension of extensions) {
-        const dirName = path.basename(path.dirname(extension.extensionPath));
-        const dest = getModuleOutputDir(extension);
-        fs.copySync(extension.extensionPath, dest);
-        if (Array.isArray(extension.staticAssets)) {
-            for (const asset of extension.staticAssets) {
-                copyStaticAsset(asset);
-            }
-        }
-    }
-}
-
-/**
- * Copy the @vendure/ui-devkit files to the static assets dir.
- */
-export function copyUiDevkit() {
-    const devkitDir = path.join(STATIC_ASSETS_OUTPUT_DIR, 'devkit');
-    fs.ensureDirSync(devkitDir);
-    fs.copySync(require.resolve('@vendure/ui-devkit'), path.join(devkitDir, 'ui-devkit.js'));
-}
-
-/**
- * Copies over any files defined by the extensions' `staticAssets` array to the shared
- * static assets directory. When the app is built by the ng cli, this assets directory is
- * the copied over to the final static assets location (i.e. http://domain/admin/assets/)
- */
-export function copyStaticAsset(staticAssetPath: string) {
-    const stats = fs.statSync(staticAssetPath);
-    if (stats.isDirectory()) {
-        const assetDirname = path.basename(staticAssetPath);
-        fs.copySync(staticAssetPath, path.join(STATIC_ASSETS_OUTPUT_DIR, assetDirname));
-    } else {
-        fs.copySync(staticAssetPath, STATIC_ASSETS_OUTPUT_DIR);
-    }
-}
-
-export function getModuleOutputDir(extension: Required<AdminUiExtension>): string {
-    return path.join(EXTENSIONS_DIR, EXTENSIONS_MODULES_DIR, extension.id);
-}
-
-export function createExtensionsModules(extensions: Array<Required<AdminUiExtension>>) {
-    const removeTsExtension = (filename: string): string => filename.replace(/\.ts$/, '');
-    const importPath = (id: string, fileName: string): string =>
-        `./${EXTENSIONS_MODULES_DIR}/${id}/${removeTsExtension(fileName)}`;
-
-    for (const type of ['lazy', 'shared'] as const) {
-        const modulesOfType = extensions
-            .reduce(
-                (modules, e) => [...modules, ...e.ngModules.map(m => ({ id: e.id, module: m }))],
-                [] as Array<{ id: string; module: AdminUiExtensionModule }>,
-            )
-            .filter(m => m.module.type === type)
-            .map(e => ({
-                className: e.module.ngModuleName,
-                path: importPath(e.id, e.module.ngModuleFileName),
-            }));
-        const source = generateExtensionModuleTsSource(type, modulesOfType);
-        const filePath = type === 'lazy' ? lazyExtensionsModuleFile : sharedExtensionsModuleFile;
-        fs.writeFileSync(filePath, source, 'utf-8');
-    }
-}
-
-export function restoreOriginalExtensionsModule() {
-    restoreExtensionsModules();
-}
-
-/**
- * Restores the placeholder ExtensionsModule file from a template.
- */
-export function restoreExtensionsModules() {
-    for (const type of ['lazy', 'shared'] as const) {
-        const source = generateExtensionModuleTsSource(type, []);
-        const filePath = type === 'lazy' ? lazyExtensionsModuleFile : sharedExtensionsModuleFile;
-        fs.writeFileSync(filePath, source, 'utf-8');
-    }
-}
-
-function generateExtensionModuleTsSource(
-    type: 'shared' | 'lazy',
-    modules: Array<{ className: string; path: string }>,
-): string {
-    return `/** This file is generated by the createExtensionsModules() function in devkit/common.ts. Do not edit. */
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-${
-    modules.length === 0
-        ? ''
-        : '\n' + modules.map(e => `import { ${e.className} } from '${e.path}';`).join('\n')
-}
-/**
- * This is a placeholder module for UI extensions provided by the AdminUiPlugin \`extensions\` option.
- * When the {@link createExtensionsModules} function is executed, this module gets temporarily replaced
- * by a generated module which includes all of the configured extension modules.
- */
-@NgModule({
-    imports: [
-        CommonModule,${
-            modules.length === 0 ? '' : '\n' + modules.map(e => '        ' + e.className + ',').join('\n')
-        }
-    ],
-})
-export class ${type === 'lazy' ? 'Lazy' : 'Shared'}ExtensionsModule {}
-`;
-}

+ 299 - 68
packages/ui-devkit/src/compiler/compile.ts

@@ -1,91 +1,322 @@
-import { AdminUiExtension } from '@vendure/common/lib/shared-types';
-import { spawn } from 'child_process';
+import {
+    AdminUiApp,
+    AdminUiAppDevMode,
+    AdminUiExtension,
+    AdminUiExtensionLazyModule,
+    AdminUiExtensionSharedModule,
+} from '@vendure/common/lib/shared-types';
+import { ChildProcess, execSync, spawn } from 'child_process';
+import { FSWatcher, watch as chokidarWatch } from 'chokidar';
+import { createHash } from 'crypto';
 import * as fs from 'fs-extra';
 import * as path from 'path';
 
-import {
-    copyExtensionModules,
-    copyUiDevkit,
-    createExtensionsModules,
-    deleteExistingExtensionModules,
-    isInVendureMonorepo,
-    restoreOriginalExtensionsModule,
-} from './common';
+const STATIC_ASSETS_OUTPUT_DIR = 'static-assets';
+const MODULES_OUTPUT_DIR = 'src/extensions';
+const EXTENSION_ROUTES_FILE = 'src/extension.routes.ts';
+const SHARED_EXTENSIONS_FILE = 'src/shared-extensions.module.ts';
+
+export interface UiExtensionCompilerOptions {
+    /**
+     * @description
+     * The directory into which the sources for the extended Admin UI will be copied.
+     */
+    outputPath: string;
+    /**
+     * @description
+     * An 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 compile 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;
+    /**
+     * @description
+     * In watch mode, allows the port of the dev server to be specified. Defaults to the Angular CLI default
+     * of `4200`.
+     *
+     * @default 4200 | undefined
+     */
+    watchPort?: number;
+}
 
 /**
  * Builds the admin-ui app using the Angular CLI `ng build --prod` command.
  */
-/*export function compileAdminUiApp(outputPath: string, extensions: Array<Required<AdminUiExtension>>) {
-    const cwd = path.join(__dirname, '..');
-    const relativeOutputPath = path.relative(cwd, outputPath);
-    return new Promise((resolve, reject) => {
-        restoreOriginalExtensionsModule();
-        deleteExistingExtensionModules();
-        copyExtensionModules(extensions);
-        copyUiDevkit();
-        createExtensionsModules(extensions);
-
-        const config = isInVendureMonorepo() ? 'plugin-dev' : 'plugin';
-        const buildProcess = spawn(
-            'yarn',
-            ['ng', 'build', `-c=${config}`, `--outputPath=${relativeOutputPath}`],
-            {
-                cwd,
+export function compileUiExtensions({
+    outputPath,
+    watch,
+    watchPort,
+    extensions,
+}: UiExtensionCompilerOptions): AdminUiApp | AdminUiAppDevMode {
+    if (watch) {
+        return runWatchMode(outputPath, watchPort || 4200, extensions);
+    } else {
+        return runCompileMode(outputPath, extensions);
+    }
+}
+
+function runCompileMode(outputPath: string, extensions: AdminUiExtension[]): AdminUiApp {
+    const cmd = shouldUseYarn() ? 'yarn' : 'npm';
+    const distPath = path.join(outputPath, 'dist');
+
+    const compile = () =>
+        new Promise<void>((resolve, reject) => {
+            setupScaffold(outputPath, extensions);
+            const buildProcess = spawn(cmd, ['run', 'build', `--outputPath=${distPath}`], {
+                cwd: outputPath,
                 shell: true,
                 stdio: 'inherit',
-            },
-        );
-        buildProcess.on('close', code => {
-            if (code === 0) {
-                resolve();
-            } else {
-                reject(code);
-            }
+            });
+
+            buildProcess.on('close', code => {
+                if (code !== 0) {
+                    reject(code);
+                } else {
+                    resolve();
+                }
+            });
         });
-        buildProcess.on('error', err => {
-            reject(err);
+
+    return {
+        path: distPath,
+        compile,
+    };
+}
+
+function runWatchMode(outputPath: string, port: number, extensions: AdminUiExtension[]): AdminUiAppDevMode {
+    const cmd = shouldUseYarn() ? 'yarn' : 'npm';
+    const devkitPath = require.resolve('@vendure/ui-devkit');
+    let buildProcess: ChildProcess;
+    let watcher: FSWatcher | undefined;
+    const compile = () =>
+        new Promise<void>((resolve, reject) => {
+            setupScaffold(outputPath, extensions);
+            const normalizedExtensions = normalizeExtensions(extensions);
+            buildProcess = spawn(cmd, ['run', 'start', `--port=${port}`, `--poll=1000`], {
+                cwd: outputPath,
+                shell: true,
+                stdio: 'inherit',
+            });
+
+            for (const extension of normalizedExtensions) {
+                if (!watcher) {
+                    watcher = chokidarWatch(extension.extensionPath, {
+                        depth: 4,
+                        ignored: '**/node_modules/',
+                    });
+                } else {
+                    watcher.add(extension.extensionPath);
+                }
+            }
+
+            if (watcher) {
+                // watch the ui-devkit package files too
+                watcher.add(devkitPath);
+            }
+
+            if (watcher) {
+                watcher.on('change', filePath => {
+                    const extension = normalizedExtensions.find(e => filePath.includes(e.extensionPath));
+                    if (extension) {
+                        if (extension.staticAssets) {
+                            for (const assetPath of extension.staticAssets) {
+                                if (filePath.includes(assetPath)) {
+                                    copyStaticAsset(outputPath, assetPath);
+                                    return;
+                                }
+                            }
+                        }
+                        const outputDir = path.join(outputPath, MODULES_OUTPUT_DIR, extension.id);
+                        const filePart = path.relative(extension.extensionPath, filePath);
+                        const dest = path.join(outputDir, filePart);
+                        fs.copyFile(filePath, dest);
+                    }
+                    if (filePath.includes(devkitPath)) {
+                        copyUiDevkit(outputPath);
+                    }
+                });
+            }
         });
-    }).finally(() => {
-        restoreOriginalExtensionsModule();
+
+    const close = () => {
+        if (watcher) {
+            watcher.close();
+        }
+        buildProcess.kill();
+    };
+
+    process.on('SIGINT', close);
+    return { sourcePath: outputPath, port, onClose: close, compile };
+}
+
+function setupScaffold(outputPath: string, extensions: AdminUiExtension[]) {
+    deleteExistingExtensionModules(outputPath);
+    copySourceIfNotExists(outputPath);
+    copyExtensionModules(outputPath, normalizeExtensions(extensions));
+    copyUiDevkit(outputPath);
+}
+
+/**
+ * Deletes the contents of the /modules directory, which contains the plugin
+ * extension modules copied over during the last compilation.
+ */
+export function deleteExistingExtensionModules(outputPath: string) {
+    fs.removeSync(path.join(outputPath, MODULES_OUTPUT_DIR));
+}
+
+/**
+ * Ensures each extension has an ID. If not defined by the user, a deterministic ID is generated
+ * from a hash of the extension config.
+ */
+function normalizeExtensions(extensions?: AdminUiExtension[]): Array<Required<AdminUiExtension>> {
+    return (extensions || []).map(e => {
+        if (e.id) {
+            return e as Required<AdminUiExtension>;
+        }
+        const hash = createHash('sha256');
+        hash.update(JSON.stringify(e));
+        const id = hash.digest('hex');
+        return { staticAssets: [], ...e, id };
     });
-}*/
+}
+
+/**
+ * Copies all files from the extensionPaths of the configured extensions into the
+ * admin-ui source tree.
+ */
+export function copyExtensionModules(outputPath: string, extensions: Array<Required<AdminUiExtension>>) {
+    const extensionRoutesSource = generateLazyExtensionRoutes(extensions);
+    fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8');
+    const sharedExtensionModulesSource = generateSharedExtensionModule(extensions);
+    fs.writeFileSync(path.join(outputPath, SHARED_EXTENSIONS_FILE), sharedExtensionModulesSource, 'utf8');
+
+    for (const extension of extensions) {
+        const dirName = path.basename(path.dirname(extension.extensionPath));
+        const dest = path.join(outputPath, MODULES_OUTPUT_DIR, extension.id);
+        fs.copySync(extension.extensionPath, dest);
+        if (Array.isArray(extension.staticAssets)) {
+            for (const asset of extension.staticAssets) {
+                copyStaticAsset(outputPath, asset);
+            }
+        }
+    }
+}
+
+function generateLazyExtensionRoutes(extensions: Array<Required<AdminUiExtension>>): string {
+    const routes: string[] = [];
+    for (const extension of extensions as Array<Required<AdminUiExtension>>) {
+        for (const module of extension.ngModules) {
+            if (module.type === 'lazy') {
+                routes.push(`  {
+    path: module.route,
+    loadChildren: () => import('${getModuleFilePath(extension.id, module)}').then(m => m.${
+                    module.ngModuleName
+                }),
+  }`);
+            }
+        }
+    }
+    return `export const extensionRoutes = [${routes.join(',\n')}];\n`;
+}
+
+function generateSharedExtensionModule(extensions: Array<Required<AdminUiExtension>>) {
+    return `import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+${extensions.map(e =>
+    e.ngModules
+        .filter(m => m.type === 'shared')
+        .map(m => `import { ${m.ngModuleName} } from '${getModuleFilePath(e.id, m)}';`)
+        .join('\n'),
+)}
+
+@NgModule({
+    imports: [CommonModule, ${extensions.map(e =>
+        e.ngModules
+            .filter(m => m.type === 'shared')
+            .map(m => m.ngModuleName)
+            .join(', '),
+    )}],
+})
+export class SharedExtensionsModule {}
+`;
+}
+
+function getModuleFilePath(
+    id: string,
+    module: AdminUiExtensionLazyModule | AdminUiExtensionSharedModule,
+): string {
+    return `./extensions/${id}/${path.basename(module.ngModuleFileName, 'ts')}`;
+}
+
+/**
+ * Copies over any files defined by the extensions' `staticAssets` array to the shared
+ * static assets directory. When the app is built by the ng cli, this assets directory is
+ * the copied over to the final static assets location (i.e. http://domain/admin/assets/)
+ */
+export function copyStaticAsset(outputPath: string, staticAssetPath: string) {
+    const stats = fs.statSync(staticAssetPath);
+    if (stats.isDirectory()) {
+        const assetDirname = path.basename(staticAssetPath);
+        fs.copySync(staticAssetPath, path.join(outputPath, STATIC_ASSETS_OUTPUT_DIR, assetDirname));
+    } else {
+        fs.copySync(staticAssetPath, path.join(outputPath, STATIC_ASSETS_OUTPUT_DIR));
+    }
+}
+
+/**
+ * Copy the @vendure/ui-devkit files to the static assets dir.
+ */
+export function copyUiDevkit(outputPath: string) {
+    const devkitDir = path.join(outputPath, STATIC_ASSETS_OUTPUT_DIR, 'devkit');
+    fs.ensureDirSync(devkitDir);
+    fs.copySync(require.resolve('@vendure/ui-devkit'), path.join(devkitDir, 'ui-devkit.js'));
+}
+
+/**
+ * Copy the Admin UI sources & static assets to the outputPath if it does not already
+ * exists there.
+ */
+function copySourceIfNotExists(outputPath: string) {
+    const angularJsonFile = path.join(outputPath, 'angular.json');
+    const indexFile = path.join(outputPath, '/src/index.html');
+    if (fs.existsSync(angularJsonFile) && fs.existsSync(indexFile)) {
+        return;
+    }
+    const scaffoldDir = path.join(__dirname, '../scaffold');
+    const adminUiSrc = path.join(__dirname, '../../admin-ui/static');
+
+    if (!fs.existsSync(scaffoldDir)) {
+        throw new Error(`Could not find the admin ui scaffold files at ${scaffoldDir}`);
+    }
+    if (!fs.existsSync(adminUiSrc)) {
+        throw new Error(`Could not find the @vendure/admin-ui sources. Looked in ${adminUiSrc}`);
+    }
 
-export function compileAdminUiWithExtensions({
-    outputPath,
-    watch,
-    extensions,
-}: {
-    outputPath: string;
-    watch?: boolean;
-    extensions: Array<Required<AdminUiExtension>>;
-}) {
     // copy scaffold
     fs.removeSync(outputPath);
     fs.ensureDirSync(outputPath);
-    fs.copySync(path.join(__dirname, '../../scaffold'), outputPath);
+    fs.copySync(scaffoldDir, outputPath);
 
     // copy source files from admin-ui package
-    const adminUiSrc = path.join(__dirname, '../../../admin-ui/library/src');
     const outputSrc = path.join(outputPath, 'src');
     fs.ensureDirSync(outputSrc);
     fs.copySync(adminUiSrc, outputSrc);
+}
 
-    return new Promise((resolve, reject) => {
-        const buildProcess = spawn('yarn', ['start'], {
-            cwd: outputPath,
-            shell: true,
-            stdio: 'inherit',
-        });
-
-        buildProcess.on('close', code => {
-            if (code === 0) {
-                resolve();
-            } else {
-                reject(code);
-            }
-        });
-        buildProcess.on('error', err => {
-            reject(err);
-        });
-    });
+export function shouldUseYarn(): boolean {
+    try {
+        execSync('yarnpkg --version', { stdio: 'ignore' });
+        return true;
+    } catch (e) {
+        return false;
+    }
 }

+ 0 - 1
packages/ui-devkit/src/compiler/index.ts

@@ -1,2 +1 @@
 export * from './compile';
-export * from './watch';

+ 0 - 94
packages/ui-devkit/src/compiler/watch.ts

@@ -1,94 +0,0 @@
-import { AdminUiExtension } from '@vendure/common/lib/shared-types';
-import { spawn } from 'child_process';
-import { FSWatcher, watch } from 'chokidar';
-import * as fs from 'fs-extra';
-import * as path from 'path';
-
-import {
-    copyExtensionModules,
-    copyStaticAsset,
-    copyUiDevkit,
-    createExtensionsModules,
-    deleteExistingExtensionModules,
-    getModuleOutputDir,
-    isInVendureMonorepo,
-    restoreExtensionsModules,
-    restoreOriginalExtensionsModule,
-} from './common';
-
-export type Watcher = {
-    close: () => void;
-};
-
-/**
- * Starts the admin ui app in watch mode using the Angular CLI `ng serve` command. Also watches
- * the individual files of any configured extensions and copies them upon change, triggering a
- * rebuild of the Angular app.
- */
-export function watchAdminUiApp(extensions: Array<Required<AdminUiExtension>>, port: number): Watcher {
-    const cwd = path.join(__dirname, '..');
-    restoreExtensionsModules();
-    deleteExistingExtensionModules();
-    copyExtensionModules(extensions);
-    copyUiDevkit();
-    createExtensionsModules(extensions);
-
-    const config = isInVendureMonorepo() ? 'plugin-dev' : 'plugin';
-    const buildProcess = spawn('yarn', ['ng', 'serve', `-c=${config}`, `--port=${port}`, `--poll=1000`], {
-        cwd,
-        shell: true,
-        stdio: 'inherit',
-    });
-    const devkitPath = require.resolve('@vendure/ui-devkit');
-
-    let watcher: FSWatcher | undefined;
-    for (const extension of extensions) {
-        if (!watcher) {
-            watcher = watch(extension.extensionPath, {
-                depth: 4,
-                ignored: '**/node_modules/',
-            });
-        } else {
-            watcher.add(extension.extensionPath);
-        }
-    }
-
-    if (watcher) {
-        // watch the ui-devkit package files too
-        watcher.add(devkitPath);
-    }
-
-    if (watcher) {
-        watcher.on('change', filePath => {
-            const extension = extensions.find(e => filePath.includes(e.extensionPath));
-            if (extension) {
-                if (extension.staticAssets) {
-                    for (const assetPath of extension.staticAssets) {
-                        if (filePath.includes(assetPath)) {
-                            copyStaticAsset(assetPath);
-                            return;
-                        }
-                    }
-                }
-                const outputDir = getModuleOutputDir(extension);
-                const filePart = path.relative(extension.extensionPath, filePath);
-                const dest = path.join(outputDir, filePart);
-                fs.copyFile(filePath, dest);
-            }
-            if (filePath.includes(devkitPath)) {
-                copyUiDevkit();
-            }
-        });
-    }
-
-    const close = () => {
-        if (watcher) {
-            watcher.close();
-        }
-        buildProcess.kill();
-        restoreOriginalExtensionsModule();
-    };
-
-    process.on('SIGINT', close);
-    return { close };
-}

+ 0 - 1
packages/ui-devkit/src/index.ts

@@ -1 +0,0 @@
-export * from './devkit-api';

+ 1 - 1
packages/ui-devkit/tsconfig.compiler.json

@@ -2,7 +2,7 @@
   "compileOnSave": false,
   "compilerOptions": {
     "baseUrl": "./",
-    "outDir": "./lib/compiler",
+    "outDir": "./compiler",
     "sourceMap": true,
     "module": "commonjs",
     "declaration": true,

+ 2 - 2
packages/ui-devkit/tsconfig.json

@@ -3,7 +3,7 @@
   "compilerOptions": {
     "baseUrl": "./",
     "importHelpers": true,
-    "outDir": "./lib",
+    "outDir": "./client",
     "sourceMap": true,
     "declaration": true,
     "moduleResolution": "node",
@@ -19,6 +19,6 @@
     ]
   },
   "files": [
-    "./src/index.ts"
+    "./src/client/index.ts"
   ]
 }