Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add extension management apis #3978

Merged
merged 27 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ef4427a
Improved plugin infrastructure -- render custom UI
a-b-r-o-w-n Jun 18, 2020
ca4a868
Aligned sample publish plugin with new publish api
tonyanziano Aug 26, 2020
efbb8cd
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Aug 27, 2020
4826f35
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Aug 31, 2020
92b3fe4
client-plugin-lib improvements
tonyanziano Aug 31, 2020
b51221a
Minor fixes for clarity and polish
tonyanziano Aug 31, 2020
3641ebb
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Sep 1, 2020
6ce7153
Updated sample-ui-plugin package with docs
tonyanziano Sep 2, 2020
294a998
Removed logs
tonyanziano Sep 2, 2020
13d574a
Updated sample-ui-plugin docs
tonyanziano Sep 2, 2020
ea5e497
Guarded against removing / disabling built-in plugins
tonyanziano Sep 2, 2020
d6965b6
Minor fixes
tonyanziano Sep 2, 2020
dad4155
Linting
tonyanziano Sep 2, 2020
02dbfee
Comment update
tonyanziano Sep 2, 2020
ac57244
More linting
tonyanziano Sep 2, 2020
6cad8c1
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Sep 2, 2020
8162927
Added licenses and removed a comment.
tonyanziano Sep 2, 2020
e0be76d
minor cleanup
a-b-r-o-w-n Sep 3, 2020
755b35f
Merge branch 'main' into toanzian/feat/extensions
a-b-r-o-w-n Sep 4, 2020
a456069
hide plugins page in client
a-b-r-o-w-n Sep 4, 2020
0f37b90
rename extension-manifest to just extensions
a-b-r-o-w-n Sep 4, 2020
2ddcb4a
run npm commands in a safe manner
a-b-r-o-w-n Sep 4, 2020
86f3250
prettify extensions.json
a-b-r-o-w-n Sep 4, 2020
a7a496a
make PluginManager a singleton
a-b-r-o-w-n Sep 4, 2020
443462e
Merge branch 'main' into toanzian/feat/extensions
a-b-r-o-w-n Sep 4, 2020
52fc8b9
Merge branch 'main' into toanzian/feat/extensions
a-b-r-o-w-n Sep 8, 2020
03f69bd
only spawn npm commands
a-b-r-o-w-n Sep 8, 2020
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
18 changes: 11 additions & 7 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
"type": "node",
"request": "launch",
"name": "Server: Launch",
"args": ["./build/server.js"],
"preLaunchTask": "server: build",
"args": ["./build/init.js"],
"env": {
"DEBUG": ""
},
"restart": true,
"outFiles": ["./build/*"],
"envFile": "${workspaceFolder}/Composer/packages/server/.env",
"preLaunchTask": "server: build",
"outputCapture": "std",
"cwd": "${workspaceFolder}/Composer/packages/server"
},
Expand Down Expand Up @@ -77,9 +79,13 @@
"request": "launch",
"name": "Electron Main Process",
"runtimeExecutable": "${workspaceRoot}/Composer/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/Composer/node_modules/.bin/electron.cmd"
},
"args": ["${workspaceRoot}/Composer/packages/electron-server"],
"env": {
"NODE_ENV": "development"
"NODE_ENV": "development",
"DEBUG": "composer*"
},
"outputCapture": "std"
},
Expand All @@ -92,9 +98,7 @@
"${workspaceRoot}/Composer/node_modules/jest/bin/jest",
"--runInBand"
],
"args": [
"${fileBasename}",
],
"args": ["${fileBasename}"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
Expand Down
8 changes: 5 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"tasks": [
{
"label": "server: build",
"type": "npm",
"script": "build",
"path": "Composer/packages/server/",
"type": "shell",
"command": "yarn build",
"options": {
"cwd": "Composer/packages/server"
},
"problemMatcher": []
}
]
Expand Down
3 changes: 1 addition & 2 deletions Composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"packages/extensions/*",
"packages/lib",
"packages/lib/*",
"packages/plugin-loader",
"packages/server",
"packages/test-utils",
"packages/tools",
Expand All @@ -38,7 +37,7 @@
"build:test": "yarn workspace @bfc/test-utils build",
"build:lib": "yarn workspace @bfc/libs build:all",
"build:electron": "yarn workspace @bfc/electron-server build",
"build:extensions": "wsrun -lt -p @bfc/plugin-loader @bfc/intellisense @bfc/extension @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* -c build",
"build:extensions": "wsrun -lt -p @bfc/plugin-loader @bfc/intellisense @bfc/extension @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* @bfc/client-plugin-lib -c build",
"build:server": "yarn workspace @bfc/server build",
"build:client": "yarn workspace @bfc/client build",
"build:tools": "yarn workspace @bfc/tools build:all",
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/client/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
build/
public/*-bundle.js
6 changes: 3 additions & 3 deletions Composer/packages/client/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const url = require('url');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);

const envPublicUrl = process.env.PUBLIC_URL;

Expand All @@ -22,7 +22,7 @@ function ensureSlash(inputPath, needsSlash) {
}
}

const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage;
const getPublicUrl = (appPackageJson) => envPublicUrl || require(appPackageJson).homepage;

// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
Expand Down Expand Up @@ -52,7 +52,7 @@ const moduleFileExtensions = [

// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`)));
const extension = moduleFileExtensions.find((extension) => fs.existsSync(resolveFn(`${filePath}.${extension}`)));

if (extension) {
return resolveFn(`${filePath}.${extension}`);
Expand Down
22 changes: 22 additions & 0 deletions Composer/packages/client/config/webpack-react-dom.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { resolve } = require('path');

module.exports = {
entry: {
'react-dom-bundle': 'react-dom',
},
mode: 'production',
// export react-dom globally under a variable named ReactDOM
output: {
path: resolve(__dirname, '../public'),
library: 'ReactDOM',
libraryTarget: 'var',
},
externals: {
// ReactDOM depends on React, but we need this to resolve to the globally-exposed React variable in react-bundle.js (created by webpack-react.config.js).
// If we don't do this, ReactDom will bundle its own copy of React and we will have 2 copies which breaks hooks.
react: 'React',
},
resolve: {
extensions: ['.js'],
},
};
17 changes: 17 additions & 0 deletions Composer/packages/client/config/webpack-react.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { resolve } = require('path');

module.exports = {
entry: {
'react-bundle': 'react',
},
mode: 'production',
// export react globally under a variable named React
output: {
path: resolve(__dirname, '../public'),
library: 'React',
libraryTarget: 'var',
},
resolve: {
extensions: ['.js'],
},
};
3 changes: 2 additions & 1 deletion Composer/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"node": ">=12"
},
"scripts": {
"start": "node scripts/start.js",
"start": "yarn build:react-bundles && node scripts/start.js",
"build": "node --max_old_space_size=4096 scripts/build.js",
"build:react-bundles": "webpack --config ./config/webpack-react.config.js && webpack --config ./config/webpack-react-dom.config.js",
"clean": "rimraf build",
"test": "jest",
"lint": "eslint --quiet --ext .js,.jsx,.ts,.tsx ./src ./__tests__",
Expand Down
68 changes: 0 additions & 68 deletions Composer/packages/client/public/extensionContainer.html

This file was deleted.

29 changes: 29 additions & 0 deletions Composer/packages/client/public/plugin-host-preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// add default doc styles
if (!document.getElementById('plugin-host-default-styles')) {
const styles = document.createElement('style');
styles.id = '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;
height: 100%;
}
`)
);
document.head.appendChild(styles);
}
// add the react mount point
if (!document.getElementById('plugin-root')) {
const root = document.createElement('div');
root.id = 'plugin-root';
document.body.appendChild(root);
}
// initialize the API object
window.Composer = {};
// init the render function
window.Composer['render'] = function (component) {
ReactDOM.render(component, document.getElementById('plugin-root'));
};
9 changes: 8 additions & 1 deletion Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React, { Fragment } from 'react';
import React, { Fragment, useEffect } from 'react';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
import { useRecoilValue } from 'recoil';

import { Header } from './components/Header';
import { Announcement } from './components/AppComponents/Announcement';
import { MainContainer } from './components/AppComponents/MainContainer';
import { dispatcherState } from './recoilModel/DispatcherWrapper';

initializeIcons(undefined, { disableWarnings: true });

export const App: React.FC = () => {
const { fetchPlugins } = useRecoilValue(dispatcherState);
useEffect(() => {
fetchPlugins();
});
Comment on lines +16 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better place in the app to perform these client-side "on startup go and fetch some data" tasks?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. This seems to be the right place. We could make an action like bootstrapApplication() or something that other initialization code can go.


return (
<Fragment>
<Announcement />
Expand Down
74 changes: 74 additions & 0 deletions Composer/packages/client/src/components/PluginHost/PluginHost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
tonyanziano marked this conversation as resolved.
Show resolved Hide resolved
import { jsx, SerializedStyles } from '@emotion/core';
import * as React from 'react';
import { useEffect, useRef } from 'react';

import { PluginAPI } from '../../plugins/api';
import { PluginType } from '../../plugins/types';

import { iframeStyle } from './styles';

interface PluginHostProps {
extraIframeStyles?: SerializedStyles[];
pluginName?: string;
pluginType?: PluginType;
}

/** Binds closures around Composer client code to plugin iframe's window object */
function attachPluginAPI(win: Window, type: PluginType) {
const api = { ...PluginAPI[type], ...PluginAPI.auth };
for (const method in api) {
win.Composer[method] = (...args) => api[method](...args);
}
}

function injectScript(doc: Document, id: string, src: string, async: boolean, onload?: () => any) {
if (!doc.getElementById(id)) {
const script = document.createElement('script');
Object.assign(script, { id, src, async, onload });
doc.body.appendChild(script);
}
}

/** Abstraction that will render an iframe injected with all the necessary UI plugin scripts,
* and then serve the plugin's client bundle.
*/
export const PluginHost: React.FC<PluginHostProps> = (props) => {
const targetRef = useRef<HTMLIFrameElement>(null);
const { extraIframeStyles = [] } = props;

useEffect(() => {
const { pluginName, pluginType } = props;
// renders the plugin's UI inside of the iframe
const renderPluginView = async () => {
if (pluginName && pluginType) {
const iframeWindow = targetRef.current?.contentWindow as Window;
const iframeDocument = targetRef.current?.contentDocument as Document;

// inject the react / react-dom bundles
injectScript(iframeDocument, 'react-bundle', '/react-bundle.js', false);
injectScript(iframeDocument, 'react-dom-bundle', '/react-dom-bundle.js', false);
// // load the preload script to setup the plugin API
injectScript(iframeDocument, 'preload-bundle', '/plugin-host-preload.js', false, () => {
attachPluginAPI(iframeWindow, pluginType);
});

//load the bundle for the specified plugin
const pluginScriptId = `plugin-${pluginType}-${pluginName}`;
await new Promise((resolve) => {
const cb = () => {
resolve();
};
// 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, `/api/plugins/${pluginName}/view/${pluginType}`, false, cb);
});
}
};
renderPluginView();
}, [props.pluginName, props.pluginType, targetRef]);

return <iframe ref={targetRef} css={[iframeStyle, ...extraIframeStyles]} title={`${props.pluginName} host`}></iframe>;
};
10 changes: 10 additions & 0 deletions Composer/packages/client/src/components/PluginHost/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { css } from '@emotion/core';

export const iframeStyle = css`
height: 100%;
width: 100%;
border: 0;
`;
15 changes: 15 additions & 0 deletions Composer/packages/client/src/pages/plugin/pluginPageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React from 'react';
import { RouteComponentProps } from '@reach/router';

import { PluginHost } from '../../components/PluginHost/PluginHost';

const PluginPageContainer: React.FC<RouteComponentProps<{ pluginId: string }>> = (props) => {
const { pluginId } = props;

return <PluginHost pluginName={pluginId} pluginType={'page'}></PluginHost>;
};

export { PluginPageContainer };
Loading