Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 0 additions & 55 deletions Composer/packages/client/config/extensions.config.js

This file was deleted.

1 change: 1 addition & 0 deletions Composer/packages/client/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
19 changes: 19 additions & 0 deletions Composer/packages/client/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
Expand Down
5 changes: 2 additions & 3 deletions Composer/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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__",
Expand Down
14 changes: 0 additions & 14 deletions Composer/packages/client/public/react-bundle.js.LICENSE.txt

This file was deleted.

23 changes: 0 additions & 23 deletions Composer/packages/client/public/react-dom-bundle.js.LICENSE.txt

This file was deleted.

12 changes: 6 additions & 6 deletions Composer/packages/client/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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),
});
}
71 changes: 52 additions & 19 deletions Composer/packages/client/src/components/PluginHost/PluginHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -54,7 +64,22 @@ function injectScript(doc: Document, id: string, src: string, async: boolean, on
*/
export const PluginHost: React.FC<PluginHostProps> = (props) => {
const targetRef = useRef<HTMLIFrameElement>(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;
Expand All @@ -72,14 +97,7 @@ export const PluginHost: React.FC<PluginHostProps> = (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);
Expand All @@ -102,5 +120,20 @@ export const PluginHost: React.FC<PluginHostProps> = (props) => {
}
}, [shell]);

return <iframe ref={targetRef} css={[iframeStyle, ...extraIframeStyles]} title={`${pluginName} host`} />;
return (
<div css={containerStyles}>
<iframe
key={`${pluginName}.${bundleId}.${pluginType}`}
ref={targetRef}
css={iframeStyle(isLoading)}
src="/plugin-host.html"
title={`${pluginName} host`}
/>
{isLoading && (
<div css={loadingStyles}>
<LoadingSpinner />
</div>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,9 @@ const CreatePublishTarget: React.FC<CreatePublishTargetProps> = (props) => {
if (selectedTarget?.bundleId) {
// render custom plugin view
return (
<PluginHost
bundleId={selectedTarget.bundleId}
extraIframeStyles={[customPublishUISurface]}
pluginName={selectedTarget.extensionId}
pluginType="publish"
/>
<div css={customPublishUISurface}>
<PluginHost bundleId={selectedTarget.bundleId} pluginName={selectedTarget.extensionId} pluginType="publish" />
</div>
);
}
// render default instruction / schema editor view
Expand Down
2 changes: 1 addition & 1 deletion Composer/packages/client/src/pages/publish/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,5 @@ export const targetSelected = css`
`;

export const customPublishUISurface = css`
min-height: 300px;
height: 230px;
`;
Loading