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