diff --git a/Composer/.gitignore b/Composer/.gitignore index 756c18e624..4a121af957 100644 --- a/Composer/.gitignore +++ b/Composer/.gitignore @@ -13,3 +13,6 @@ packages/server/schemas/*.schema packages/server/schemas/*.uischema !packages/server/schemas/sdk.schema !packages/server/schemas/sdk.uischema + +# remote extensions +.composer diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 8e86f112b8..3887eda3b0 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -63,7 +63,7 @@ async function createAppDataDir() { const azurePublishPath: string = join(composerAppDataPath, 'publishBots'); process.env.COMPOSER_APP_DATA = join(composerAppDataPath, 'data.json'); // path to the actual data file process.env.COMPOSER_EXTENSION_DATA = join(composerAppDataPath, 'extensions.json'); - process.env.COMPOSER_REMOTE_PLUGINS_DIR = join(composerAppDataPath, '.composer'); + process.env.COMPOSER_REMOTE_EXTENSIONS_DIR = join(composerAppDataPath, '.composer'); log('creating composer app data path at: ', composerAppDataPath); @@ -142,7 +142,7 @@ async function loadServer() { // only change paths if packaged electron app const unpackedDir = getUnpackedAsarPath(); process.env.COMPOSER_RUNTIME_FOLDER = join(unpackedDir, 'runtime'); - process.env.COMPOSER_BUILTIN_PLUGINS_DIR = join(unpackedDir, 'build', 'plugins'); + process.env.COMPOSER_BUILTIN_EXTENSIONS_DIR = join(unpackedDir, 'build', 'plugins'); } // only create a new data directory if packaged electron app diff --git a/Composer/packages/extension/.gitignore b/Composer/packages/extension/.gitignore index c3af857904..0b59fe24ac 100644 --- a/Composer/packages/extension/.gitignore +++ b/Composer/packages/extension/.gitignore @@ -1 +1,4 @@ lib/ +src/__tests__/__manifest__.json +src/__tests__/__builtin__ +src/__tests__/__remote__ diff --git a/Composer/packages/extension/jest.config.js b/Composer/packages/extension/jest.config.js new file mode 100644 index 0000000000..81af8fc3ab --- /dev/null +++ b/Composer/packages/extension/jest.config.js @@ -0,0 +1,8 @@ +const path = require('path'); + +const { createConfig } = require('@bfc/test-utils'); + +module.exports = createConfig('extension', 'node', { + setupFiles: [path.resolve(__dirname, 'src/__tests__/setupEnv.ts')], + testPathIgnorePatterns: ['src/__tests__/setupEnv.ts'], +}); diff --git a/Composer/packages/extension/package.json b/Composer/packages/extension/package.json index c48d6b1f87..cd99329ead 100644 --- a/Composer/packages/extension/package.json +++ b/Composer/packages/extension/package.json @@ -7,12 +7,15 @@ "private": true, "scripts": { "build": "yarn build:clean && yarn build:ts", - "build:ts": "tsc", + "build:ts": "tsc -p tsconfig.build.json", "build:clean": "rimraf lib && rimraf build", - "lint": "eslint --quiet ./src" + "lint": "eslint --quiet ./src", + "test": "jest" }, "devDependencies": { + "@bfc/test-utils": "*", "@types/express": "^4.17.6", + "@types/fs-extra": "^9.0.1", "@types/passport": "^1.0.3", "@types/path-to-regexp": "^1.7.0", "json-schema": "^0.2.5", @@ -21,6 +24,7 @@ }, "dependencies": { "debug": "^4.1.1", + "fs-extra": "^9.0.1", "globby": "^11.0.0", "passport": "^0.4.1", "path-to-regexp": "^6.1.0" diff --git a/Composer/packages/extension/src/__tests__/setupEnv.ts b/Composer/packages/extension/src/__tests__/setupEnv.ts new file mode 100644 index 0000000000..42911b1eac --- /dev/null +++ b/Composer/packages/extension/src/__tests__/setupEnv.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import path from 'path'; + +process.env.COMPOSER_EXTENSION_DATA = path.resolve(__dirname, '__manifest__.json'); +process.env.COMPOSER_BUILTIN_EXTENSIONS_DIR = path.resolve(__dirname, '__builtin__'); +process.env.COMPOSER_REMOTE_EXTENSIONS_DIR = path.resolve(__dirname, '__remote__'); diff --git a/Composer/packages/extension/src/loader/pluginLoader.ts b/Composer/packages/extension/src/extensionContext.ts similarity index 85% rename from Composer/packages/extension/src/loader/pluginLoader.ts rename to Composer/packages/extension/src/extensionContext.ts index 5ccc65929e..3ece1f7681 100644 --- a/Composer/packages/extension/src/loader/pluginLoader.ts +++ b/Composer/packages/extension/src/extensionContext.ts @@ -10,12 +10,13 @@ import { pathToRegexp } from 'path-to-regexp'; import glob from 'globby'; import formatMessage from 'format-message'; -import { UserIdentity, ExtensionCollection, RuntimeTemplate, DEFAULT_RUNTIME } from '../types/types'; -import log from '../logger'; +import { UserIdentity, ExtensionCollection, RuntimeTemplate, DEFAULT_RUNTIME } from './types/types'; +import logger from './logger'; +import { ExtensionRegistration } from './extensionRegistration'; -import { ComposerPluginRegistration } from './composerPluginRegistration'; +const log = logger.extend('extension-context'); -export class PluginLoader { +class ExtensionContext { private _passport: passport.PassportStatic; private _webserver: Express | undefined; public loginUri = '/login'; @@ -67,7 +68,8 @@ export class PluginLoader { } public async loadPlugin(name: string, description: string, thisPlugin: any) { - const pluginRegistration = new ComposerPluginRegistration(this, name, description); + log('Loading extension: %s', name); + const pluginRegistration = new ExtensionRegistration(this, name, description); if (typeof thisPlugin.default === 'function') { // the module exported just an init function thisPlugin.default.call(null, pluginRegistration); @@ -82,12 +84,12 @@ export class PluginLoader { } } - public async loadPluginFromFile(path: string) { - const packageJSON = fs.readFileSync(path, 'utf8'); + public async loadPluginFromFile(packageJsonPath: string) { + const packageJSON = fs.readFileSync(packageJsonPath, 'utf8'); const json = JSON.parse(packageJSON); if (json.extendsComposer) { - const modulePath = path.replace(/package\.json$/, ''); + const modulePath = path.dirname(packageJsonPath); try { // eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-var-requires const thisPlugin = require(modulePath); @@ -131,10 +133,10 @@ export class PluginLoader { } } - static async getUserFromRequest(req): Promise { + public async getUserFromRequest(req): Promise { return req.user || undefined; } } -export const pluginLoader = new PluginLoader(); -export default pluginLoader; +const context = new ExtensionContext(); +export { context as ExtensionContext }; diff --git a/Composer/packages/extension/src/loader/composerPluginRegistration.ts b/Composer/packages/extension/src/extensionRegistration.ts similarity index 72% rename from Composer/packages/extension/src/loader/composerPluginRegistration.ts rename to Composer/packages/extension/src/extensionRegistration.ts index 1297eb2af1..8a3d90de86 100644 --- a/Composer/packages/extension/src/loader/composerPluginRegistration.ts +++ b/Composer/packages/extension/src/extensionRegistration.ts @@ -4,26 +4,27 @@ import { RequestHandler } from 'express-serve-static-core'; import { Debugger } from 'debug'; -import log from '../logger'; -import { PublishPlugin, RuntimeTemplate, BotTemplate } from '../types/types'; +import logger from './logger'; +import { PublishPlugin, RuntimeTemplate, BotTemplate } from './types/types'; +import { ExtensionContext } from './extensionContext'; -import { PluginLoader } from './pluginLoader'; +const log = logger.extend('extension-registration'); -export class ComposerPluginRegistration { - public loader: PluginLoader; +export class ExtensionRegistration { + public context: typeof ExtensionContext; private _name: string; private _description: string; private _log: Debugger; - constructor(loader: PluginLoader, name: string, description: string) { - this.loader = loader; + constructor(context: typeof ExtensionContext, name: string, description: string) { + this.context = context; this._name = name; this._description = description; this._log = log.extend(name); } public get passport() { - return this.loader.passport; + return this.context.passport; } public get name(): string { @@ -46,8 +47,8 @@ export class ComposerPluginRegistration { * Storage related features *************************************************************************************/ public async useStorage(customStorageClass: any) { - if (!this.loader.extensions.storage.customStorageClass) { - this.loader.extensions.storage.customStorageClass = customStorageClass; + if (!this.context.extensions.storage.customStorageClass) { + this.context.extensions.storage.customStorageClass = customStorageClass; } else { throw new Error('Cannot redefine storage driver once set.'); } @@ -58,7 +59,7 @@ export class ComposerPluginRegistration { *************************************************************************************/ public async addPublishMethod(plugin: PublishPlugin) { log('registering publish method', this.name); - this.loader.extensions.publish[plugin.customName || this.name] = { + this.context.extensions.publish[plugin.customName || this.name] = { plugin: { name: plugin.customName || this.name, description: plugin.customDescription || this.description, @@ -90,56 +91,56 @@ export class ComposerPluginRegistration { * ``` */ public addRuntimeTemplate(plugin: RuntimeTemplate) { - this.loader.extensions.runtimeTemplates.push(plugin); + this.context.extensions.runtimeTemplates.push(plugin); } /************************************************************************************** * Get current runtime from project *************************************************************************************/ public getRuntimeByProject(project): RuntimeTemplate { - return this.loader.getRuntimeByProject(project); + return this.context.getRuntimeByProject(project); } /************************************************************************************** * Get current runtime by type *************************************************************************************/ public getRuntime(type: string | undefined): RuntimeTemplate { - return this.loader.getRuntime(type); + return this.context.getRuntime(type); } /************************************************************************************** * Add Bot Template (aka, SampleBot) *************************************************************************************/ public addBotTemplate(template: BotTemplate) { - this.loader.extensions.botTemplates.push(template); + this.context.extensions.botTemplates.push(template); } /************************************************************************************** * Add Base Template (aka, BoilerPlate) *************************************************************************************/ public addBaseTemplate(template: BotTemplate) { - this.loader.extensions.baseTemplates.push(template); + this.context.extensions.baseTemplates.push(template); } /************************************************************************************** * Express/web related features *************************************************************************************/ public addWebMiddleware(middleware: RequestHandler) { - if (!this.loader.webserver) { + if (!this.context.webserver) { throw new Error('Plugin loaded in context without webserver. Cannot add web middleware.'); } else { - this.loader.webserver.use(middleware); + this.context.webserver.use(middleware); } } public addWebRoute(type: string, url: string, ...handlers: RequestHandler[]) { - if (!this.loader.webserver) { + if (!this.context.webserver) { throw new Error('Plugin loaded in context without webserver. Cannot add web route.'); } else { - const method = this.loader.webserver[type.toLowerCase()]; + const method = this.context.webserver[type.toLowerCase()]; if (typeof method === 'function') { - method.call(this.loader.webserver, url, ...handlers); + method.call(this.context.webserver, url, ...handlers); } else { throw new Error(`Unhandled web route type ${type}`); } @@ -151,55 +152,55 @@ export class ComposerPluginRegistration { *************************************************************************************/ public usePassportStrategy(passportStrategy) { // set up the passport strategy to be used - this.loader.passport.use(passportStrategy); + this.context.passport.use(passportStrategy); // bind a basic auth middleware. this can be overridden. see setAuthMiddleware below - this.loader.extensions.authentication.middleware = (req, res, next) => { + this.context.extensions.authentication.middleware = (req, res, next) => { if (req.isAuthenticated()) { next(); } else { log('Rejecting access to ', req.url); - res.redirect(this.loader.loginUri); + res.redirect(this.context.loginUri); } }; // set up default serializer, takes entire object and json encodes - this.loader.extensions.authentication.serializeUser = (user, done) => { + this.context.extensions.authentication.serializeUser = (user, done) => { done(null, JSON.stringify(user)); }; // set up default deserializer. - this.loader.extensions.authentication.deserializeUser = (user, done) => { + this.context.extensions.authentication.deserializeUser = (user, done) => { done(null, JSON.parse(user)); }; // use a wrapper on the serializer that calls configured serializer this.passport.serializeUser((user, done) => { - if (this.loader.extensions.authentication.serializeUser) { - this.loader.extensions.authentication.serializeUser(user, done); + if (this.context.extensions.authentication.serializeUser) { + this.context.extensions.authentication.serializeUser(user, done); } }); // use a wrapper on the deserializer that calls configured deserializer this.passport.deserializeUser((user, done) => { - if (this.loader.extensions.authentication.deserializeUser) { - this.loader.extensions.authentication.deserializeUser(user, done); + if (this.context.extensions.authentication.deserializeUser) { + this.context.extensions.authentication.deserializeUser(user, done); } }); } public useAuthMiddleware(middleware: RequestHandler) { - this.loader.extensions.authentication.middleware = middleware; + this.context.extensions.authentication.middleware = middleware; } public useUserSerializers(serialize, deserialize) { - this.loader.extensions.authentication.serializeUser = serialize; - this.loader.extensions.authentication.deserializeUser = deserialize; + this.context.extensions.authentication.serializeUser = serialize; + this.context.extensions.authentication.deserializeUser = deserialize; } public addAllowedUrl(url: string) { - if (this.loader.extensions.authentication.allowedUrls.indexOf(url) < 0) { - this.loader.extensions.authentication.allowedUrls.push(url); + if (this.context.extensions.authentication.allowedUrls.indexOf(url) < 0) { + this.context.extensions.authentication.allowedUrls.push(url); } } } diff --git a/Composer/packages/extension/src/index.ts b/Composer/packages/extension/src/index.ts index 2abf54ebfa..1c381ecc22 100644 --- a/Composer/packages/extension/src/index.ts +++ b/Composer/packages/extension/src/index.ts @@ -3,7 +3,8 @@ export { JSONSchema7 } from 'json-schema'; -export * from './loader'; export * from './manager'; export * from './storage'; export * from './types/types'; +export * from './extensionContext'; +export * from './extensionRegistration'; diff --git a/Composer/packages/extension/src/loader/index.ts b/Composer/packages/extension/src/loader/index.ts deleted file mode 100644 index ae6fcecc77..0000000000 --- a/Composer/packages/extension/src/loader/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export * from './composerPluginRegistration'; -export * from './pluginLoader'; diff --git a/Composer/packages/extension/src/logger.ts b/Composer/packages/extension/src/logger.ts index 8656206e45..0f4dfdf1ac 100644 --- a/Composer/packages/extension/src/logger.ts +++ b/Composer/packages/extension/src/logger.ts @@ -3,4 +3,4 @@ import debug from 'debug'; -export default debug('composer:plugins'); +export default debug('composer:extensions'); diff --git a/Composer/packages/extension/src/manager/__tests__/manager.test.ts b/Composer/packages/extension/src/manager/__tests__/manager.test.ts new file mode 100644 index 0000000000..2f80727d89 --- /dev/null +++ b/Composer/packages/extension/src/manager/__tests__/manager.test.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { writeJsonSync } from 'fs-extra'; + +import { ExtensionManager } from '../manager'; + +const mockManifest = { + extension1: { + id: 'extension1', + builtIn: true, + enabled: true, + }, + extension2: { + id: 'extension2', + enabled: true, + }, + extension3: { + id: 'extension3', + enabled: false, + }, +}; + +beforeEach(() => { + writeJsonSync(process.env.COMPOSER_EXTENSION_DATA as string, mockManifest); + ExtensionManager.reloadManifest(); +}); + +describe('#getAll', () => { + it('return an array of all extensions', () => { + expect(ExtensionManager.getAll()).toEqual([ + { + id: 'extension1', + builtIn: true, + enabled: true, + }, + { + id: 'extension2', + enabled: true, + }, + { + id: 'extension3', + enabled: false, + }, + ]); + }); +}); + +describe('#find', () => { + it('returns extension metadata for id', () => { + expect(ExtensionManager.find('extension1')).toEqual({ id: 'extension1', builtIn: true, enabled: true }); + expect(ExtensionManager.find('does-not-exist')).toBeUndefined(); + }); +}); + +describe('#loadAll', () => { + it('loads built-in extensions and remote extensions that are enabled', async () => { + const loadSpy = jest.spyOn(ExtensionManager, 'load'); + + loadSpy.mockReturnValue(Promise.resolve()); + + await ExtensionManager.loadAll(); + + expect(loadSpy).toHaveBeenCalledTimes(2); + expect(loadSpy).toHaveBeenCalledWith('extension1'); + expect(loadSpy).toHaveBeenCalledWith('extension2'); + }); +}); + +// describe('#installRemote', () => {}); +// describe('#loadBuiltinExtensions', () => {}); +// describe('#loadRemotePlugins', () => {}); +// describe('#load', () => {}); +// describe('#enable', () => {}); +// describe('#disable', () => {}); +// describe('#remove', () => {}); +// describe('#search', () => {}); +// describe('#getAllBundles', () => {}); +// describe('#getBundle', () => {}); diff --git a/Composer/packages/extension/src/manager/manager.ts b/Composer/packages/extension/src/manager/manager.ts index d62c7f1e57..29c80a0ade 100644 --- a/Composer/packages/extension/src/manager/manager.ts +++ b/Composer/packages/extension/src/manager/manager.ts @@ -2,48 +2,22 @@ // Licensed under the MIT License. import path from 'path'; -import { spawn } from 'child_process'; import glob from 'globby'; import { readJson } from 'fs-extra'; -import { pluginLoader } from '../loader'; +import { ExtensionContext } from '../extensionContext'; import logger from '../logger'; import { ExtensionManifestStore } from '../storage/extensionManifestStore'; import { ExtensionBundle, PackageJSON, ExtensionMetadata, ExtensionSearchResult } from '../types/extension'; +import { npm } from '../utils/npm'; -const log = logger.extend('plugins'); +const log = logger.extend('manager'); -/** - * Used to safely execute commands that include user input - */ -async function runNpm(command: string): Promise<{ stdout: string; stderr: string }> { - return new Promise((resolve) => { - log('npm %s', command); - const cmdArgs = command.split(' '); - let stdout = ''; - let stderr = ''; - - const proc = spawn('npm', cmdArgs); - - proc.stdout.on('data', (data) => { - stdout += data; - }); - - proc.stderr.on('data', (data) => { - stderr += data; - }); - - proc.on('close', () => { - resolve({ stdout, stderr }); - }); - }); -} - -function processBundles(pluginPath: string, bundles: ExtensionBundle[]) { +function processBundles(extensionPath: string, bundles: ExtensionBundle[]) { return bundles.map((b) => ({ ...b, - path: path.resolve(pluginPath, b.path), + path: path.resolve(extensionPath, b.path), })); } @@ -68,7 +42,7 @@ class ExtensionManager { */ public getAll() { const extensions = this.manifest.getExtensions(); - return Object.keys(extensions).map((extId) => extensions[extId]); + return Object.values(extensions); } /** @@ -76,139 +50,127 @@ class ExtensionManager { * @param id Id of the extension to search for */ public find(id: string) { - return this.manifest.getExtensions()[id]; + return this.manifest.getExtensionConfig(id); } /** - * Installs a remote plugin via NPM - * @param name The name of the plugin to install - * @param version The version of the plugin to install + * Loads all builtin extensions and remote extensions. */ - public async installRemote(name: string, version?: string) { - const packageNameAndVersion = version ? `${name}@${version}` : name; - const cmd = `install --no-audit --prefix ${this.remotePluginsDir} ${packageNameAndVersion}`; - log('Installing %s@%s to %s', name, version, this.remotePluginsDir); - - const { stdout } = await runNpm(cmd); - - log('%s', stdout); + public async loadAll() { + await this.seedBuiltinExtensions(); - const packageJson = await this.getPackageJson(name); + const extensions = Object.entries(this.manifest.getExtensions()); - if (packageJson) { - const pluginPath = path.resolve(this.remotePluginsDir, 'node_modules', name); - this.manifest.updateExtensionConfig(name, getExtensionMetadata(pluginPath, packageJson)); - } else { - throw new Error(`Unable to install ${packageNameAndVersion}`); + for (const [id, metadata] of extensions) { + if (metadata?.enabled) { + await this.load(id); + } } } /** - * Loads all the plugins that are checked into the Composer project (1P plugins) + * Installs a remote extension via NPM + * @param name The name of the extension to install + * @param version The version of the extension to install */ - public async loadBuiltinPlugins() { - log('Loading inherent plugins from: ', this.builtinPluginsDir); - - // get all plugins with a package.json in the plugins dir - const plugins = await glob('*/package.json', { cwd: this.builtinPluginsDir, dot: true }); - for (const p in plugins) { - // go through each plugin, make sure to add it to the manager store then load it as usual - const pluginPackageJsonPath = plugins[p]; - const fullPath = path.join(this.builtinPluginsDir, pluginPackageJsonPath); - const pluginInstallPath = path.dirname(fullPath); - const packageJson = (await readJson(fullPath)) as PackageJSON; - if (packageJson && (!!packageJson.composer || !!packageJson.extendsComposer)) { - const metadata = getExtensionMetadata(pluginInstallPath, packageJson); - this.manifest.updateExtensionConfig(packageJson.name, { - ...metadata, - builtIn: true, - }); - await pluginLoader.loadPluginFromFile(fullPath); + public async installRemote(name: string, version?: string) { + const packageNameAndVersion = version ? `${name}@${version}` : `${name}@latest`; + log('Installing %s to %s', packageNameAndVersion, this.remoteDir); + + try { + const { stdout } = await npm('install', packageNameAndVersion, { '--prefix': this.remoteDir }); + + log('%s', stdout); + + const packageJson = await this.getPackageJson(name, this.remoteDir); + + if (packageJson) { + const extensionPath = path.resolve(this.remoteDir, 'node_modules', name); + this.manifest.updateExtensionConfig(name, getExtensionMetadata(extensionPath, packageJson)); + } else { + throw new Error(`Unable to install ${packageNameAndVersion}`); } + } catch (err) { + if (err?.stderr) { + log('%s', err.stderr); + } + throw new Error(`Unable to install ${packageNameAndVersion}`); } } - /** - * Loads all installed remote plugins - * TODO (toanzian / abrown): Needs to be implemented - */ - public async loadRemotePlugins() { - // should perform the same function as loadBuiltInPlugins but from the - // location that remote / 3P plugins are installed - } - public async load(id: string) { + const metadata = this.manifest.getExtensionConfig(id); try { - const modulePath = require.resolve(id, { - paths: [`${this.remotePluginsDir}/node_modules`], - }); // eslint-disable-next-line @typescript-eslint/no-var-requires, security/detect-non-literal-require - const plugin = require(modulePath); - log('got plugin: ', plugin); + const extension = metadata?.path && require(metadata.path); - if (!plugin) { - throw new Error('Plugin not found'); + if (!extension) { + throw new Error(`Extension not found: ${id}`); } - await pluginLoader.loadPlugin(id, '', plugin); + await ExtensionContext.loadPlugin(id, '', extension); } catch (err) { - log('Unable to load plugin `%s`', id); + log('Unable to load extension `%s`', id); log('%O', err); - await this.remove(id); + if (!metadata?.builtIn) { + await this.remove(id); + } throw err; } } /** - * Enables a plugin - * @param id Id of the plugin to be enabled + * Enables an extension + * @param id Id of the extension to be enabled */ public async enable(id: string) { this.manifest.updateExtensionConfig(id, { enabled: true }); - // re-load plugin + await this.load(id); } /** - * Disables a plugin - * @param id Id of the plugin to be disabled + * Disables an extension + * @param id Id of the extension to be disabled */ public async disable(id: string) { this.manifest.updateExtensionConfig(id, { enabled: false }); - // tear down plugin? + // TODO: tear down extension? } /** - * Removes a remote plugin via NPM - * @param id Id of the plugin to be removed + * Removes a remote extension via NPM + * @param id Id of the extension to be removed */ public async remove(id: string) { - const cmd = `uninstall --no-audit --prefix ${this.remotePluginsDir} ${id}`; log('Removing %s', id); - const { stdout } = await runNpm(cmd); + try { + const { stdout } = await npm('uninstall', id, { '--prefix': this.remoteDir }); - log('%s', stdout); + log('%s', stdout); - this.manifest.removeExtension(id); + this.manifest.removeExtension(id); + } catch (err) { + log('%s', err); + throw new Error(`Unable to remove extension: ${id}`); + } } /** - * Searches for a plugin via NPM's search function + * Searches for an extension via NPM's search function * @param query The search query */ public async search(query: string) { - const cmd = `search --json keywords:botframework-composer ${query}`; - - const { stdout } = await runNpm(cmd); + const { stdout } = await npm('search', `keywords:botframework-composer extension ${query}`, { '--json': '' }); try { const result = JSON.parse(stdout); if (Array.isArray(result)) { result.forEach((searchResult) => { const { name, keywords = [], version, description, links } = searchResult; - if (keywords.includes('botframework-composer')) { + if (keywords.includes('botframework-composer') && keywords.includes('extension')) { const url = links?.npm ?? ''; this.searchCache.set(name, { id: name, @@ -235,7 +197,7 @@ class ExtensionManager { const info = this.find(id); if (!info) { - throw new Error('plugin not found'); + throw new Error('extension not found'); } return info.bundles ?? []; @@ -250,7 +212,7 @@ class ExtensionManager { const info = this.find(id); if (!info) { - throw new Error('plugin not found'); + throw new Error('extension not found'); } const bundle = info.bundles.find((b) => b.id === bundleId); @@ -262,41 +224,64 @@ class ExtensionManager { return bundle.path; } - private async getPackageJson(id: string): Promise { + private async getPackageJson(id: string, dir: string): Promise { try { - const pluginPackagePath = path.resolve(this.remotePluginsDir, 'node_modules', id, 'package.json'); - log('fetching package.json for %s at %s', id, pluginPackagePath); - const packageJson = await readJson(pluginPackagePath); + const extensionPackagePath = path.resolve(dir, 'node_modules', id, 'package.json'); + log('fetching package.json for %s at %s', id, extensionPackagePath); + const packageJson = await readJson(extensionPackagePath); return packageJson as PackageJSON; } catch (err) { log('Error getting package json for %s', id); + // eslint-disable-next-line no-console console.error(err); } } + public async seedBuiltinExtensions() { + const extensions = await glob('*/package.json', { cwd: this.builtinDir, dot: true }); + for (const extensionPackageJsonPath of extensions) { + // go through each extension, make sure to add it to the manager store then load it as usual + const fullPath = path.join(this.builtinDir, extensionPackageJsonPath); + const extensionInstallPath = path.dirname(fullPath); + const packageJson = (await readJson(fullPath)) as PackageJSON; + const isEnabled = packageJson?.composer && packageJson.composer.enabled !== false; + if (packageJson && (isEnabled || packageJson.extendsComposer === true)) { + const metadata = getExtensionMetadata(extensionInstallPath, packageJson); + this.manifest.updateExtensionConfig(packageJson.name, { + ...metadata, + builtIn: true, + }); + } + } + } + + public reloadManifest() { + this._manifest = undefined; + } + private get manifest() { if (this._manifest) { return this._manifest; } - this._manifest = new ExtensionManifestStore(); + this._manifest = new ExtensionManifestStore(process.env.COMPOSER_EXTENSION_DATA as string); return this._manifest; } - private get builtinPluginsDir() { - if (!process.env.COMPOSER_BUILTIN_PLUGINS_DIR) { - throw new Error('COMPOSER_BUILTIN_PLUGINS_DIR must be set.'); + private get builtinDir() { + if (!process.env.COMPOSER_BUILTIN_EXTENSIONS_DIR) { + throw new Error('COMPOSER_BUILTIN_EXTENSIONS_DIR must be set.'); } - return process.env.COMPOSER_BUILTIN_PLUGINS_DIR; + return process.env.COMPOSER_BUILTIN_EXTENSIONS_DIR; } - private get remotePluginsDir() { - if (!process.env.COMPOSER_REMOTE_PLUGINS_DIR) { - throw new Error('COMPOSER_REMOTE_PLUGINS_DIR must be set.'); + private get remoteDir() { + if (!process.env.COMPOSER_REMOTE_EXTENSIONS_DIR) { + throw new Error('COMPOSER_REMOTE_EXTENSIONS_DIR must be set.'); } - return process.env.COMPOSER_REMOTE_PLUGINS_DIR; + return process.env.COMPOSER_REMOTE_EXTENSIONS_DIR; } } diff --git a/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts b/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts new file mode 100644 index 0000000000..3b67b42bb5 --- /dev/null +++ b/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import fs from 'fs'; +import path from 'path'; + +import { existsSync, writeJsonSync, readJsonSync } from 'fs-extra'; + +import { ExtensionManifestStore } from '../extensionManifestStore'; + +jest.mock('fs-extra', () => ({ + existsSync: jest.fn(), + readJsonSync: jest.fn(), + writeJsonSync: jest.fn(), +})); + +const manifestPath = path.resolve(__dirname, '../../../__manifest__.json'); + +afterAll(() => { + if (fs.existsSync(manifestPath)) { + fs.unlinkSync(manifestPath); + } +}); + +describe('when the manifest does not exist', () => { + it('creates one with the default data', () => { + (existsSync as jest.Mock).mockReturnValue(false); + new ExtensionManifestStore(manifestPath); + expect(writeJsonSync).toHaveBeenCalledWith(manifestPath, {}, { spaces: 2 }); + }); +}); + +describe('when the manifest already exists', () => { + const currentManifest = { + extension1: { + id: 'extension1', + }, + }; + + beforeAll(() => { + if (fs.existsSync(manifestPath)) { + fs.unlinkSync(manifestPath); + } + + fs.writeFileSync(manifestPath, JSON.stringify({})); + }); + + beforeEach(() => { + (existsSync as jest.Mock).mockReturnValue(true); + (readJsonSync as jest.Mock).mockImplementation((path) => { + if (path === manifestPath) { + return { ...currentManifest }; + } + }); + }); + + it('reads from the manifest', () => { + new ExtensionManifestStore(manifestPath); + expect(readJsonSync).toHaveBeenCalledWith(manifestPath); + }); + + describe('#getExtensionConfig', () => { + it('returns the extension metadata', () => { + const store = new ExtensionManifestStore(manifestPath); + + expect(store.getExtensionConfig('extension1')).toEqual(currentManifest.extension1); + }); + }); + + describe('#getExtensions', () => { + it('returns all extension metadata', () => { + const store = new ExtensionManifestStore(manifestPath); + + expect(store.getExtensions()).toEqual(currentManifest); + }); + }); + + describe('#updateExtensionConfig', () => { + it('creates a new entry if config does not exist', () => { + const newExtension = { id: 'newExtension' }; + const store = new ExtensionManifestStore(manifestPath); + store.updateExtensionConfig('newExtension', newExtension); + + expect(writeJsonSync).toHaveBeenCalledWith( + manifestPath, + { ...currentManifest, newExtension }, + expect.any(Object) + ); + }); + + it('updates the entry if config exist', () => { + const store = new ExtensionManifestStore(manifestPath); + store.updateExtensionConfig('extension1', { name: 'new name' }); + + expect(writeJsonSync).toHaveBeenCalledWith( + manifestPath, + { extension1: { id: 'extension1', name: 'new name' } }, + expect.any(Object) + ); + }); + }); + + describe('#removeExtension', () => { + it('removes the extension from the manifest', () => { + const store = new ExtensionManifestStore(manifestPath); + store.removeExtension('extension1'); + + expect(writeJsonSync).toHaveBeenCalledWith(manifestPath, {}, expect.any(Object)); + expect(store.getExtensionConfig('extension1')).toBeUndefined(); + expect(store.getExtensions()).toEqual({}); + }); + }); +}); diff --git a/Composer/packages/extension/src/storage/extensionManifestStore.ts b/Composer/packages/extension/src/storage/extensionManifestStore.ts index 99f2079dee..4483d9555c 100644 --- a/Composer/packages/extension/src/storage/extensionManifestStore.ts +++ b/Composer/packages/extension/src/storage/extensionManifestStore.ts @@ -8,79 +8,80 @@ import { ExtensionMap, ExtensionMetadata } from '../types/extension'; const log = logger.extend('plugins'); -export interface ExtensionManifest { - extensions: ExtensionMap; -} - -const DEFAULT_MANIFEST: ExtensionManifest = { - extensions: {}, -}; +export type ExtensionManifest = ExtensionMap; -function omitBuiltinProperty(key: string, value: string) { - if (key && key === 'builtIn') { - return undefined; - } - return value; -} +const DEFAULT_MANIFEST: ExtensionManifest = {}; /** In-memory representation of extensions.json as well as reads / writes data to disk. */ export class ExtensionManifestStore { private manifest: ExtensionManifest = DEFAULT_MANIFEST; - private manifestPath: string; - constructor() { - this.manifestPath = process.env.COMPOSER_EXTENSION_DATA as string; + constructor(private manifestPath: string) { // create extensions.json if it doesn't exist + if (!existsSync(this.manifestPath)) { log('extensions.json does not exist yet. Writing file to path: %s', this.manifestPath); writeJsonSync(this.manifestPath, DEFAULT_MANIFEST, { spaces: 2 }); } - this.readManifestFromDisk(); // load manifest into memory - } - // load manifest into memory - private readManifestFromDisk() { - try { - const manifest: ExtensionManifest = readJsonSync(this.manifestPath); - this.manifest = manifest; - } catch (e) { - log('Error reading %s: %s', this.manifestPath, e); - } - } + this.readManifestFromDisk(); // load manifest into memory - // write manifest from memory to disk - private writeManifestToDisk() { - try { - writeJsonSync(this.manifestPath, this.manifest, { replacer: omitBuiltinProperty, spaces: 2 }); - } catch (e) { - log('Error writing %s: %s', this.manifestPath, e); + // remove extensions key from existing manifests + // TODO: remove in the future + /* istanbul ignore next */ + if (this.manifest && this.manifest.extensions) { + this.manifest = (this.manifest.extensions as unknown) as ExtensionMap; + this.writeManifestToDisk(); } } public getExtensionConfig(id: string) { - return this.manifest.extensions[id]; + return this.manifest[id]; } public getExtensions() { - return this.manifest.extensions; + return this.manifest; } public removeExtension(id: string) { - delete this.manifest.extensions[id]; + delete this.manifest[id]; // sync changes to disk this.writeManifestToDisk(); } // update extension config public updateExtensionConfig(id: string, newConfig: Partial) { - const currentConfig = this.manifest.extensions[id]; + const currentConfig = this.manifest[id]; if (currentConfig) { - this.manifest.extensions[id] = Object.assign(currentConfig, newConfig); + this.manifest[id] = Object.assign({}, currentConfig, newConfig); } else { - this.manifest.extensions[id] = Object.assign({} as ExtensionMetadata, newConfig); + this.manifest[id] = Object.assign({} as ExtensionMetadata, newConfig); } // sync changes to disk this.writeManifestToDisk(); } + + public reload() { + this.readManifestFromDisk(); + } + + // load manifest into memory + private readManifestFromDisk() { + try { + const manifest: ExtensionManifest = readJsonSync(this.manifestPath); + this.manifest = manifest; + } catch (e) { + log('Error reading %s: %s', this.manifestPath, e); + } + } + + // write manifest from memory to disk + private writeManifestToDisk() { + try { + writeJsonSync(this.manifestPath, this.manifest, { spaces: 2 }); + } catch (e) { + log('Error writing %s: %s', this.manifestPath, e); + } + } } diff --git a/Composer/packages/extension/src/types/extension.ts b/Composer/packages/extension/src/types/extension.ts index c0ae780e5a..8fceeda395 100644 --- a/Composer/packages/extension/src/types/extension.ts +++ b/Composer/packages/extension/src/types/extension.ts @@ -20,7 +20,7 @@ export type ExtensionBundle = { path: string; }; -export interface ExtensionMetadata { +export type ExtensionMetadata = { /** name field from package.json */ id: string; /** name field from composer object in package.json, defaults to id */ @@ -35,10 +35,10 @@ export interface ExtensionMetadata { builtIn?: boolean; bundles: ExtensionBundle[]; contributes: ExtensionContribution; -} +}; export interface ExtensionMap { - [id: string]: ExtensionMetadata; + [id: string]: ExtensionMetadata | undefined; } /** Info about a plugin returned from an NPM search query */ @@ -58,6 +58,7 @@ export interface PackageJSON { extendsComposer: boolean; composer?: { name?: string; + enabled?: boolean; contributes?: ExtensionContribution; bundles?: ExtensionBundle[]; }; diff --git a/Composer/packages/extension/src/types/types.ts b/Composer/packages/extension/src/types/types.ts index 76854b0fbe..888ce72c08 100644 --- a/Composer/packages/extension/src/types/types.ts +++ b/Composer/packages/extension/src/types/types.ts @@ -53,7 +53,7 @@ export interface PublishPlugin { instructions?: string; customName?: string; customDescription?: string; - hasView: boolean; + hasView?: boolean; [key: string]: any; } @@ -117,7 +117,7 @@ export interface ExtensionCollection { /** (Optional) Schema for publishing configuration. */ schema?: JSONSchema7; /** Whether or not the plugin has custom UI to host in the publish surface */ - hasView: boolean; + hasView?: boolean; }; methods: PublishPlugin; }; diff --git a/Composer/packages/extension/src/utils/npm.ts b/Composer/packages/extension/src/utils/npm.ts new file mode 100644 index 0000000000..043ff76bb4 --- /dev/null +++ b/Composer/packages/extension/src/utils/npm.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { spawn } from 'child_process'; + +import logger from '../logger'; + +const log = logger.extend('npm'); + +type NpmOutput = { + stdout: string; + stderr: string; + code: number; +}; +type NpmCommand = 'install' | 'uninstall' | 'search'; +type NpmOptions = { + [key: string]: string; +}; + +function processOptions(opts: NpmOptions) { + return Object.entries({ '--no-fund': '', '--no-audit': '', ...opts }).map(([flag, value]) => { + return value ? `${flag}=${value}` : flag; + }); +} + +/** + * Executes npm commands that include user input safely + * @param `command` npm command to execute. + * @param `args` cli arguments + * @param `opts` cli flags + * @returns Object with stdout, stderr, and exit code from command + */ +export async function npm(command: NpmCommand, args: string, opts: NpmOptions = {}): Promise { + return new Promise((resolve, reject) => { + const cmdOptions = processOptions(opts); + const spawnArgs = [command, ...cmdOptions, args]; + log('npm %s', spawnArgs.join(' ')); + let stdout = ''; + let stderr = ''; + + const proc = spawn('npm', spawnArgs); + + proc.stdout.on('data', (data) => { + stdout += data; + }); + + proc.stderr.on('data', (data) => { + stderr += data; + }); + + proc.on('close', (code) => { + if (code > 0) { + reject({ stdout, stderr, code }); + } else { + resolve({ stdout, stderr, code }); + } + }); + }); +} diff --git a/Composer/packages/extension/tsconfig.build.json b/Composer/packages/extension/tsconfig.build.json new file mode 100644 index 0000000000..3d2578d39e --- /dev/null +++ b/Composer/packages/extension/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + /* Options used for building production code (tests excluded) */ + "extends": "./tsconfig.json", + "exclude": [ + "**/__tests__/**", + "**/__mocks__/**" + ] +} diff --git a/Composer/packages/server/.gitignore b/Composer/packages/server/.gitignore index 46e4d8f37b..3a2f30f54c 100644 --- a/Composer/packages/server/.gitignore +++ b/Composer/packages/server/.gitignore @@ -1,7 +1,6 @@ build/ data.json -__tests__/__data__.json +src/__tests__/__data__.json schemas/locales/en-US-pseudo/ src/locales/en-US-pseudo.json -.composer extensions.json diff --git a/Composer/packages/server/jest.config.js b/Composer/packages/server/jest.config.js index af5aea9fbf..96ddb74adc 100644 --- a/Composer/packages/server/jest.config.js +++ b/Composer/packages/server/jest.config.js @@ -3,6 +3,6 @@ const path = require('path'); const { createConfig } = require('@bfc/test-utils'); module.exports = createConfig('server', 'node', { - setupFiles: [path.resolve(__dirname, '__tests__/setupEnv.ts')], - testPathIgnorePatterns: ['__tests__/setupEnv.ts'], + setupFiles: [path.resolve(__dirname, 'src/__tests__/setupEnv.ts')], + testPathIgnorePatterns: ['src/__tests__/setupEnv.ts'], }); diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 1d3f0e8dd8..aa25dff4b3 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -17,14 +17,15 @@ "test": "jest", "test:watch": "jest --watch", "typecheck": "tsc --noEmit", - "lint": "eslint --quiet ./src ./__tests__", + "lint": "eslint --quiet ./src", "lint:fix": "yarn lint --fix" }, "author": "", "nodemonConfig": { "exec": "cross-env TS_NODE_FILES=true node -r ts-node/register src/init.ts", "watch": [ - "src" + "src", + "../extension/lib" ], "ext": "ts", "delay": 2 diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/bot1.dialog b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/bot1.dialog similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/bot1.dialog rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/bot1.dialog diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/a.dialog b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/a.dialog similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/a.dialog rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/a.dialog diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/knowledge-base/en-us/a.en-us.qna b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/knowledge-base/en-us/a.en-us.qna similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/knowledge-base/en-us/a.en-us.qna rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/knowledge-base/en-us/a.en-us.qna diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/language-generation/en-us/a.en-us.lg b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/language-generation/en-us/a.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/language-generation/en-us/a.en-us.lg rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/language-generation/en-us/a.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/language-understanding/en-us/a.en-us.lu b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/language-understanding/en-us/a.en-us.lu similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/a/language-understanding/en-us/a.en-us.lu rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/a/language-understanding/en-us/a.en-us.lu diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/b.dialog b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/b.dialog similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/b.dialog rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/b.dialog diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/knowledge-base/en-us/b.en-us.qna b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/knowledge-base/en-us/b.en-us.qna similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/knowledge-base/en-us/b.en-us.qna rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/knowledge-base/en-us/b.en-us.qna diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/language-generation/en-us/b.en-us.lg b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/language-generation/en-us/b.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/language-generation/en-us/b.en-us.lg rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/language-generation/en-us/b.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/language-understanding/en-us/b.en-us.lu b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/language-understanding/en-us/b.en-us.lu similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/dialogs/b/language-understanding/en-us/b.en-us.lu rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/dialogs/b/language-understanding/en-us/b.en-us.lu diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/knowledge-base/en-us/bot1.en-us.qna b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/knowledge-base/en-us/bot1.en-us.qna similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/knowledge-base/en-us/bot1.en-us.qna rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/knowledge-base/en-us/bot1.en-us.qna diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/language-generation/en-us/bot1.en-us.lg b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/language-generation/en-us/bot1.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/language-generation/en-us/bot1.en-us.lg rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/language-generation/en-us/bot1.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/language-generation/en-us/common.en-us.lg b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/language-generation/en-us/common.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/language-generation/en-us/common.en-us.lg rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/language-generation/en-us/common.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/language-understanding/en-us/bot1.en-us.lu b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/language-understanding/en-us/bot1.en-us.lu similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/language-understanding/en-us/bot1.en-us.lu rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/language-understanding/en-us/bot1.en-us.lu diff --git a/Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/settings/appsettings.json b/Composer/packages/server/src/__mocks__/asset/projects/SampleBot/settings/appsettings.json similarity index 100% rename from Composer/packages/server/__tests__/mocks/asset/projects/SampleBot/settings/appsettings.json rename to Composer/packages/server/src/__mocks__/asset/projects/SampleBot/settings/appsettings.json diff --git a/Composer/packages/server/__tests__/mocks/runtimes/CSharp/readme.md b/Composer/packages/server/src/__mocks__/runtimes/CSharp/readme.md similarity index 100% rename from Composer/packages/server/__tests__/mocks/runtimes/CSharp/readme.md rename to Composer/packages/server/src/__mocks__/runtimes/CSharp/readme.md diff --git a/Composer/packages/server/__tests__/mocks/runtimes/dotnet/readme.md b/Composer/packages/server/src/__mocks__/runtimes/dotnet/readme.md similarity index 100% rename from Composer/packages/server/__tests__/mocks/runtimes/dotnet/readme.md rename to Composer/packages/server/src/__mocks__/runtimes/dotnet/readme.md diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/bot1.dialog b/Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.dialog similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/bot1.dialog rename to Composer/packages/server/src/__mocks__/samplebots/bot1/bot1.dialog diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/a/a.dialog b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/a.dialog similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/a/a.dialog rename to Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/a.dialog diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/a/language-generation/en-us/a.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/language-generation/en-us/a.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/a/language-generation/en-us/a.en-us.lg rename to Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/language-generation/en-us/a.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/a/language-understanding/en-us/a.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/language-understanding/en-us/a.en-us.lu similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/a/language-understanding/en-us/a.en-us.lu rename to Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/a/language-understanding/en-us/a.en-us.lu diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/b/b.dialog b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/b.dialog similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/b/b.dialog rename to Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/b.dialog diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/b/language-generation/en-us/b.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/language-generation/en-us/b.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/b/language-generation/en-us/b.en-us.lg rename to Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/language-generation/en-us/b.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/b/language-understanding/en-us/b.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/language-understanding/en-us/b.en-us.lu similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/dialogs/b/language-understanding/en-us/b.en-us.lu rename to Composer/packages/server/src/__mocks__/samplebots/bot1/dialogs/b/language-understanding/en-us/b.en-us.lu diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/language-generation/en-us/bot1.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/bot1/language-generation/en-us/bot1.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/language-generation/en-us/bot1.en-us.lg rename to Composer/packages/server/src/__mocks__/samplebots/bot1/language-generation/en-us/bot1.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/language-generation/en-us/common.en-us.lg b/Composer/packages/server/src/__mocks__/samplebots/bot1/language-generation/en-us/common.en-us.lg similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/language-generation/en-us/common.en-us.lg rename to Composer/packages/server/src/__mocks__/samplebots/bot1/language-generation/en-us/common.en-us.lg diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/language-understanding/en-us/bot1.en-us.lu b/Composer/packages/server/src/__mocks__/samplebots/bot1/language-understanding/en-us/bot1.en-us.lu similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/language-understanding/en-us/bot1.en-us.lu rename to Composer/packages/server/src/__mocks__/samplebots/bot1/language-understanding/en-us/bot1.en-us.lu diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/settings/appsettings.json b/Composer/packages/server/src/__mocks__/samplebots/bot1/settings/appsettings.json similarity index 100% rename from Composer/packages/server/__tests__/mocks/samplebots/bot1/settings/appsettings.json rename to Composer/packages/server/src/__mocks__/samplebots/bot1/settings/appsettings.json diff --git a/Composer/packages/server/__tests__/setupEnv.ts b/Composer/packages/server/src/__tests__/setupEnv.ts similarity index 100% rename from Composer/packages/server/__tests__/setupEnv.ts rename to Composer/packages/server/src/__tests__/setupEnv.ts diff --git a/Composer/packages/server/__tests__/controllers/asset.test.ts b/Composer/packages/server/src/controllers/__tests__/asset.test.ts similarity index 90% rename from Composer/packages/server/__tests__/controllers/asset.test.ts rename to Composer/packages/server/src/controllers/__tests__/asset.test.ts index e4cd1671ac..85415fe0cd 100644 --- a/Composer/packages/server/__tests__/controllers/asset.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/asset.test.ts @@ -3,7 +3,7 @@ import { Request, Response } from 'express'; -import { AssetController } from '../../src/controllers/asset'; +import { AssetController } from '../asset'; let mockRes: Response; diff --git a/Composer/packages/server/__tests__/controllers/eject.test.ts b/Composer/packages/server/src/controllers/__tests__/eject.test.ts similarity index 85% rename from Composer/packages/server/__tests__/controllers/eject.test.ts rename to Composer/packages/server/src/controllers/__tests__/eject.test.ts index cc8983562a..eb621a17d9 100644 --- a/Composer/packages/server/__tests__/controllers/eject.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/eject.test.ts @@ -3,22 +3,20 @@ import { Request, Response } from 'express'; import rimraf from 'rimraf'; -import { pluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; -import { BotProjectService } from '../../src/services/project'; -import { Path } from '../../src/utility/path'; -import { EjectController } from '../../src/controllers/eject'; +import { BotProjectService } from '../../services/project'; +import { Path } from '../../utility/path'; +import { EjectController } from '../eject'; jest.mock('@bfc/extension', () => { return { - pluginLoader: { + ExtensionContext: { extensions: { botTemplates: [], baseTemplates: [], runtimeTemplates: [], }, - }, - PluginLoader: { getUserFromRequest: jest.fn(), }, }; @@ -26,8 +24,8 @@ jest.mock('@bfc/extension', () => { let mockRes: Response; -const useFortest = Path.resolve(__dirname, '../mocks/samplebots/testEject'); -const bot1 = Path.resolve(__dirname, '../mocks/samplebots/bot1'); +const useFortest = Path.resolve(__dirname, '../../__mocks__/samplebots/testEject'); +const bot1 = Path.resolve(__dirname, '../../__mocks__/samplebots/bot1'); const location1 = { storageId: 'default', @@ -48,7 +46,7 @@ beforeEach(() => { }); beforeAll(async () => { - pluginLoader.extensions.runtimeTemplates.push({ + ExtensionContext.extensions.runtimeTemplates.push({ key: 'azurewebapp', name: 'C#', startCommand: 'dotnet run --project azurewebapp', diff --git a/Composer/packages/server/src/controllers/__tests__/extensions.test.ts b/Composer/packages/server/src/controllers/__tests__/extensions.test.ts new file mode 100644 index 0000000000..bb220dfea7 --- /dev/null +++ b/Composer/packages/server/src/controllers/__tests__/extensions.test.ts @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Request, Response } from 'express'; +import { ExtensionManager } from '@bfc/extension'; + +import * as ExtensionsController from '../extensions'; + +jest.mock('@bfc/extension', () => ({ + ExtensionManager: { + disable: jest.fn(), + enable: jest.fn(), + find: jest.fn(), + getAll: jest.fn(), + getBundle: jest.fn(), + installRemote: jest.fn(), + load: jest.fn(), + remove: jest.fn(), + search: jest.fn(), + }, +})); + +const req: Request = {} as Request; +let res: Response = {} as Response; + +beforeEach(() => { + res = ({ + json: jest.fn(), + status: jest.fn().mockReturnThis(), + sendFile: jest.fn().mockReturnThis(), + } as unknown) as Response; +}); + +describe('listing all extensions', () => { + it('returns all extensions', () => { + (ExtensionManager.getAll as jest.Mock).mockReturnValue(['list', 'of', 'extensions']); + + ExtensionsController.listExtensions(req, res); + expect(res.json).toHaveBeenCalledWith(['list', 'of', 'extensions']); + }); +}); + +describe('adding an extension', () => { + const id = 'new-extension'; + + it('validates id parameter', async () => { + await ExtensionsController.addExtension({ body: { id: '' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + it('installs a remote extension', async () => { + await ExtensionsController.addExtension({ body: { id, version: 'some-version' } } as Request, res); + + expect(ExtensionManager.installRemote).toHaveBeenCalledWith(id, 'some-version'); + }); + + it('loads the extension', async () => { + await ExtensionsController.addExtension({ body: { id } } as Request, res); + + expect(ExtensionManager.load).toHaveBeenCalledWith(id); + }); + + it('returns the extension', async () => { + (ExtensionManager.find as jest.Mock).mockReturnValue('installed extension'); + await ExtensionsController.addExtension({ body: { id } } as Request, res); + + expect(ExtensionManager.find).toHaveBeenCalledWith(id); + expect(res.json).toHaveBeenCalledWith('installed extension'); + }); +}); + +describe('toggling an extension', () => { + it('validates id parameter', async () => { + await ExtensionsController.toggleExtension({ body: { id: '' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + it('returns a 404 if the extension is not found', async () => { + (ExtensionManager.find as jest.Mock).mockReturnValue(null); + await ExtensionsController.toggleExtension({ body: { id: 'does-not-exist' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + describe('when extension is found', () => { + const id = 'extension-id'; + + beforeEach(() => { + (ExtensionManager.find as jest.Mock).mockReturnValue('found extension'); + }); + + it('can enable an extension', async () => { + await ExtensionsController.toggleExtension({ body: { id, enabled: true } } as Request, res); + expect(ExtensionManager.enable).toBeCalledWith(id); + }); + + it('can disable an extension', async () => { + await ExtensionsController.toggleExtension({ body: { id, enabled: false } } as Request, res); + await ExtensionsController.toggleExtension({ body: { id, enabled: 'true' } } as Request, res); + await ExtensionsController.toggleExtension({ body: { id, enabled: '' } } as Request, res); + await ExtensionsController.toggleExtension({ body: { id, enabled: 1 } } as Request, res); + expect(ExtensionManager.disable).toBeCalledTimes(4); + }); + + it('returns the updated extension', async () => { + await ExtensionsController.toggleExtension({ body: { id, enabled: true } } as Request, res); + + expect(res.json).toBeCalledWith('found extension'); + }); + }); +}); + +describe('removing an extension', () => { + it('validates id parameter', async () => { + await ExtensionsController.removeExtension({ body: { id: '' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + it('returns a 404 if the extension is not found', async () => { + (ExtensionManager.find as jest.Mock).mockReturnValue(null); + await ExtensionsController.removeExtension({ body: { id: 'does-not-exist' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + describe('when extension is found', () => { + const id = 'extension-id'; + + beforeEach(() => { + (ExtensionManager.find as jest.Mock).mockReturnValue('found extension'); + }); + + it('removes the extension', async () => { + await ExtensionsController.removeExtension({ body: { id } } as Request, res); + expect(ExtensionManager.remove).toHaveBeenCalledWith(id); + }); + + it('returns the list of extensions', async () => { + (ExtensionManager.getAll as jest.Mock).mockReturnValue(['list', 'of', 'extensions']); + + await ExtensionsController.removeExtension({ body: { id } } as Request, res); + expect(res.json).toHaveBeenCalledWith(['list', 'of', 'extensions']); + }); + }); +}); + +describe('searching extensions', () => { + it('returns the search result', async () => { + (ExtensionManager.search as jest.Mock).mockReturnValue(['search', 'results']); + await ExtensionsController.searchExtensions({ query: { q: 'search query' } } as Request, res); + + expect(ExtensionManager.search).toHaveBeenCalledWith('search query'); + expect(res.json).toHaveBeenCalledWith(['search', 'results']); + }); +}); + +describe('getting a view bundle', () => { + it('validates id parameter', async () => { + await ExtensionsController.getBundleForView({ params: { id: '' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + it('validates view parameter', async () => { + await ExtensionsController.getBundleForView({ params: { id: 'some-id', view: '' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + it('returns 404 if extension not found', async () => { + (ExtensionManager.find as jest.Mock).mockReturnValue(null); + await ExtensionsController.getBundleForView({ params: { id: 'does-not-exist', view: 'some-id' } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + describe('when extension found', () => { + const id = 'extension-id'; + const viewId = 'view-id'; + const bundleId = 'bundle-id'; + + beforeEach(() => { + (ExtensionManager.find as jest.Mock).mockReturnValue({ + contributes: { + views: { + [viewId]: { + bundleId, + }, + }, + }, + }); + }); + + it('returns a 404 if bundle not found', async () => { + (ExtensionManager.getBundle as jest.Mock).mockReturnValue(null); + await ExtensionsController.getBundleForView({ params: { id, view: viewId } } as Request, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: expect.any(String) }); + }); + + it('sends the javascript bundle', async () => { + (ExtensionManager.getBundle as jest.Mock).mockReturnValue('js bundle path'); + await ExtensionsController.getBundleForView({ params: { id, view: viewId } } as Request, res); + + expect(res.sendFile).toHaveBeenCalledWith('js bundle path'); + }); + }); +}); + +describe('proxying extension requests', () => { + it.todo('proxies requests from extensions'); +}); diff --git a/Composer/packages/server/__tests__/controllers/project.test.ts b/Composer/packages/server/src/controllers/__tests__/project.test.ts similarity index 91% rename from Composer/packages/server/__tests__/controllers/project.test.ts rename to Composer/packages/server/src/controllers/__tests__/project.test.ts index 5ed15daf4c..78d7fc3675 100644 --- a/Composer/packages/server/__tests__/controllers/project.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/project.test.ts @@ -3,35 +3,33 @@ import { Request, Response } from 'express'; import rimraf from 'rimraf'; -import { pluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; -import { BotProjectService } from '../../src/services/project'; -import { ProjectController } from '../../src/controllers/project'; -import { Path } from '../../src/utility/path'; +import { BotProjectService } from '../../services/project'; +import { ProjectController } from '../../controllers/project'; +import { Path } from '../../utility/path'; jest.mock('@bfc/extension', () => { return { - pluginLoader: { + ExtensionContext: { extensions: { botTemplates: [], baseTemplates: [], publish: [], }, - }, - PluginLoader: { getUserFromRequest: jest.fn(), }, }; }); -const mockSampleBotPath = Path.join(__dirname, '../mocks/asset/projects/SampleBot'); +const mockSampleBotPath = Path.join(__dirname, '../../__mocks__/asset/projects/SampleBot'); let mockRes: Response; -const newBot = Path.resolve(__dirname, '../mocks/samplebots/newBot'); -const saveAsDir = Path.resolve(__dirname, '../mocks/samplebots/saveAsBot'); -const useFortest = Path.resolve(__dirname, '../mocks/samplebots/test'); -const bot1 = Path.resolve(__dirname, '../mocks/samplebots/bot1'); +const newBot = Path.resolve(__dirname, '../../__mocks__/samplebots/newBot'); +const saveAsDir = Path.resolve(__dirname, '../../__mocks__/samplebots/saveAsBot'); +const useFortest = Path.resolve(__dirname, '../../__mocks__/samplebots/test'); +const bot1 = Path.resolve(__dirname, '../../__mocks__/samplebots/bot1'); const location1 = { storageId: 'default', @@ -52,7 +50,7 @@ beforeEach(() => { }); beforeAll(async () => { - pluginLoader.extensions.botTemplates.push({ + ExtensionContext.extensions.botTemplates.push({ id: 'SampleBot', name: 'Sample Bot', description: 'Sample Bot', @@ -92,7 +90,7 @@ describe('get bot project', () => { const mockReq = { params: {}, query: {}, - body: { storageId: 'default', path: Path.resolve(__dirname, '../mocks/samplebots/bot1') }, + body: { storageId: 'default', path: Path.resolve(__dirname, '../../__mocks__/samplebots/bot1') }, } as Request; await ProjectController.openProject(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -104,7 +102,7 @@ describe('get all projects', () => { const mockReq = { params: {}, query: {}, - body: { storageId: 'default', path: Path.resolve(__dirname, '../mocks/samplebots') }, + body: { storageId: 'default', path: Path.resolve(__dirname, '../../__mocks__/samplebots') }, } as Request; await ProjectController.getAllProjects(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -129,7 +127,7 @@ describe('open bot operation', () => { const mockReq = { params: {}, query: {}, - body: { storageId: 'default', path: Path.resolve(__dirname, '../mocks/samplebots/bot1') }, + body: { storageId: 'default', path: Path.resolve(__dirname, '../../__mocks__/samplebots/bot1') }, } as Request; await ProjectController.openProject(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); @@ -137,7 +135,7 @@ describe('open bot operation', () => { }); describe('should save as bot', () => { - const saveAsDir = Path.resolve(__dirname, '../mocks/samplebots/'); + const saveAsDir = Path.resolve(__dirname, '../../__mocks__/samplebots/'); it('saveProjectAs', async () => { const projectId = await BotProjectService.openProject(location1); const schemaUrl = 'http://json-schema.org/draft-07/schema#'; @@ -166,7 +164,7 @@ describe('should get recent projects', () => { describe('create a Empty Bot project', () => { it('should create a new project', async () => { - const newBotDir = Path.resolve(__dirname, '../mocks/samplebots/'); + const newBotDir = Path.resolve(__dirname, '../../__mocks__/samplebots/'); const name = 'newBot'; const mockReq = { params: {}, diff --git a/Composer/packages/server/__tests__/controllers/publisher.test.ts b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts similarity index 77% rename from Composer/packages/server/__tests__/controllers/publisher.test.ts rename to Composer/packages/server/src/controllers/__tests__/publisher.test.ts index 33d7c44329..9fcfc8ccb4 100644 --- a/Composer/packages/server/__tests__/controllers/publisher.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts @@ -5,18 +5,18 @@ import path from 'path'; import { Request, Response } from 'express'; import rimraf from 'rimraf'; -import { pluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; -import { BotProjectService } from '../../src/services/project'; -import { Path } from '../../src/utility/path'; -import { PublishController } from '../../src/controllers/publisher'; +import { BotProjectService } from '../../services/project'; +import { Path } from '../../utility/path'; +import { PublishController } from '../../controllers/publisher'; -const pluginDir = path.resolve(__dirname, '../../../../plugins'); +const pluginDir = path.resolve(__dirname, '../../../../../plugins'); let mockRes: Response; -const useFortest = Path.resolve(__dirname, '../mocks/samplebots/testPublish'); -const bot1 = Path.resolve(__dirname, '../mocks/samplebots/bot1'); +const useFortest = Path.resolve(__dirname, '../../__mocks__/samplebots/testPublish'); +const bot1 = Path.resolve(__dirname, '../../__mocks__/samplebots/bot1'); const location1 = { storageId: 'default', @@ -28,22 +28,23 @@ const location2 = { path: useFortest, }; -beforeEach(() => { +beforeEach(async () => { mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis(), } as any; -}); -beforeAll(async () => { const currentProjectId = await BotProjectService.openProject(location1); const currentProject = await BotProjectService.getProjectById(currentProjectId); await BotProjectService.saveProjectAs(currentProject, location2); - await pluginLoader.loadPluginsFromFolder(pluginDir); }); -afterAll(() => { +beforeAll(async () => { + await ExtensionContext.loadPluginsFromFolder(pluginDir); +}); + +afterEach(() => { // remove the new bot files try { rimraf.sync(useFortest); @@ -65,13 +66,11 @@ describe('get types', () => { }); describe('status', () => { - let projectId = ''; const target = 'default'; - beforeEach(async () => { - projectId = await BotProjectService.openProject(location2); - }); - it('should get status', async () => { + it.only('should get status', async () => { + const projectId = await BotProjectService.openProject(location2); + const mockReq = { params: { projectId, target }, query: {}, diff --git a/Composer/packages/server/__tests__/controllers/storage.test.ts b/Composer/packages/server/src/controllers/__tests__/storage.test.ts similarity index 89% rename from Composer/packages/server/__tests__/controllers/storage.test.ts rename to Composer/packages/server/src/controllers/__tests__/storage.test.ts index 3fe4172f49..61e243c244 100644 --- a/Composer/packages/server/__tests__/controllers/storage.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/storage.test.ts @@ -3,10 +3,10 @@ import { Request, Response } from 'express'; -import StorageService from '../../src/services/storage'; -import { StorageController } from '../../src/controllers/storage'; +import StorageService from '../../services/storage'; +import { StorageController } from '../../controllers/storage'; -jest.mock('../../src/services/storage', () => ({ +jest.mock('../../services/storage', () => ({ getBlob: jest.fn(), })); diff --git a/Composer/packages/server/src/controllers/eject.ts b/Composer/packages/server/src/controllers/eject.ts index ca997a23dc..0098c2d467 100644 --- a/Composer/packages/server/src/controllers/eject.ts +++ b/Composer/packages/server/src/controllers/eject.ts @@ -1,21 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { pluginLoader, PluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; import { BotProjectService } from '../services/project'; import { LocalDiskStorage } from '../models/storage/localDiskStorage'; export const EjectController = { getTemplates: async (req, res) => { - res.json(pluginLoader.extensions.runtimeTemplates); + res.json(ExtensionContext.extensions.runtimeTemplates); }, eject: async (req, res) => { - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); - const template = pluginLoader.extensions.runtimeTemplates.find((i) => i.key === req.params.template); + const template = ExtensionContext.extensions.runtimeTemplates.find((i) => i.key === req.params.template); if (template) { let runtimePath; try { diff --git a/Composer/packages/server/src/controllers/extensions.ts b/Composer/packages/server/src/controllers/extensions.ts index 833ed092ee..3ab9d85644 100644 --- a/Composer/packages/server/src/controllers/extensions.ts +++ b/Composer/packages/server/src/controllers/extensions.ts @@ -6,7 +6,7 @@ import { ExtensionManager } from '@bfc/extension'; interface AddExtensionRequest extends Request { body: { - name?: string; + id?: string; version?: string; }; } @@ -49,16 +49,16 @@ export async function listExtensions(req: Request, res: Response) { } export async function addExtension(req: AddExtensionRequest, res: Response) { - const { name, version } = req.body; + const { id, version } = req.body; - if (!name) { - res.status(400).send({ error: '`name` is missing from body' }); + if (!id) { + res.status(400).json({ error: '`id` is missing from body' }); return; } - await ExtensionManager.installRemote(name, version); - await ExtensionManager.load(name); - res.json(ExtensionManager.find(name)); + await ExtensionManager.installRemote(id, version); + await ExtensionManager.load(id); + res.json(ExtensionManager.find(id)); } export async function toggleExtension(req: ToggleExtensionRequest, res: Response) { @@ -87,7 +87,7 @@ export async function removeExtension(req: RemoveExtensionRequest, res: Response const { id } = req.body; if (!id) { - res.status(400).send({ error: '`id` is missing from body' }); + res.status(400).json({ error: '`id` is missing from body' }); return; } @@ -109,14 +109,29 @@ export async function searchExtensions(req: SearchExtensionsRequest, res: Respon export async function getBundleForView(req: ExtensionViewBundleRequest, res: Response) { const { id, view } = req.params; + + if (!id) { + res.status(400).json({ error: '`id` is missing from body' }); + return; + } + + if (!view) { + res.status(400).json({ error: '`view` is missing from body' }); + return; + } + const extension = ExtensionManager.find(id); - const bundleId = extension.contributes.views?.[view].bundleId as string; - const bundle = ExtensionManager.getBundle(id, bundleId); - if (bundle) { - res.sendFile(bundle); - } else { - res.status(404); + + if (extension) { + const bundleId = extension.contributes.views?.[view].bundleId as string; + const bundle = ExtensionManager.getBundle(id, bundleId); + if (bundle) { + res.sendFile(bundle); + return; + } } + + res.status(404).json({ error: 'extension or bundle not found' }); } export async function performExtensionFetch(req: ExtensionFetchRequest, res: Response) { diff --git a/Composer/packages/server/src/controllers/formDialog.ts b/Composer/packages/server/src/controllers/formDialog.ts index 19c8f4e38e..2cf3fc987e 100644 --- a/Composer/packages/server/src/controllers/formDialog.ts +++ b/Composer/packages/server/src/controllers/formDialog.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Request, Response } from 'express'; -import { PluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; import { schemas, expandPropertyDefinition } from '@microsoft/bf-generate-library'; import { BotProjectService } from '../services/project'; @@ -34,7 +34,7 @@ const getTemplateSchemas = async (req: Request, res: Response) => { const generate = async (req: Request, res: Response) => { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 61956e4727..c609ec934e 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import { Request, Response } from 'express'; import { Archiver } from 'archiver'; -import { PluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; import log from '../logger'; import { BotProjectService } from '../services/project'; @@ -20,7 +20,7 @@ import { Path } from './../utility/path'; async function createProject(req: Request, res: Response) { let { templateId } = req.body; const { name, description, storageId, location, schemaUrl, locale } = req.body; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); if (templateId === '') { templateId = 'EmptyBot'; } @@ -76,7 +76,7 @@ async function createProject(req: Request, res: Response) { async function getProjectById(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); try { const currentProject = await BotProjectService.getProjectById(projectId, user); @@ -129,7 +129,7 @@ async function openProject(req: Request, res: Response) { return; } - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const location: LocationRef = { storageId: req.body.storageId, @@ -167,7 +167,7 @@ async function saveProjectAs(req: Request, res: Response) { } const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const originalProject = await BotProjectService.getProjectById(projectId, user); const { name, description, location, storageId } = req.body; @@ -201,7 +201,7 @@ async function saveProjectAs(req: Request, res: Response) { } async function getRecentProjects(req: Request, res: Response) { - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const projects = await BotProjectService.getRecentBotProjects(user); return res.status(200).json(projects); @@ -209,7 +209,7 @@ async function getRecentProjects(req: Request, res: Response) { async function updateFile(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { const lastModified = await currentProject.updateFile(req.body.name, req.body.content); @@ -223,7 +223,7 @@ async function updateFile(req: Request, res: Response) { async function createFile(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { @@ -241,7 +241,7 @@ async function createFile(req: Request, res: Response) { async function removeFile(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { @@ -254,7 +254,7 @@ async function removeFile(req: Request, res: Response) { async function getSkill(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { @@ -288,7 +288,7 @@ async function exportProject(req: Request, res: Response) { async function setQnASettings(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { @@ -309,7 +309,7 @@ async function setQnASettings(req: Request, res: Response) { async function build(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { @@ -338,7 +338,7 @@ async function build(req: Request, res: Response) { async function getAllProjects(req: Request, res: Response) { const storageId = 'default'; const folderPath = Path.resolve(settings.botsFolder); - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); try { res.status(200).json(await StorageService.getBlob(storageId, folderPath, user)); @@ -351,7 +351,7 @@ async function getAllProjects(req: Request, res: Response) { async function checkBoilerplateVersion(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { @@ -375,7 +375,7 @@ async function checkBoilerplateVersion(req: Request, res: Response) { async function updateBoilerplate(req: Request, res: Response) { const projectId = req.params.projectId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const currentProject = await BotProjectService.getProjectById(projectId, user); diff --git a/Composer/packages/server/src/controllers/publisher.ts b/Composer/packages/server/src/controllers/publisher.ts index 6d788483c2..fb5e03d566 100644 --- a/Composer/packages/server/src/controllers/publisher.ts +++ b/Composer/packages/server/src/controllers/publisher.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import merge from 'lodash/merge'; -import { pluginLoader, PluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; import { defaultPublishConfig } from '@bfc/shared'; import { BotProjectService } from '../services/project'; @@ -10,7 +10,7 @@ import { BotProjectService } from '../services/project'; export const PublishController = { getTypes: async (req, res) => { res.json( - Object.values(pluginLoader.extensions.publish) + Object.values(ExtensionContext.extensions.publish) .filter((extension) => extension.plugin.name !== defaultPublishConfig.type) .map((extension) => { const { plugin, methods } = extension; @@ -33,7 +33,7 @@ export const PublishController = { }, publish: async (req, res) => { const target = req.params.target; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const { metadata, sensitiveSettings } = req.body; const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); @@ -46,7 +46,7 @@ export const PublishController = { const profile = profiles.length ? profiles[0] : undefined; const method = profile ? profile.type : undefined; // get the publish plugin key - if (profile && method && pluginLoader?.extensions?.publish[method]?.methods?.publish) { + if (profile && method && ExtensionContext?.extensions?.publish[method]?.methods?.publish) { // append config from client(like sensitive settings) const configuration = { profileName: profile.name, @@ -55,7 +55,7 @@ export const PublishController = { }; // get the externally defined method - const pluginMethod = pluginLoader.extensions.publish[method].methods.publish; + const pluginMethod = ExtensionContext.extensions.publish[method].methods.publish; try { // call the method @@ -84,7 +84,7 @@ export const PublishController = { }, status: async (req, res) => { const target = req.params.target; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); @@ -95,15 +95,9 @@ export const PublishController = { const profile = profiles.length ? profiles[0] : undefined; // get the publish plugin key const method = profile ? profile.type : undefined; - if ( - profile && - method && - pluginLoader.extensions.publish[method] && - pluginLoader.extensions.publish[method].methods && - pluginLoader.extensions.publish[method].methods.getStatus - ) { + if (profile && method && ExtensionContext.extensions.publish[method]?.methods?.getStatus) { // get the externally defined method - const pluginMethod = pluginLoader.extensions.publish[method].methods.getStatus; + const pluginMethod = ExtensionContext.extensions.publish[method].methods.getStatus; if (typeof pluginMethod === 'function') { const configuration = { @@ -131,7 +125,7 @@ export const PublishController = { }, history: async (req, res) => { const target = req.params.target; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); @@ -143,15 +137,9 @@ export const PublishController = { // get the publish plugin key const method = profile ? profile.type : undefined; - if ( - profile && - method && - pluginLoader.extensions.publish[method] && - pluginLoader.extensions.publish[method].methods && - pluginLoader.extensions.publish[method].methods.history - ) { + if (profile && method && ExtensionContext.extensions.publish[method]?.methods?.history) { // get the externally defined method - const pluginMethod = pluginLoader.extensions.publish[method].methods.history; + const pluginMethod = ExtensionContext.extensions.publish[method].methods.history; if (typeof pluginMethod === 'function') { const configuration = { profileName: profile.name, @@ -173,7 +161,7 @@ export const PublishController = { }, rollback: async (req, res) => { const target = req.params.target; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); const { version, sensitiveSettings } = req.body; const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); @@ -187,13 +175,7 @@ export const PublishController = { // get the publish plugin key const method = profile ? profile.type : undefined; - if ( - profile && - method && - pluginLoader.extensions.publish[method] && - pluginLoader.extensions.publish[method].methods && - pluginLoader.extensions.publish[method].methods.rollback - ) { + if (profile && method && ExtensionContext.extensions.publish[method]?.methods?.rollback) { // append config from client(like sensitive settings) const configuration = { profileName: profile.name, @@ -201,7 +183,7 @@ export const PublishController = { ...JSON.parse(profile.configuration), }; // get the externally defined method - const pluginMethod = pluginLoader.extensions.publish[method].methods.rollback; + const pluginMethod = ExtensionContext.extensions.publish[method].methods.rollback; if (typeof pluginMethod === 'function') { try { // call the method @@ -233,14 +215,8 @@ export const PublishController = { const projectId = req.params.projectId; const profile = defaultPublishConfig; const method = profile.type; - if ( - profile && - method && - pluginLoader.extensions.publish[method] && - pluginLoader.extensions.publish[method].methods && - pluginLoader.extensions.publish[method].methods.stopBot - ) { - const pluginMethod = pluginLoader.extensions.publish[method].methods.stopBot; + if (profile && method && ExtensionContext.extensions.publish[method]?.methods?.stopBot) { + const pluginMethod = ExtensionContext.extensions.publish[method].methods.stopBot; if (typeof pluginMethod === 'function') { try { await pluginMethod.call(null, projectId); @@ -252,13 +228,8 @@ export const PublishController = { } } } - if ( - profile && - pluginLoader.extensions.publish[method] && - pluginLoader.extensions.publish[method].methods && - pluginLoader.extensions.publish[method].methods.removeRuntimeData - ) { - const pluginMethod = pluginLoader.extensions.publish[method].methods.removeRuntimeData; + if (profile && ExtensionContext.extensions.publish[method]?.methods?.removeRuntimeData) { + const pluginMethod = ExtensionContext.extensions.publish[method].methods.removeRuntimeData; if (typeof pluginMethod === 'function') { try { const result = await pluginMethod.call(null, projectId); @@ -281,14 +252,8 @@ export const PublishController = { const projectId = req.params.projectId; const profile = defaultPublishConfig; const method = profile.type; - if ( - profile && - method && - pluginLoader.extensions.publish[method] && - pluginLoader.extensions.publish[method].methods && - pluginLoader.extensions.publish[method].methods.stopBot - ) { - const pluginMethod = pluginLoader.extensions.publish[method].methods.stopBot; + if (profile && method && ExtensionContext.extensions.publish[method]?.methods?.stopBot) { + const pluginMethod = ExtensionContext.extensions.publish[method].methods.stopBot; if (typeof pluginMethod === 'function') { try { await pluginMethod.call(null, projectId); diff --git a/Composer/packages/server/src/controllers/storage.ts b/Composer/packages/server/src/controllers/storage.ts index a404c983d4..ac2dece498 100644 --- a/Composer/packages/server/src/controllers/storage.ts +++ b/Composer/packages/server/src/controllers/storage.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Request, Response } from 'express'; -import { PluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; import StorageService from '../services/storage'; import { Path } from '../utility/path'; @@ -45,7 +45,7 @@ async function updateFolder(req: Request, res: Response) { async function getBlob(req: Request, res: Response) { const storageId = req.params.storageId; - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); try { if (!req.query.path) { diff --git a/Composer/packages/server/__tests__/models/asset/assetManager.test.ts b/Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts similarity index 76% rename from Composer/packages/server/__tests__/models/asset/assetManager.test.ts rename to Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts index bd87f44055..a29937baf3 100644 --- a/Composer/packages/server/__tests__/models/asset/assetManager.test.ts +++ b/Composer/packages/server/src/models/asset/__tests__/assetManager.test.ts @@ -2,10 +2,10 @@ // Licensed under the MIT License. import rimraf from 'rimraf'; -import { pluginLoader } from '@bfc/extension'; +import { ExtensionContext } from '@bfc/extension'; -import { Path } from '../../../src/utility/path'; -import { AssetManager } from '../../../src/models/asset/assetManager'; +import { Path } from '../../../utility/path'; +import { AssetManager } from '../assetManager'; jest.mock('azure-storage', () => { return {}; }); @@ -13,7 +13,7 @@ jest.mock('azure-storage', () => { jest.mock('@bfc/extension', () => { //const p = require('path'); return { - pluginLoader: { + ExtensionContext: { extensions: { botTemplates: [], }, @@ -21,15 +21,15 @@ jest.mock('@bfc/extension', () => { }; }); -const mockSampleBotPath = Path.join(__dirname, '../../mocks/asset/projects/SampleBot'); -const mockCopyToPath = Path.join(__dirname, '../../mocks/new'); +const mockSampleBotPath = Path.join(__dirname, '../../../__mocks__/asset/projects/SampleBot'); +const mockCopyToPath = Path.join(__dirname, '../../../__mocks__/new'); const locationRef = { storageId: 'default', path: mockCopyToPath, }; beforeAll(() => { - pluginLoader.extensions.botTemplates.push({ + ExtensionContext.extensions.botTemplates.push({ id: 'SampleBot', name: 'Sample Bot', description: 'Sample Bot', diff --git a/Composer/packages/server/src/models/asset/assetManager.ts b/Composer/packages/server/src/models/asset/assetManager.ts index 36eb0b0b95..0df9b9ca04 100644 --- a/Composer/packages/server/src/models/asset/assetManager.ts +++ b/Composer/packages/server/src/models/asset/assetManager.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import path from 'path'; import find from 'lodash/find'; -import { UserIdentity, pluginLoader } from '@bfc/extension'; +import { UserIdentity, ExtensionContext } from '@bfc/extension'; import log from '../../logger'; import { LocalDiskStorage } from '../storage/localDiskStorage'; @@ -24,7 +24,7 @@ export class AssetManager { } public async getProjectTemplates() { - return pluginLoader.extensions.botTemplates; + return ExtensionContext.extensions.botTemplates; } public async copyProjectTemplateTo( @@ -45,7 +45,7 @@ export class AssetManager { } private async copyDataFilesTo(templateId: string, dstDir: string, dstStorage: IFileStorage, locale?: string) { - const template = find(pluginLoader.extensions.botTemplates, { id: templateId }); + const template = find(ExtensionContext.extensions.botTemplates, { id: templateId }); if (template === undefined || template.path === undefined) { throw new Error(`no such template with id ${templateId}`); } @@ -66,7 +66,7 @@ export class AssetManager { // Copy material from the boilerplate into the project // This is used to copy shared content into every new project public async copyBoilerplate(dstDir: string, dstStorage: IFileStorage) { - for (const boilerplate of pluginLoader.extensions.baseTemplates) { + for (const boilerplate of ExtensionContext.extensions.baseTemplates) { const boilerplatePath = boilerplate.path; if (await this.templateStorage.exists(boilerplatePath)) { await copyDir(boilerplatePath, this.templateStorage, dstDir, dstStorage); @@ -97,10 +97,10 @@ export class AssetManager { // return the current version of the boilerplate content, if one exists so specified // this is based off of the first boilerplate template added to the app. public getBoilerplateCurrentVersion(): string | undefined { - if (!pluginLoader.extensions.baseTemplates.length) { + if (!ExtensionContext.extensions.baseTemplates.length) { return undefined; } - const boilerplate = pluginLoader.extensions.baseTemplates[0]; + const boilerplate = ExtensionContext.extensions.baseTemplates[0]; const location = Path.join(boilerplate.path, 'scripts', 'package.json'); try { if (fs.existsSync(location)) { diff --git a/Composer/packages/server/__tests__/models/bot/botProject.test.ts b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts similarity index 97% rename from Composer/packages/server/__tests__/models/bot/botProject.test.ts rename to Composer/packages/server/src/models/bot/__tests__/botProject.test.ts index 56ef84919d..6dcf62c385 100644 --- a/Composer/packages/server/__tests__/models/bot/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -6,15 +6,15 @@ import fs from 'fs'; import rimraf from 'rimraf'; import { DialogFactory, SDKKinds } from '@bfc/shared'; -import { Path } from '../../../src/utility/path'; -import { BotProject } from '../../../src/models/bot/botProject'; -import { LocationRef } from '../../../src/models/bot/interface'; +import { Path } from '../../../utility/path'; +import { BotProject } from '../botProject'; +import { LocationRef } from '../interface'; jest.mock('azure-storage', () => { return {}; }); -const botDir = '../../mocks/samplebots/bot1'; +const botDir = '../../../__mocks__/samplebots/bot1'; const mockLocationRef: LocationRef = { storageId: 'default', diff --git a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts b/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts similarity index 98% rename from Composer/packages/server/__tests__/models/bot/botStructure.test.ts rename to Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts index 7e56c70ad7..afa9a14dae 100644 --- a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { defaultFilePath, parseFileName } from '../../../src/models/bot/botStructure'; +import { defaultFilePath, parseFileName } from '../../../models/bot/botStructure'; const botName = 'Mybot'; const defaultLocale = 'en-us'; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index abc5938af2..2dacd7b9c4 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -8,7 +8,7 @@ import axios from 'axios'; import { autofixReferInDialog } from '@bfc/indexers'; import { getNewDesigner, FileInfo, Skill, Diagnostic, IBotProject, DialogSetting, FileExtensions } from '@bfc/shared'; import merge from 'lodash/merge'; -import { UserIdentity, pluginLoader } from '@bfc/extension'; +import { UserIdentity, ExtensionContext } from '@bfc/extension'; import { FeedbackType, generate } from '@microsoft/bf-generate-library'; import { Path } from '../../utility/path'; @@ -548,14 +548,14 @@ export class BotProject implements IBotProject { private async removeLocalRuntimeData(projectId) { const method = 'localpublish'; - if (pluginLoader.extensions.publish[method]?.methods?.stopBot) { - const pluginMethod = pluginLoader.extensions.publish[method].methods.stopBot; + if (ExtensionContext.extensions.publish[method]?.methods?.stopBot) { + const pluginMethod = ExtensionContext.extensions.publish[method].methods.stopBot; if (typeof pluginMethod === 'function') { await pluginMethod.call(null, projectId); } } - if (pluginLoader.extensions.publish[method]?.methods?.removeRuntimeData) { - const pluginMethod = pluginLoader.extensions.publish[method].methods.removeRuntimeData; + if (ExtensionContext.extensions.publish[method]?.methods?.removeRuntimeData) { + const pluginMethod = ExtensionContext.extensions.publish[method].methods.removeRuntimeData; if (typeof pluginMethod === 'function') { await pluginMethod.call(null, projectId); } diff --git a/Composer/packages/server/__tests__/models/bot/sampler/BootstrapSampler.test.ts b/Composer/packages/server/src/models/bot/sampler/__tests__/BootstrapSampler.test.ts similarity index 91% rename from Composer/packages/server/__tests__/models/bot/sampler/BootstrapSampler.test.ts rename to Composer/packages/server/src/models/bot/sampler/__tests__/BootstrapSampler.test.ts index c0d26af350..94d0577fbb 100644 --- a/Composer/packages/server/__tests__/models/bot/sampler/BootstrapSampler.test.ts +++ b/Composer/packages/server/src/models/bot/sampler/__tests__/BootstrapSampler.test.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ComposerBootstrapSampler } from './../../../../src/models/bot/sampler/BootstrapSampler'; +import { ComposerBootstrapSampler } from '../BootstrapSampler'; describe('BootstrapSampler', () => { it('balence the utterances ratio in intents after bootstrap sampling', async () => { diff --git a/Composer/packages/server/__tests__/models/bot/sampler/ReservoirSampler.test.ts b/Composer/packages/server/src/models/bot/sampler/__tests__/ReservoirSampler.test.ts similarity index 93% rename from Composer/packages/server/__tests__/models/bot/sampler/ReservoirSampler.test.ts rename to Composer/packages/server/src/models/bot/sampler/__tests__/ReservoirSampler.test.ts index e6e8269119..072b0c4b9f 100644 --- a/Composer/packages/server/__tests__/models/bot/sampler/ReservoirSampler.test.ts +++ b/Composer/packages/server/src/models/bot/sampler/__tests__/ReservoirSampler.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ComposerReservoirSampler } from './../../../../src/models/bot/sampler/ReservoirSampler'; +import { ComposerReservoirSampler } from './../../../../models/bot/sampler/ReservoirSampler'; describe('BootstrapSampler', () => { it('down size the number of utterances reservoir sampling', async () => { diff --git a/Composer/packages/server/__tests__/models/settings/fileSettingManager.test.ts b/Composer/packages/server/src/models/settings/__tests__/fileSettingManager.test.ts similarity index 85% rename from Composer/packages/server/__tests__/models/settings/fileSettingManager.test.ts rename to Composer/packages/server/src/models/settings/__tests__/fileSettingManager.test.ts index 1aa933d10c..67d78c9289 100644 --- a/Composer/packages/server/__tests__/models/settings/fileSettingManager.test.ts +++ b/Composer/packages/server/src/models/settings/__tests__/fileSettingManager.test.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileSettingManager } from '../../../src/models/settings/fileSettingManager'; -import { Path } from '../../../src/utility/path'; +import { FileSettingManager } from '../../../models/settings/fileSettingManager'; +import { Path } from '../../../utility/path'; const dir = './mocks'; const defaultDir = Path.join(__dirname, dir); diff --git a/Composer/packages/server/__tests__/models/settings/mocks/bonus/settings/appsettings.json b/Composer/packages/server/src/models/settings/__tests__/mocks/bonus/settings/appsettings.json similarity index 100% rename from Composer/packages/server/__tests__/models/settings/mocks/bonus/settings/appsettings.json rename to Composer/packages/server/src/models/settings/__tests__/mocks/bonus/settings/appsettings.json diff --git a/Composer/packages/server/__tests__/models/settings/mocks/integration/settings/appsettings.json b/Composer/packages/server/src/models/settings/__tests__/mocks/integration/settings/appsettings.json similarity index 100% rename from Composer/packages/server/__tests__/models/settings/mocks/integration/settings/appsettings.json rename to Composer/packages/server/src/models/settings/__tests__/mocks/integration/settings/appsettings.json diff --git a/Composer/packages/server/__tests__/models/settings/mocks/production/settings/appsettings.json b/Composer/packages/server/src/models/settings/__tests__/mocks/production/settings/appsettings.json similarity index 100% rename from Composer/packages/server/__tests__/models/settings/mocks/production/settings/appsettings.json rename to Composer/packages/server/src/models/settings/__tests__/mocks/production/settings/appsettings.json diff --git a/Composer/packages/server/__tests__/models/settings/mocks/settings/appsettings.json b/Composer/packages/server/src/models/settings/__tests__/mocks/settings/appsettings.json similarity index 100% rename from Composer/packages/server/__tests__/models/settings/mocks/settings/appsettings.json rename to Composer/packages/server/src/models/settings/__tests__/mocks/settings/appsettings.json diff --git a/Composer/packages/server/src/models/storage/storageFactory.ts b/Composer/packages/server/src/models/storage/storageFactory.ts index 8c2a32c055..cabb6ab2e3 100644 --- a/Composer/packages/server/src/models/storage/storageFactory.ts +++ b/Composer/packages/server/src/models/storage/storageFactory.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { pluginLoader, UserIdentity } from '@bfc/extension'; +import { ExtensionContext, UserIdentity } from '@bfc/extension'; import { LocalDiskStorage } from './localDiskStorage'; import { StorageConnection, IFileStorage } from './interface'; export class StorageFactory { public static createStorageClient(conn: StorageConnection, user?: UserIdentity): IFileStorage { - if (pluginLoader.extensions.storage && pluginLoader.extensions.storage.customStorageClass) { - const customStorageClass = pluginLoader.extensions.storage.customStorageClass; + if (ExtensionContext.extensions.storage && ExtensionContext.extensions.storage.customStorageClass) { + const customStorageClass = ExtensionContext.extensions.storage.customStorageClass; if (customStorageClass) { return new customStorageClass(conn, user) as IFileStorage; } diff --git a/Composer/packages/server/src/server.ts b/Composer/packages/server/src/server.ts index 25f79a702a..2d004377de 100644 --- a/Composer/packages/server/src/server.ts +++ b/Composer/packages/server/src/server.ts @@ -17,7 +17,7 @@ import { IntellisenseServer } from '@bfc/intellisense-languageserver'; import { LGServer } from '@bfc/lg-languageserver'; import { LUServer } from '@bfc/lu-languageserver'; import chalk from 'chalk'; -import { pluginLoader, ExtensionManager } from '@bfc/extension'; +import { ExtensionContext, ExtensionManager } from '@bfc/extension'; import { BotProjectService } from './services/project'; import { getAuthProvider } from './router/auth'; @@ -40,18 +40,17 @@ export async function start(): Promise { app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ extended: false })); app.use(session({ secret: 'bot-framework-composer' })); - app.use(pluginLoader.passport.initialize()); - app.use(pluginLoader.passport.session()); + app.use(ExtensionContext.passport.initialize()); + app.use(ExtensionContext.passport.session()); // make sure plugin has access to our express... - pluginLoader.useExpress(app); + ExtensionContext.useExpress(app); // load all installed plugins setEnvDefault('COMPOSER_EXTENSION_DATA', path.resolve(__dirname, '../extensions.json')); - setEnvDefault('COMPOSER_BUILTIN_PLUGINS_DIR', path.resolve(__dirname, '../../../plugins')); - setEnvDefault('COMPOSER_REMOTE_PLUGINS_DIR', path.resolve(__dirname, '../../../.composer')); - await ExtensionManager.loadBuiltinPlugins(); - // TODO (toanzian / abrown): load 3P plugins + setEnvDefault('COMPOSER_BUILTIN_EXTENSIONS_DIR', path.resolve(__dirname, '../../../plugins')); + setEnvDefault('COMPOSER_REMOTE_EXTENSIONS_DIR', path.resolve(__dirname, '../../../.composer')); + await ExtensionManager.loadAll(); const { login, authorize } = getAuthProvider(); diff --git a/Composer/packages/server/__tests__/services/project.test.ts b/Composer/packages/server/src/services/__tests__/project.test.ts similarity index 85% rename from Composer/packages/server/__tests__/services/project.test.ts rename to Composer/packages/server/src/services/__tests__/project.test.ts index 088212e577..eab70305bc 100644 --- a/Composer/packages/server/__tests__/services/project.test.ts +++ b/Composer/packages/server/src/services/__tests__/project.test.ts @@ -3,11 +3,11 @@ import rimraf from 'rimraf'; -import { Path } from '../../src/utility/path'; -import { BotProjectService } from '../../src/services/project'; +import { Path } from '../../utility/path'; +import { BotProjectService } from '../project'; // offer a bot project ref which to open -jest.mock('../../src/store/store', () => { +jest.mock('../../store/store', () => { const data = { storageConnections: [ { @@ -35,9 +35,9 @@ jest.mock('../../src/store/store', () => { jest.mock('azure-storage', () => {}); -const projPath = Path.resolve(__dirname, '../mocks/samplebots/bot1'); +const projPath = Path.resolve(__dirname, '../../__mocks__/samplebots/bot1'); -const saveAsDir = Path.resolve(__dirname, '../mocks/samplebots/saveas'); +const saveAsDir = Path.resolve(__dirname, '../../__mocks__/samplebots/saveas'); const location1 = { storageId: 'default', diff --git a/Composer/packages/server/__tests__/services/storage.test.ts b/Composer/packages/server/src/services/__tests__/storage.test.ts similarity index 90% rename from Composer/packages/server/__tests__/services/storage.test.ts rename to Composer/packages/server/src/services/__tests__/storage.test.ts index b93b445482..e54739f4ab 100644 --- a/Composer/packages/server/__tests__/services/storage.test.ts +++ b/Composer/packages/server/src/services/__tests__/storage.test.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Path } from '../../src/utility/path'; -import StorageService from '../../src/services/storage'; +import { Path } from '../../utility/path'; +import StorageService from '../../services/storage'; jest.mock('azure-storage', () => { return { createBlobService: (_account: string, _key: string) => { @@ -17,7 +17,7 @@ jest.mock('azure-storage', () => { }, }; }); -jest.mock('../../src/store/store', () => { +jest.mock('../../store/store', () => { const data = [ { id: 'default', diff --git a/Composer/packages/server/tsconfig.build.json b/Composer/packages/server/tsconfig.build.json index 5c27bc8c34..3d2578d39e 100644 --- a/Composer/packages/server/tsconfig.build.json +++ b/Composer/packages/server/tsconfig.build.json @@ -2,7 +2,7 @@ /* Options used for building production code (tests excluded) */ "extends": "./tsconfig.json", "exclude": [ - "__tests__", + "**/__tests__/**", "**/__mocks__/**" ] } diff --git a/Composer/packages/server/tsconfig.json b/Composer/packages/server/tsconfig.json index 991f443d52..aea331b403 100644 --- a/Composer/packages/server/tsconfig.json +++ b/Composer/packages/server/tsconfig.json @@ -5,5 +5,5 @@ "sourceMap": true, "target": "es6" }, - "include": ["src/**/*.ts", "__tests__/**/*"] + "include": ["src/**/*.ts"] } diff --git a/Composer/packages/test-utils/src/base/jest.config.ts b/Composer/packages/test-utils/src/base/jest.config.ts index e94f02436d..a041eb0921 100644 --- a/Composer/packages/test-utils/src/base/jest.config.ts +++ b/Composer/packages/test-utils/src/base/jest.config.ts @@ -20,7 +20,7 @@ const base: Partial = { transformIgnorePatterns: ['/node_modules/'], - setupFilesAfterEnv: [path.resolve(__dirname, 'setup.js')], + setupFiles: [path.resolve(__dirname, 'setupEnv.js')], }; export default base; diff --git a/Composer/packages/test-utils/src/base/setup.ts b/Composer/packages/test-utils/src/base/setup.ts deleted file mode 100644 index a40322e27d..0000000000 --- a/Composer/packages/test-utils/src/base/setup.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-var-requires */ - -try { - // Not all packages will require format-message, so just swallow the error - const formatMessage = require('format-message'); - - formatMessage.setup({ - missingTranslation: 'ignore', - }); -} catch { - // ignore -} diff --git a/Composer/packages/test-utils/src/base/setupEnv.ts b/Composer/packages/test-utils/src/base/setupEnv.ts new file mode 100644 index 0000000000..a0d11aa5cd --- /dev/null +++ b/Composer/packages/test-utils/src/base/setupEnv.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-var-requires */ + +// try { +// // Not all packages will require format-message, so just swallow the error +// const formatMessage = require('format-message'); + +// formatMessage.setup({ +// missingTranslation: 'ignore', +// }); +// } catch { +// // ignore +// } + +// eslint-disable-next-line no-console +const oldWarn = console.warn; +const oldError = console.error; + +console.warn = (...args) => { + if (args.some((msg) => typeof msg === 'string' && msg.startsWith('Translation for'))) { + return; + } + + oldWarn(...args); +}; + +console.error = (...args) => { + if (args.some((msg) => typeof msg === 'string' && msg.startsWith('Warning: Cannot update a component'))) { + return; + } + + oldError(...args); +}; diff --git a/Composer/plugins/README.md b/Composer/plugins/README.md index 87e1971d9d..984a4f0438 100644 --- a/Composer/plugins/README.md +++ b/Composer/plugins/README.md @@ -42,7 +42,7 @@ Plugin modules must come in one of the following forms: Currently, plugins can be loaded into Composer using 1 of 2 methods: * The plugin is placed in the /plugins/ folder, and contains a package.json file with `extendsComposer` set to `true` -* The plugin is loaded directly via changes to Composer code, using `pluginLoader.loadPlugin(name, plugin)` +* The plugin is loaded directly via changes to Composer code, using `ExtensionContext.loadPlugin(name, plugin)` The simplest form of a plugin module is below: @@ -122,9 +122,9 @@ This value is used by the built-in authentication middleware to redirect the use Note that if you specify an alternate URI for the login page, you must use `addAllowedUrl` to whitelist it. -#### PluginLoader.getUserFromRequest(req)` +#### ExtensionContext.getUserFromRequest(req)` -This is a static method on the PluginLoader class that extracts the user identity information provided by Passport. +This is a static method on the ExtensionContext class that extracts the user identity information provided by Passport. This is for use in the web route implementations to get user and provide it to other components of Composer. For example: @@ -132,7 +132,7 @@ For example: ```ts const RequestHandlerX = async (req, res) => { - const user = await PluginLoader.getUserFromRequest(req); + const user = await ExtensionContext.getUserFromRequest(req); // ... do some stuff diff --git a/Composer/plugins/mockRemotePublish/src/index.ts b/Composer/plugins/mockRemotePublish/src/index.ts index 3a113c7f2d..1ee87e60c2 100644 --- a/Composer/plugins/mockRemotePublish/src/index.ts +++ b/Composer/plugins/mockRemotePublish/src/index.ts @@ -8,7 +8,7 @@ */ import { v4 as uuid } from 'uuid'; -import { ComposerPluginRegistration, PublishResponse, PublishPlugin, JSONSchema7 } from '@bfc/extension'; +import { ExtensionRegistration, PublishResponse, PublishPlugin, JSONSchema7 } from '@bfc/extension'; import schema from './schema'; @@ -23,9 +23,9 @@ interface PublishConfig { class LocalPublisher implements PublishPlugin { private data: { [botId: string]: LocalPublishData }; - private composer: ComposerPluginRegistration; + private composer: ExtensionRegistration; public schema: JSONSchema7; - constructor(composer: ComposerPluginRegistration) { + constructor(composer: ExtensionRegistration) { this.data = {}; this.composer = composer; this.schema = schema; @@ -134,7 +134,7 @@ class LocalPublisher implements PublishPlugin { }; } -export default async (composer: ComposerPluginRegistration): Promise => { +export default async (composer: ExtensionRegistration): Promise => { const publisher = new LocalPublisher(composer); // pass in the custom storage class that will override the default await composer.addPublishMethod(publisher); diff --git a/Composer/plugins/sample-ui-plugin/README.md b/Composer/plugins/sample-ui-plugin/README.md index a39e185054..30c2abd40d 100644 --- a/Composer/plugins/sample-ui-plugin/README.md +++ b/Composer/plugins/sample-ui-plugin/README.md @@ -214,7 +214,7 @@ When Composer tries to load your extension, it will use this `main` property to ## Sample -To see a working sample in action, just navigate to `/sample-ui-plugin/package.json` in this directory and change the `extendsComposer` property to `true`, and the `composer.contributes.views.page-DISABLED` key to `page`. +To see a working sample in action, just navigate to `/sample-ui-plugin/package.json` in this directory and change the `composer.enabled` property to `true`, and the `composer.contributes.views.page-DISABLED` key to `page`. Then restart the Composer server. diff --git a/Composer/plugins/sample-ui-plugin/package.json b/Composer/plugins/sample-ui-plugin/package.json index 8cc70507a4..b5942c14de 100644 --- a/Composer/plugins/sample-ui-plugin/package.json +++ b/Composer/plugins/sample-ui-plugin/package.json @@ -9,6 +9,7 @@ "clean": "rimraf dist" }, "composer": { + "enabled": false, "bundles": [ { "id": "publish", @@ -31,7 +32,6 @@ } } }, - "extendsComposer": false, "main": "dist/index.js", "dependencies": { "@bfc/extension-client": "file:../../packages/extension-client", diff --git a/Composer/yarn.lock b/Composer/yarn.lock index ba3bde2608..3e8beffec5 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -865,9 +865,9 @@ js-tokens "^4.0.0" "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.11.3", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.6": - version "7.11.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" - integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -3528,6 +3528,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.1.tgz#91c8fc4c51f6d5dbe44c2ca9ab09310bd00c7918" + integrity sha512-B42Sxuaz09MhC3DDeW5kubRcQ5by4iuVQ0cRRWM2lggLzAa/KVom0Aft/208NgMvNQQZ86s5rVcqDdn/SH0/mg== + dependencies: + "@types/node" "*" + "@types/glob@*", "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -8522,8 +8529,8 @@ elegant-spinner@^1.0.1: elliptic@^6.0.0, elliptic@^6.5.3: version "6.5.3" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y= + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -9961,8 +9968,8 @@ fs-extra@^9.0.0: fs-extra@^9.0.1: version "9.0.1" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha1-kQ2gBiQ3ukw5/t2GPxZ1zP78ufw= + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" @@ -12503,8 +12510,8 @@ killable@^1.0.1: kind-of@^2.0.1, kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0= + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== kleur@^3.0.2: version "3.0.2" @@ -12918,8 +12925,8 @@ lodash.uniq@^4.5.0: "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.20" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI= + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== log-driver@^1.2.7: version "1.2.7" @@ -13439,8 +13446,8 @@ mixin-object@^2.0.1: mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.2, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8= + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" @@ -16044,7 +16051,7 @@ read-text-file@^1.1.0: iconv-lite "^0.4.17" jschardet "^1.4.2" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -16057,6 +16064,19 @@ read-text-file@^1.1.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^2.3.5: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^3.0.6, readable-stream@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" @@ -16995,8 +17015,8 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: set-value@^0.4.3, set-value@^2.0.0, set-value@^3.0.2: version "3.0.2" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A= + resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" + integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== dependencies: is-plain-object "^2.0.4"