Skip to content

Commit

Permalink
feat(ui-devkit): Create ui-devkit package for developing UI extensions
Browse files Browse the repository at this point in the history
This package exposes an API which allows embedded UI extension applications to access core functionality. So far implements GraphQL queries & mutations. Relates to #225
  • Loading branch information
michaelbromley committed Dec 10, 2019
1 parent 68eeb46 commit 20cd34d
Show file tree
Hide file tree
Showing 17 changed files with 453 additions and 54 deletions.
2 changes: 1 addition & 1 deletion packages/admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@ng-select/ng-select": "^3.0.5",
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"@vendure/ui-extension-devkit": "^0.6.4",
"@vendure/ui-devkit": "^0.6.4",
"@webcomponents/custom-elements": "^1.2.4",
"apollo-angular": "^1.6.0",
"apollo-cache-inmemory": "^1.6.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ExtensionHostService } from './extension-host.service';
selector: 'vdr-extension-host',
templateUrl: './extension-host.component.html',
styleUrls: ['./extension-host.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
providers: [ExtensionHostService],
})
export class ExtensionHostComponent implements OnInit {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,83 @@
import { Injectable, OnDestroy } from '@angular/core';
import { DataService } from '@vendure/admin-ui/src/app/data/providers/data.service';
import { ExtensionMesssage, MessageResponse } from '@vendure/common/lib/extension-host-types';
import { parse } from 'graphql';
import { merge, Observer, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { assertNever } from 'shared/shared-utils';

import { ExtensionMesssage } from './extension-message-types';
import { DataService } from '../../../data/providers/data.service';

@Injectable()
export class ExtensionHostService implements OnDestroy {
private extensionWindow: Window;
private cancellationMessage$ = new Subject<string>();
private destroyMessage$ = new Subject<void>();

constructor(private dataService: DataService) {}

init(extensionWindow: Window) {
this.extensionWindow = extensionWindow;

window.addEventListener('message', this.handleMessage);
}

ngOnDestroy(): void {
window.removeEventListener('message', this.handleMessage);
this.destroyMessage$.next();
}

private handleMessage = (message: MessageEvent) => {
const { data, origin } = message;
if (this.isExtensionMessage(data)) {
const cancellation$ = this.cancellationMessage$.pipe(
filter(requestId => requestId === data.requestId),
);
const end$ = merge(cancellation$, this.destroyMessage$);
switch (data.type) {
case 'query': {
case 'cancellation': {
this.cancellationMessage$.next(data.requestId);
break;
}
case 'destroy': {
this.destroyMessage$.next();
break;
}
case 'graphql-query': {
const { document, variables, fetchPolicy } = data.data;
this.dataService
.query(parse(document), variables, fetchPolicy)
.single$.subscribe(result => this.sendMessage(result, origin, data.requestId));
.stream$.pipe(takeUntil(end$))
.subscribe(this.createObserver(data.requestId, origin));
break;
}
case 'mutation': {
case 'graphql-mutation': {
const { document, variables } = data.data;
this.dataService
.mutate(parse(document), variables)
.subscribe(result => this.sendMessage(result, origin, data.requestId));
.pipe(takeUntil(end$))
.subscribe(this.createObserver(data.requestId, origin));
break;
}
default:
assertNever(data);
}
}
};

private sendMessage(message: any, origin, requestId: string) {
this.extensionWindow.postMessage({ requestId, data: message }, origin);
private createObserver(requestId: string, origin: string): Observer<any> {
return {
next: data => this.sendMessage({ data, error: false, complete: false, requestId }, origin),
error: err => this.sendMessage({ data: err, error: true, complete: false, requestId }, origin),
complete: () => this.sendMessage({ data: null, error: false, complete: true, requestId }, origin),
};
}

private sendMessage(response: MessageResponse, origin: string) {
this.extensionWindow.postMessage(response, origin);
}

private isExtensionMessage(input: any): input is ExtensionMesssage {
return input.hasOwnProperty('type') && input.hasOwnProperty('data');
return (
input.hasOwnProperty('type') && input.hasOwnProperty('data') && input.hasOwnProperty('requestId')
);
}
}

This file was deleted.

13 changes: 9 additions & 4 deletions packages/admin-ui/src/compiler/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ export function copyExtensionModules(extensions: Array<Required<AdminUiExtension
}
}
}
fs.copySync(
require.resolve('@vendure/ui-extension-devkit'),
path.join(STATIC_ASSETS_OUTPUT_DIR, 'ui-extension-devkit.js'),
);
}

/**
* 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'));
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/admin-ui/src/compiler/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from 'path';

import {
copyExtensionModules,
copyUiDevkit,
createExtensionsModules,
deleteExistingExtensionModules,
isInVendureMonorepo,
Expand All @@ -20,6 +21,7 @@ export function compileAdminUiApp(outputPath: string, extensions: Array<Required
restoreOriginalExtensionsModule();
deleteExistingExtensionModules();
copyExtensionModules(extensions);
copyUiDevkit();
createExtensionsModules(extensions);

const config = isInVendureMonorepo() ? 'plugin-dev' : 'plugin';
Expand Down
11 changes: 11 additions & 0 deletions packages/admin-ui/src/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as path from 'path';
import {
copyExtensionModules,
copyStaticAsset,
copyUiDevkit,
createExtensionsModules,
deleteExistingExtensionModules,
getModuleOutputDir,
Expand All @@ -29,6 +30,7 @@ export function watchAdminUiApp(extensions: Array<Required<AdminUiExtension>>, p
restoreExtensionsModules();
deleteExistingExtensionModules();
copyExtensionModules(extensions);
copyUiDevkit();
createExtensionsModules(extensions);

const config = isInVendureMonorepo() ? 'plugin-dev' : 'plugin';
Expand All @@ -37,6 +39,7 @@ export function watchAdminUiApp(extensions: Array<Required<AdminUiExtension>>, p
shell: true,
stdio: 'inherit',
});
const devkitPath = require.resolve('@vendure/ui-devkit');

let watcher: FSWatcher | undefined;
for (const extension of extensions) {
Expand All @@ -50,6 +53,11 @@ export function watchAdminUiApp(extensions: Array<Required<AdminUiExtension>>, p
}
}

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));
Expand All @@ -67,6 +75,9 @@ export function watchAdminUiApp(extensions: Array<Required<AdminUiExtension>>, p
const dest = path.join(outputDir, filePart);
fs.copyFile(filePath, dest);
}
if (filePath.includes(devkitPath)) {
copyUiDevkit();
}
});
}

Expand Down
44 changes: 44 additions & 0 deletions packages/common/src/extension-host-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export type FetchPolicy = 'cache-first' | 'network-only' | 'cache-only' | 'no-cache' | 'standby';
export type WatchQueryFetchPolicy = FetchPolicy | 'cache-and-network';

export interface BaseExtensionMessage {
requestId: string;
type: string;
data: any;
}

export interface QueryMessage extends BaseExtensionMessage {
type: 'graphql-query';
data: {
document: string;
variables?: { [key: string]: any };
fetchPolicy?: WatchQueryFetchPolicy;
};
}

export interface MutationMessage extends BaseExtensionMessage {
type: 'graphql-mutation';
data: {
document: string;
variables?: { [key: string]: any };
};
}

export interface CancellationMessage extends BaseExtensionMessage {
type: 'cancellation';
data: null;
}

export interface DestroyMessage extends BaseExtensionMessage {
type: 'destroy';
data: null;
}

export type ExtensionMesssage = QueryMessage | MutationMessage | CancellationMessage | DestroyMessage;

export interface MessageResponse {
requestId: string;
data: any;
complete: boolean;
error: boolean;
}
2 changes: 2 additions & 0 deletions packages/ui-devkit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib
node_modules
5 changes: 5 additions & 0 deletions packages/ui-devkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @vendure/ui-devkit

This package contains utilities for creating extensions to the Vendure Admin UI.


47 changes: 47 additions & 0 deletions packages/ui-devkit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@vendure/ui-devkit",
"version": "0.6.4",
"description": "A library for authoring Vendure Admin UI extensions",
"keywords": [
"vendure",
"javascript",
"extensions"
],
"author": "Michael Bromley <[email protected]>",
"homepage": "https://github.com/vendure-ecommerce/vendure#readme",
"license": "MIT",
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"main": "lib/index.js",
"types": "lib/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",
"watch": "rimraf ./lib && rollup -c rollup.config.js -w",
"lint": "tslint --fix --project ./"
},
"bugs": {
"url": "https://github.com/vendure-ecommerce/vendure/issues"
},
"dependencies": {
"@vendure/common": "^0.6.4",
"rxjs": "^6.5.3"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^6.0.0",
"@vendure/core": "^0.6.4",
"rimraf": "^3.0.0",
"rollup": "^1.27.9",
"rollup-plugin-terser": "^5.1.2",
"rollup-plugin-typescript2": "^0.25.3",
"tslib": "^1.10.0",
"typescript": "^3.6.4"
}
}
21 changes: 21 additions & 0 deletions packages/ui-devkit/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
import { terser } from 'rollup-plugin-terser';
import resolve from '@rollup/plugin-node-resolve';

export default commandLineArgs => {
const isProd = commandLineArgs.configProduction === true;
return {
input: 'src/index.ts',
output: {
dir: 'lib',
format: 'umd',
name: 'VendureUiDevkit',
},
plugins: [resolve(), typescript(), ...(isProd ? [terser({
output: {
comments: false,
}
})] : [])],
};
};
Loading

0 comments on commit 20cd34d

Please sign in to comment.