diff --git a/Composer/packages/client/config/extensions.config.js b/Composer/packages/client/config/extensions.config.js
deleted file mode 100644
index da8f3122ee..0000000000
--- a/Composer/packages/client/config/extensions.config.js
+++ /dev/null
@@ -1,55 +0,0 @@
-const path = require('path');
-
-const PnpWebpackPlugin = require('pnp-webpack-plugin');
-const TerserPlugin = require('terser-webpack-plugin');
-
-module.exports = (webpackEnv) => {
- const isEnvProduction = webpackEnv === 'production';
-
- return {
- mode: isEnvProduction ? 'production' : 'development',
- entry: {
- 'plugin-host-preload': path.resolve(__dirname, '../extension-container/plugin-host-preload.tsx'),
- },
- output: {
- path: path.resolve(__dirname, '../public'),
- filename: '[name].js',
- },
- resolve: {
- extensions: ['.js'],
- plugins: [PnpWebpackPlugin],
- },
- resolveLoader: {
- plugins: [
- // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
- // from the current package.
- PnpWebpackPlugin.moduleLoader(module),
- ],
- },
- module: {
- rules: [
- {
- test: /\.tsx?$/,
- loader: require.resolve('ts-loader'),
- include: [path.resolve(__dirname, '../extension-container')],
- options: PnpWebpackPlugin.tsLoaderOptions({
- transpileOnly: true,
- configFile: path.resolve(__dirname, '../tsconfig.build.json'),
- }),
- },
- ],
- },
- optimization: {
- minimize: isEnvProduction,
-
- minimizer: [
- new TerserPlugin({
- extractComments: false,
- terserOptions: {
- sourceMap: true,
- },
- }),
- ],
- },
- };
-};
diff --git a/Composer/packages/client/config/paths.js b/Composer/packages/client/config/paths.js
index 13b4627e5e..96f941dc41 100644
--- a/Composer/packages/client/config/paths.js
+++ b/Composer/packages/client/config/paths.js
@@ -69,6 +69,7 @@ module.exports = {
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
+ pluginHostIndexJs: resolveModule(resolveApp, 'src/plugin-host-preload'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
diff --git a/Composer/packages/client/config/webpack.config.js b/Composer/packages/client/config/webpack.config.js
index f026477c45..cf9abb2b02 100644
--- a/Composer/packages/client/config/webpack.config.js
+++ b/Composer/packages/client/config/webpack.config.js
@@ -107,6 +107,7 @@ module.exports = function (webpackEnv) {
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: {
+ 'plugin-host-preload': paths.pluginHostIndexJs,
main: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
@@ -434,6 +435,24 @@ module.exports = function (webpackEnv) {
: undefined
)
),
+ new HtmlWebpackPlugin(
+ Object.assign(
+ {},
+ {
+ inject: true,
+ filename: isEnvProduction ? 'plugin-host.ejs' : 'plugin-host.html',
+ template: paths.appHtml,
+ chunks: ['plugin-host-preload'],
+ },
+ isEnvProduction
+ ? {
+ minify: {
+ removeComments: true,
+ },
+ }
+ : undefined
+ )
+ ),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
//
diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json
index ff3fa93a80..1e24637023 100644
--- a/Composer/packages/client/package.json
+++ b/Composer/packages/client/package.json
@@ -8,9 +8,8 @@
"node": ">=12"
},
"scripts": {
- "start": "yarn build:extension-bundles && node scripts/start.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",
+ "start": "node scripts/start.js",
+ "build": "node --max_old_space_size=4096 scripts/build.js",
"clean": "rimraf build",
"test": "jest",
"lint": "eslint --quiet --ext .js,.jsx,.ts,.tsx ./src ./__tests__",
diff --git a/Composer/packages/client/public/react-bundle.js.LICENSE.txt b/Composer/packages/client/public/react-bundle.js.LICENSE.txt
deleted file mode 100644
index 4467f637b9..0000000000
--- a/Composer/packages/client/public/react-bundle.js.LICENSE.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
-object-assign
-(c) Sindre Sorhus
-@license MIT
-*/
-
-/** @license React v16.13.1
- * react.production.min.js
- *
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
diff --git a/Composer/packages/client/public/react-dom-bundle.js.LICENSE.txt b/Composer/packages/client/public/react-dom-bundle.js.LICENSE.txt
deleted file mode 100644
index 787b51e5d7..0000000000
--- a/Composer/packages/client/public/react-dom-bundle.js.LICENSE.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-object-assign
-(c) Sindre Sorhus
-@license MIT
-*/
-
-/** @license React v0.19.1
- * scheduler.production.min.js
- *
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-/** @license React v16.13.1
- * react-dom.production.min.js
- *
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
diff --git a/Composer/packages/client/scripts/build.js b/Composer/packages/client/scripts/build.js
index 5453353231..8ddb159fc9 100644
--- a/Composer/packages/client/scripts/build.js
+++ b/Composer/packages/client/scripts/build.js
@@ -7,7 +7,7 @@ process.env.NODE_ENV = 'production';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
-process.on('unhandledRejection', err => {
+process.on('unhandledRejection', (err) => {
throw err;
});
@@ -57,7 +57,7 @@ checkBrowsers(paths.appPath, isInteractive)
// This lets us display how much they changed later.
return measureFileSizesBeforeBuild(paths.appBuild);
})
- .then(previousFileSizes => {
+ .then((previousFileSizes) => {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
fs.emptyDirSync(paths.appBuild);
@@ -92,13 +92,13 @@ checkBrowsers(paths.appPath, isInteractive)
// Merge with the public folder
copyPublicFolder();
},
- err => {
+ (err) => {
console.log(chalk.red('Failed to compile.\n'));
printBuildError(err);
process.exit(1);
}
)
- .catch(err => {
+ .catch((err) => {
if (err && err.message) {
console.log(err.message);
}
@@ -154,7 +154,7 @@ function build(previousFileSizes) {
return bfj
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
.then(() => resolve(resolveArgs))
- .catch(error => reject(new Error(error)));
+ .catch((error) => reject(new Error(error)));
}
return resolve(resolveArgs);
@@ -166,6 +166,6 @@ function copyPublicFolder() {
// copy to build/public folder
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
- filter: file => ![paths.appHtml, paths.extensionContainerHtml].includes(file),
+ filter: (file) => ![paths.appHtml].includes(file),
});
}
diff --git a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx
index 72bc3bff14..f067bcd11a 100644
--- a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx
+++ b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx
@@ -2,32 +2,42 @@
// Licensed under the MIT License.
/** @jsx jsx */
-import { jsx, css, SerializedStyles } from '@emotion/core';
-import React, { useEffect, useRef } from 'react';
+import { jsx, css } from '@emotion/core';
+import React, { useState, useEffect, useRef } from 'react';
import { Shell } from '@botframework-composer/types';
import { PluginType } from '@bfc/extension-client';
+import { LoadingSpinner } from '../LoadingSpinner';
import { PluginAPI } from '../../plugins/api';
-export const iframeStyle = css`
+const containerStyles = css`
+ position: relative;
+ height: 100%;
+ width: 100%;
+`;
+
+const iframeStyle = (isLoading = false) => css`
height: 100%;
width: 100%;
border: 0;
+ display: ${isLoading ? 'none' : 'block'};
+`;
+
+const loadingStyles = css`
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
`;
interface PluginHostProps {
- extraIframeStyles?: SerializedStyles[];
pluginName: string;
pluginType: PluginType;
bundleId: string;
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 };
@@ -54,7 +64,22 @@ function injectScript(doc: Document, id: string, src: string, async: boolean, on
*/
export const PluginHost: React.FC = (props) => {
const targetRef = useRef(null);
- const { extraIframeStyles = [], pluginType, pluginName, bundleId, shell } = props;
+ const { pluginType, pluginName, bundleId, shell } = props;
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const isReady = (ev) => {
+ if (ev.data === 'plugin-rendered') {
+ setIsLoading(false);
+ }
+ };
+
+ window.addEventListener('message', isReady);
+
+ return () => {
+ window.removeEventListener('message', isReady);
+ };
+ }, []);
const loadBundle = (name: string, bundle: string, type: PluginType) => {
const iframeWindow = targetRef.current?.contentWindow as Window;
@@ -72,14 +97,7 @@ export const PluginHost: React.FC = (props) => {
useEffect(() => {
// renders the plugin's UI inside of the iframe
if (pluginName && pluginType && targetRef.current) {
- const iframeDocument = targetRef.current.contentDocument as Document;
-
- // cleanup
- resetIframe(iframeDocument);
-
- // load the preload script to setup the plugin API
- injectScript(iframeDocument, 'preload-bundle', '/plugin-host-preload.js', false);
-
+ setIsLoading(true);
const onPreloaded = (ev) => {
if (ev.data === 'host-preload-complete') {
loadBundle(pluginName, bundleId, pluginType);
@@ -102,5 +120,20 @@ export const PluginHost: React.FC = (props) => {
}
}, [shell]);
- return ;
+ return (
+
+
+ {isLoading && (
+
+
+
+ )}
+
+ );
};
diff --git a/Composer/packages/client/src/pages/publish/createPublishTarget.tsx b/Composer/packages/client/src/pages/publish/createPublishTarget.tsx
index bfe636056a..cdf5d671a6 100644
--- a/Composer/packages/client/src/pages/publish/createPublishTarget.tsx
+++ b/Composer/packages/client/src/pages/publish/createPublishTarget.tsx
@@ -106,12 +106,9 @@ const CreatePublishTarget: React.FC = (props) => {
if (selectedTarget?.bundleId) {
// render custom plugin view
return (
-
+
);
}
// render default instruction / schema editor view
diff --git a/Composer/packages/client/src/pages/publish/styles.ts b/Composer/packages/client/src/pages/publish/styles.ts
index 908cab1f8b..d15bf8c69b 100644
--- a/Composer/packages/client/src/pages/publish/styles.ts
+++ b/Composer/packages/client/src/pages/publish/styles.ts
@@ -117,5 +117,5 @@ export const targetSelected = css`
`;
export const customPublishUISurface = css`
- min-height: 300px;
+ height: 230px;
`;
diff --git a/Composer/packages/client/extension-container/plugin-host-preload.tsx b/Composer/packages/client/src/plugin-host-preload.ts
similarity index 59%
rename from Composer/packages/client/extension-container/plugin-host-preload.tsx
rename to Composer/packages/client/src/plugin-host-preload.ts
index 6f67c236b4..a3db9f1299 100644
--- a/Composer/packages/client/extension-container/plugin-host-preload.tsx
+++ b/Composer/packages/client/src/plugin-host-preload.ts
@@ -3,9 +3,15 @@
import React from 'react';
import ReactDOM from 'react-dom';
+// eslint-disable-next-line @bfc/bfcomposer/office-ui-import-scope
+import * as Fabric from 'office-ui-fabric-react';
import * as ExtensionClient from '@bfc/extension-client';
import { syncStore, Shell } from '@bfc/extension-client';
+import './index.css';
+
+Fabric.initializeIcons(undefined, { disableWarnings: true });
+
if (!document.head.title) {
const title = document.createElement('title');
title.innerHTML = 'Plugin Host';
@@ -19,7 +25,6 @@ if (!document.getElementById('plugin-host-default-styles')) {
styles.type = 'text/css';
styles.appendChild(
document.createTextNode(`
- html, body { padding: 0; margin: 0; }
#plugin-root {
display: flex;
flex-flow: column nowrap;
@@ -30,33 +35,33 @@ if (!document.getElementById('plugin-host-default-styles')) {
document.head.appendChild(styles);
}
// add the react mount point
-if (!document.getElementById('plugin-root')) {
+if (!document.getElementById('root')) {
const root = document.createElement('div');
- root.id = 'plugin-root';
+ root.id = 'root';
document.body.appendChild(root);
}
// initialize the API object
window.React = React;
window.ReactDOM = ReactDOM;
window.ExtensionClient = ExtensionClient;
-// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
-// @ts-ignore
-window.Composer = {};
+window.Fabric = Fabric;
+window.Composer = {
+ __pluginType: '',
+ render: (type: string, shell: Shell, component: React.ReactElement) => {
+ // eslint-disable-next-line no-underscore-dangle
+ window.Composer.__pluginType = type;
-// init the render function
-window.Composer.render = function (type: string, shell: Shell, component: React.ReactElement) {
- // eslint-disable-next-line no-underscore-dangle
- window.Composer.__pluginType = type;
+ if (shell) {
+ syncStore(shell);
+ }
- if (shell) {
+ ReactDOM.render(component, document.getElementById('root'));
+ window.parent?.postMessage('plugin-rendered', '*');
+ },
+ sync: (shell: Shell) => {
syncStore(shell);
- }
-
- ReactDOM.render(component, document.getElementById('plugin-root'));
-};
-
-window.Composer.sync = function (shell: Shell) {
- syncStore(shell);
+ },
};
+// signal to the host that we are ready to accept the plugin bundle
window.parent?.postMessage('host-preload-complete', '*');
diff --git a/Composer/packages/client/src/types/window.d.ts b/Composer/packages/client/src/types/window.d.ts
index 18e8c289d8..dd5892d0c3 100644
--- a/Composer/packages/client/src/types/window.d.ts
+++ b/Composer/packages/client/src/types/window.d.ts
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+// eslint-disable-next-line @bfc/bfcomposer/office-ui-import-scope
+import * as Fabric from 'office-ui-fabric-react';
import * as ExtensionClient from '@bfc/extension-client';
declare global {
@@ -26,5 +28,6 @@ declare global {
};
ExtensionClient: typeof ExtensionClient;
+ Fabric: typeof Fabric;
}
}
diff --git a/Composer/packages/client/tsconfig.json b/Composer/packages/client/tsconfig.json
index 2db7715527..8511477d6c 100644
--- a/Composer/packages/client/tsconfig.json
+++ b/Composer/packages/client/tsconfig.json
@@ -3,7 +3,9 @@
"compilerOptions": {
"outDir": "build",
"allowJs": true,
- "module": "esnext"
+ "module": "esnext",
+ "declaration": false,
+ "declarationMap": false
},
"include": ["./src/**/*", "./__tests__/**/*", "./extension-container/**/*"]
}
diff --git a/Composer/packages/server/src/server.ts b/Composer/packages/server/src/server.ts
index d925787081..a657aacce8 100644
--- a/Composer/packages/server/src/server.ts
+++ b/Composer/packages/server/src/server.ts
@@ -115,6 +115,10 @@ export async function start(): Promise {
}
});
+ app.get(`${BASEURL}/plugin-host.html`, (req, res) => {
+ res.render(path.resolve(clientDirectory, 'plugin-host.ejs'), { __nonce__: req.__nonce__ });
+ });
+
app.get('*', (req, res) => {
res.render(path.resolve(clientDirectory, 'index.ejs'), { __nonce__: req.__nonce__ });
});