From 6957c246d1aa0271aba5c461e36b35142a9a9dd9 Mon Sep 17 00:00:00 2001 From: Andy Brown Date: Mon, 19 Oct 2020 10:59:55 -0700 Subject: [PATCH] fix: allow extension to register multiple publish targets (#4424) * build extension bundle when building client * enable re-use of plugin host * require name and description for publish plugins * update publish plugins * add multiple publish plugins in sample-ui-plugin * update azure publish description * wrap file path in quote for windows compat * fix tests --- .../__tests__/pages/publish/Publish.test.tsx | 1 + Composer/packages/client/package.json | 2 +- .../src/components/PluginHost/PluginHost.tsx | 66 ++-- .../src/components/PluginHost/styles.ts | 10 - .../src/pages/publish/createPublishTarget.tsx | 22 +- .../packages/client/src/recoilModel/types.ts | 1 + Composer/packages/extension/package.json | 2 +- .../extension/src/extensionContext.ts | 31 +- .../extension/src/extensionRegistration.ts | 13 +- .../server/src/controllers/extensions.ts | 17 +- .../server/src/controllers/publisher.ts | 1 + Composer/packages/types/package.json | 4 +- Composer/packages/types/src/extension.ts | 1 + Composer/packages/types/src/publish.ts | 5 +- Composer/scripts/compileExtensions.js | 2 +- extensions/.prettierrc | 7 + extensions/authTest/index.js | 4 +- extensions/azurePublish/src/index.ts | 28 +- extensions/githubAuth/index.js | 17 +- extensions/localPublish/package.json | 2 + extensions/localPublish/src/index.ts | 43 ++- extensions/mockRemotePublish/src/index.ts | 3 + extensions/mongoStorage/src/index.ts | 14 +- extensions/publishTest/index.js | 21 -- extensions/publishTest/package.json | 14 - extensions/sample-ui-plugin/README.md | 2 +- extensions/sample-ui-plugin/package.json | 11 +- .../src/client/publish/main.tsx | 6 +- .../publish/{index.tsx => publish1.tsx} | 2 +- .../src/client/publish/publish2.tsx | 9 + extensions/sample-ui-plugin/src/node/index.ts | 23 +- .../sample-ui-plugin/tsconfig.node.json | 1 + extensions/sample-ui-plugin/webpack.config.js | 3 +- extensions/sample-ui-plugin/yarn.lock | 308 +++++++++++++++++- extensions/samples/.eslintrc.js | 16 +- .../shared/scripts/provisionComposer.js | 71 ++-- extensions/samples/src/index.ts | 35 +- extensions/webRoute/index.js | 3 +- 38 files changed, 582 insertions(+), 239 deletions(-) delete mode 100644 Composer/packages/client/src/components/PluginHost/styles.ts create mode 100644 extensions/.prettierrc delete mode 100644 extensions/publishTest/index.js delete mode 100644 extensions/publishTest/package.json rename extensions/sample-ui-plugin/src/client/publish/{index.tsx => publish1.tsx} (84%) create mode 100644 extensions/sample-ui-plugin/src/client/publish/publish2.tsx diff --git a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx index 5d498305a3..e42d04f5b8 100644 --- a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx +++ b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx @@ -35,6 +35,7 @@ const state = { name: 'azurePublish', description: 'azure publish', instructions: 'plugin instruction', + extensionId: 'azurePublish', schema: { default: { test: 'test', diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 00f12fa533..ff3fa93a80 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -9,7 +9,7 @@ }, "scripts": { "start": "yarn build:extension-bundles && node scripts/start.js", - "build": "node --max_old_space_size=4096 scripts/build.js", + "build": "yarn build:extension-bundles && node --max_old_space_size=4096 scripts/build.js", "build:extension-bundles": "webpack --config ./config/extensions.config.js --env production", "clean": "rimraf build", "test": "jest", diff --git a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx index 0df82eabd5..72bc3bff14 100644 --- a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx +++ b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx @@ -2,14 +2,18 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { jsx, SerializedStyles } from '@emotion/core'; -import React, { useState, useEffect, useRef } from 'react'; +import { jsx, css, SerializedStyles } from '@emotion/core'; +import React, { useEffect, useRef } from 'react'; import { Shell } from '@botframework-composer/types'; import { PluginType } from '@bfc/extension-client'; import { PluginAPI } from '../../plugins/api'; -import { iframeStyle } from './styles'; +export const iframeStyle = css` + height: 100%; + width: 100%; + border: 0; +`; interface PluginHostProps { extraIframeStyles?: SerializedStyles[]; @@ -19,6 +23,11 @@ interface PluginHostProps { shell?: Shell; } +function resetIframe(iframeDoc: Document) { + iframeDoc.head.innerHTML = ''; + iframeDoc.body.innerHTML = ''; +} + /** Binds closures around Composer client code to plugin iframe's window object */ function attachPluginAPI(win: Window, type: PluginType, shell?: object) { const api = { ...PluginAPI[type], ...PluginAPI.auth }; @@ -45,20 +54,35 @@ function injectScript(doc: Document, id: string, src: string, async: boolean, on */ export const PluginHost: React.FC = (props) => { const targetRef = useRef(null); - const [isLoaded, setIsLoaded] = useState(false); const { extraIframeStyles = [], pluginType, pluginName, bundleId, shell } = props; + const loadBundle = (name: string, bundle: string, type: PluginType) => { + const iframeWindow = targetRef.current?.contentWindow as Window; + const iframeDocument = targetRef.current?.contentDocument as Document; + + attachPluginAPI(iframeWindow, type, shell); + + //load the bundle for the specified plugin + const pluginScriptId = `plugin-${type}-${name}`; + const bundleUri = `/api/extensions/${name}/${bundle}`; + // If plugin bundles end up being too large and block the client thread due to the load, enable the async flag on this call + injectScript(iframeDocument, pluginScriptId, bundleUri, false); + }; + useEffect(() => { // renders the plugin's UI inside of the iframe - if (pluginName && pluginType) { - const iframeDocument = targetRef.current?.contentDocument as Document; + if (pluginName && pluginType && targetRef.current) { + const iframeDocument = targetRef.current.contentDocument as Document; + + // cleanup + resetIframe(iframeDocument); - // // load the preload script to setup the plugin API + // load the preload script to setup the plugin API injectScript(iframeDocument, 'preload-bundle', '/plugin-host-preload.js', false); const onPreloaded = (ev) => { if (ev.data === 'host-preload-complete') { - setIsLoaded(true); + loadBundle(pluginName, bundleId, pluginType); } }; @@ -68,29 +92,15 @@ export const PluginHost: React.FC = (props) => { window.removeEventListener('message', onPreloaded); }; } - }, [pluginName, pluginType, bundleId, targetRef]); - - useEffect(() => { - if (isLoaded && pluginType && pluginName && bundleId) { - const iframeWindow = targetRef.current?.contentWindow as Window; - const iframeDocument = targetRef.current?.contentDocument as Document; - - attachPluginAPI(iframeWindow, pluginType, shell); - - //load the bundle for the specified plugin - const pluginScriptId = `plugin-${pluginType}-${pluginName}`; - const bundleUri = `/api/extensions/${pluginName}/${bundleId}`; - // If plugin bundles end up being too large and block the client thread due to the load, enable the async flag on this call - injectScript(iframeDocument, pluginScriptId, bundleUri, false); - } - }, [isLoaded]); + }, [pluginName, pluginType, bundleId]); // sync the shell to the iframe store when shell changes useEffect(() => { - if (isLoaded && targetRef.current) { - targetRef.current.contentWindow?.Composer.sync(shell); + const frameApi = targetRef.current?.contentWindow?.Composer; + if (frameApi && typeof frameApi.sync === 'function') { + frameApi.sync(shell); } - }, [isLoaded, shell]); + }, [shell]); - return ; + return