Skip to content

Commit

Permalink
feat(admin-ui-plugin): Allow UI extensions to contain multiple modules
Browse files Browse the repository at this point in the history
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',
        }],
    }
],
```
  • Loading branch information
michaelbromley committed Oct 22, 2019
1 parent d222449 commit b23c3e8
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 29 deletions.
2 changes: 1 addition & 1 deletion packages/admin-ui-plugin/src/ui-app-compiler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
29 changes: 17 additions & 12 deletions packages/admin-ui/src/devkit/common.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
}
}

Expand All @@ -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');
}
Expand Down
8 changes: 4 additions & 4 deletions packages/admin-ui/src/devkit/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
39 changes: 27 additions & 12 deletions packages/common/src/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down

0 comments on commit b23c3e8

Please sign in to comment.