Browse Source

feat(admin-ui-plugin): Allow UI extensions to contain multiple modules

BREAKING CHANGE: The API for configuring Admin UI extensions has changed to allow a single extension to define multiple Angular NgModules. This arose as a requirement when working on more complex UI extensions which e.g. define both a shared and a lazy module which share code. Such an arrangement was not possible using the existing API.

Here's how to update:
```TypeScript
// Old API
extensions: [
    {
        type: 'lazy',
        ngModulePath: path.join(__dirname, 'ui-extensions/greeter'),
        ngModuleFileName: 'greeter-extension.module.ts',
        ngModuleName: 'GreeterModule',
    }
],

// New API
extensions: [
    {
        extensionPath: path.join(__dirname, 'ui-extensions/greeter'),
        ngModules: [{
            type: 'lazy',
            ngModuleFileName: 'greeter-extension.module.ts',
            ngModuleName: 'GreeterModule',
        }],
    }
],
```
Michael Bromley 6 years ago
parent
commit
b23c3e803f

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

@@ -76,7 +76,7 @@ export class UiAppCompiler {
     private getExtensionModulesHash(extensions: Array<Required<AdminUiExtension>>): string {
         let modifiedDates: string[] = [];
         for (const extension of extensions) {
-            modifiedDates = [...modifiedDates, ...this.getAllModifiedDates(extension.ngModulePath)];
+            modifiedDates = [...modifiedDates, ...this.getAllModifiedDates(extension.extensionPath)];
         }
         const hash = crypto.createHash('sha256');
         hash.update(modifiedDates.join('') + JSON.stringify(extensions));

+ 17 - 12
packages/admin-ui/src/devkit/common.ts

@@ -1,4 +1,4 @@
-import { AdminUiExtension } from '@vendure/common/lib/shared-types';
+import { AdminUiExtension, AdminUiExtensionModule } from '@vendure/common/lib/shared-types';
 import * as fs from 'fs-extra';
 import * as path from 'path';
 
@@ -23,14 +23,14 @@ export function deleteExistingExtensionModules() {
 }
 
 /**
- * Copies all files from the ngModulePaths of the configured extensions into the
+ * 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.ngModulePath));
+        const dirName = path.basename(path.dirname(extension.extensionPath));
         const dest = getModuleOutputDir(extension);
-        fs.copySync(extension.ngModulePath, dest);
+        fs.copySync(extension.extensionPath, dest);
     }
 }
 
@@ -40,16 +40,21 @@ export function getModuleOutputDir(extension: Required<AdminUiExtension>): strin
 
 export function createExtensionsModules(extensions: Array<Required<AdminUiExtension>>) {
     const removeTsExtension = (filename: string): string => filename.replace(/\.ts$/, '');
-    const importPath = (e: Required<AdminUiExtension>): string =>
-        `./${EXTENSIONS_MODULES_DIR}/${e.id}/${removeTsExtension(e.ngModuleFileName)}`;
+    const importPath = (id: string, fileName: string): string =>
+        `./${EXTENSIONS_MODULES_DIR}/${id}/${removeTsExtension(fileName)}`;
 
     for (const type of ['lazy', 'shared'] as const) {
-        const source = generateExtensionModuleTsSource(
-            type,
-            extensions
-                .filter(e => e.type === type)
-                .map(e => ({ className: e.ngModuleName, path: importPath(e) })),
-        );
+        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');
     }

+ 4 - 4
packages/admin-ui/src/devkit/watch.ts

@@ -40,21 +40,21 @@ export function watchAdminUiApp(extensions: Array<Required<AdminUiExtension>>, p
     let watcher: FSWatcher | undefined;
     for (const extension of extensions) {
         if (!watcher) {
-            watcher = watch(extension.ngModulePath, {
+            watcher = watch(extension.extensionPath, {
                 depth: 4,
                 ignored: '**/node_modules/',
             });
         } else {
-            watcher.add(extension.ngModulePath);
+            watcher.add(extension.extensionPath);
         }
     }
 
     if (watcher) {
         watcher.on('change', filePath => {
-            const extension = extensions.find(e => filePath.includes(e.ngModulePath));
+            const extension = extensions.find(e => filePath.includes(e.extensionPath));
             if (extension) {
                 const outputDir = getModuleOutputDir(extension);
-                const filePart = path.relative(extension.ngModulePath, filePath);
+                const filePart = path.relative(extension.extensionPath, filePath);
                 const dest = path.join(outputDir, filePart);
                 fs.copyFile(filePath, dest);
             }

+ 27 - 12
packages/common/src/shared-types.ts

@@ -5,11 +5,11 @@
  * Source: https://stackoverflow.com/a/49936686/772859
  */
 export type DeepPartial<T> = {
-  [P in keyof T]?: null | (T[P] extends Array<infer U>
+    [P in keyof T]?: null | (T[P] extends Array<infer U>
     ? Array<DeepPartial<U>>
     : T[P] extends ReadonlyArray<infer U>
-      ? ReadonlyArray<DeepPartial<U>>
-      : DeepPartial<T[P]>)
+        ? ReadonlyArray<DeepPartial<U>>
+        : DeepPartial<T[P]>)
 };
 // tslint:enable:no-shadowed-variable
 
@@ -22,7 +22,7 @@ export type DeepRequired<T, U extends object | undefined = undefined> = T extend
     ? {
           [P in keyof T]-?: NonNullable<T[P]> extends NonNullable<U | Function | Type<any>>
               ? NonNullable<T[P]>
-              : DeepRequired<NonNullable<T[P]>, U>;
+              : DeepRequired<NonNullable<T[P]>, U>
       }
     : T;
 // tslint:enable:ban-types
@@ -105,9 +105,31 @@ export interface AdminUiExtension {
     /**
      * @description
      * An optional ID for the extension module. Only used internally for generating
-     * import paths to your module.
+     * import paths to your module. If not specified, a unique hash will be used as the id.
      */
     id?: string;
+
+    /**
+     * @description
+     * The path to the directory containing the extension module(s). The entire contents of this directory
+     * will be copied into the Admin UI app, including all TypeScript source files, html templates,
+     * scss style sheets etc.
+     */
+    extensionPath: string;
+    /**
+     * @description
+     * One or more Angular modules which extend the default Admin UI.
+     */
+    ngModules: AdminUiExtensionModule[];
+}
+
+/**
+ * @description
+ * Configuration defining a single NgModule with which to extend the Admin UI.
+ *
+ * @docsCategory AdminUiPlugin
+ */
+export interface AdminUiExtensionModule {
     /**
      * @description
      * Lazy modules are lazy-loaded at the `/extensions/` route and should be used for
@@ -118,13 +140,6 @@ export interface AdminUiExtension {
      * navigation items.
      */
     type: 'shared' | 'lazy';
-    /**
-     * @description
-     * The path to the directory containing the extension module. Each extension module
-     * should be located in its own directory. The entire contents of this directory
-     * will be copied into the Admin UI app.
-     */
-    ngModulePath: string;
     /**
      * @description
      * The name of the file containing the extension module class.