diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 7d1e51d0a2..8243be9cf6 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -62,7 +62,8 @@ async function createAppDataDir() { const localPublishPath: string = join(composerAppDataPath, 'hostedBots'); 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_EXTENSION_MANIFEST = join(composerAppDataPath, 'extensions.json'); + process.env.COMPOSER_EXTENSION_DATA_DIR = join(composerAppDataPath, 'extension-data'); process.env.COMPOSER_REMOTE_EXTENSIONS_DIR = join(composerAppDataPath, 'extensions'); log('creating composer app data path at: ', composerAppDataPath); diff --git a/Composer/packages/extension/package.json b/Composer/packages/extension/package.json index 6c47a6889e..7f2dcb9968 100644 --- a/Composer/packages/extension/package.json +++ b/Composer/packages/extension/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@botframework-composer/types": "*", + "@types/debug": "^4.1.5", "@types/passport": "^1.0.3", "debug": "^4.1.1", "fs-extra": "^9.0.1", diff --git a/Composer/packages/extension/src/__tests__/setupEnv.ts b/Composer/packages/extension/src/__tests__/setupEnv.ts index 42911b1eac..5fa0510a59 100644 --- a/Composer/packages/extension/src/__tests__/setupEnv.ts +++ b/Composer/packages/extension/src/__tests__/setupEnv.ts @@ -3,6 +3,7 @@ import path from 'path'; -process.env.COMPOSER_EXTENSION_DATA = path.resolve(__dirname, '__manifest__.json'); +process.env.COMPOSER_EXTENSION_MANIFEST = path.resolve(__dirname, '__manifest__.json'); +process.env.COMPOSER_EXTENSION_DATA_DIR = path.resolve(__dirname, '__extension-data__'); 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/extensionContext.ts b/Composer/packages/extension/src/extensionContext.ts index ce41a505dc..e6678c6768 100644 --- a/Composer/packages/extension/src/extensionContext.ts +++ b/Composer/packages/extension/src/extensionContext.ts @@ -1,20 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import path from 'path'; -import fs from 'fs'; - import passport from 'passport'; import { Express } from 'express'; import { pathToRegexp } from 'path-to-regexp'; -import glob from 'globby'; import formatMessage from 'format-message'; import { UserIdentity, ExtensionCollection, RuntimeTemplate } from '@botframework-composer/types'; -import logger from './logger'; -import { ExtensionRegistration } from './extensionRegistration'; - -const log = logger.extend('extension-context'); export const DEFAULT_RUNTIME = 'csharp-azurewebapp'; class ExtensionContext { @@ -68,56 +60,6 @@ class ExtensionContext { }); } - public async loadPlugin(name: string, description: string, thisPlugin: any) { - log('Loading extension: %s', name); - - try { - const pluginRegistration = new ExtensionRegistration(this, name, description); - if (typeof thisPlugin.default === 'function') { - // the module exported just an init function - await thisPlugin.default.call(null, pluginRegistration); - } else if (thisPlugin.default && thisPlugin.default.initialize) { - // the module exported an object with an initialize method - await thisPlugin.default.initialize.call(null, pluginRegistration); - } else if (thisPlugin.initialize && typeof thisPlugin.initialize === 'function') { - // the module exported an object with an initialize method - await thisPlugin.initialize.call(null, pluginRegistration); - } else { - throw new Error(formatMessage('Could not init plugin')); - } - } catch (err) { - log('Error loading extension: %s', name); - // eslint-disable-next-line no-console - console.error(err); - } - } - - public async loadPluginFromFile(packageJsonPath: string) { - const packageJSON = fs.readFileSync(packageJsonPath, 'utf8'); - const json = JSON.parse(packageJSON); - - if (json.composer?.enabled !== false) { - const modulePath = path.dirname(packageJsonPath); - try { - // eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-var-requires - const thisPlugin = require(modulePath); - this.loadPlugin(json.name, json.description, thisPlugin); - } catch (err) { - log('Error:', err?.message); - } - } else { - // noop - this is not a composer plugin - } - } - - /** @deprecated */ - public async loadPluginsFromFolder(dir: string) { - const plugins = await glob('*/package.json', { cwd: dir, dot: true }); - for (const p in plugins) { - await this.loadPluginFromFile(path.join(dir, plugins[p])); - } - } - // get the runtime template currently used from project public getRuntimeByProject(project): RuntimeTemplate { const type = project.settings.runtime?.key || DEFAULT_RUNTIME; diff --git a/Composer/packages/extension/src/extensionRegistration.ts b/Composer/packages/extension/src/extensionRegistration.ts index e77f8e5b5e..6a21ba1846 100644 --- a/Composer/packages/extension/src/extensionRegistration.ts +++ b/Composer/packages/extension/src/extensionRegistration.ts @@ -1,20 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; + import { RequestHandler } from 'express-serve-static-core'; import { Debugger } from 'debug'; import { PublishPlugin, RuntimeTemplate, BotTemplate } from '@botframework-composer/types'; -import logger from './logger'; +import log from './logger'; import { ExtensionContext } from './extensionContext'; - -const log = logger.extend('extension-registration'); +import { Store } from './storage/store'; export class ExtensionRegistration { public context: typeof ExtensionContext; private _name: string; private _description: string; private _log: Debugger; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _store: Store | null = null; constructor(context: typeof ExtensionContext, name: string, description: string) { this.context = context; @@ -43,9 +46,19 @@ export class ExtensionRegistration { return this._log; } + public get store() { + if (this._store === null) { + const storePath = path.join(this.dataDir, `${this.name}.json`); + this._store = new Store(storePath, {}, this.log); + } + + return this._store; + } + /************************************************************************************** * Storage related features *************************************************************************************/ + // eslint-disable-next-line @typescript-eslint/no-explicit-any public async useStorage(customStorageClass: any) { if (!this.context.extensions.storage.customStorageClass) { this.context.extensions.storage.customStorageClass = customStorageClass; @@ -208,4 +221,13 @@ export class ExtensionRegistration { this.context.extensions.authentication.allowedUrls.push(url); } } + + private get dataDir() { + /* istanbul ignore next */ + if (!process.env.COMPOSER_EXTENSION_DATA_DIR) { + throw new Error('COMPOSER_EXTENSION_DATA_DIR must be set.'); + } + + return process.env.COMPOSER_EXTENSION_DATA_DIR; + } } diff --git a/Composer/packages/extension/src/manager/manager.ts b/Composer/packages/extension/src/manager/manager.ts index 6735a6370c..a4d7f5fd6e 100644 --- a/Composer/packages/extension/src/manager/manager.ts +++ b/Composer/packages/extension/src/manager/manager.ts @@ -12,6 +12,7 @@ import logger from '../logger'; import { ExtensionManifestStore } from '../storage/extensionManifestStore'; import { search, downloadPackage } from '../utils/npm'; import { isSubdirectory } from '../utils/isSubdirectory'; +import { ExtensionRegistration } from '../extensionRegistration'; const log = logger.extend('manager'); @@ -59,6 +60,7 @@ export class ExtensionManagerImp { */ public async loadAll() { await ensureDir(this.remoteDir); + await ensureDir(this.dataDir); await this.loadFromDir(this.builtinDir, true); await this.loadFromDir(this.remoteDir); @@ -131,11 +133,23 @@ export class ExtensionManagerImp { // eslint-disable-next-line @typescript-eslint/no-var-requires, security/detect-non-literal-require const extension = metadata?.path && require(metadata.path); - if (!extension) { + if (!extension || !metadata) { throw new Error(`Extension not found: ${id}`); } - await ExtensionContext.loadPlugin(id, '', extension); + const registration = new ExtensionRegistration(ExtensionContext, metadata.id, metadata.description); + if (typeof extension.default === 'function') { + // the module exported just an init function + await extension.default.call(null, registration); + } else if (extension.default && extension.default.initialize) { + // the module exported an object with an initialize method + await extension.default.initialize.call(null, registration); + } else if (extension.initialize && typeof extension.initialize === 'function') { + // the module exported an object with an initialize method + await extension.initialize.call(null, registration); + } else { + throw new Error('Could not init extension'); + } } catch (err) { log('Unable to load extension `%s`', id); log('%O', err); @@ -245,7 +259,7 @@ export class ExtensionManagerImp { private get manifest() { /* istanbul ignore next */ if (!this._manifest) { - this._manifest = new ExtensionManifestStore(process.env.COMPOSER_EXTENSION_DATA as string); + this._manifest = new ExtensionManifestStore(process.env.COMPOSER_EXTENSION_MANIFEST as string); } return this._manifest; @@ -268,6 +282,15 @@ export class ExtensionManagerImp { return process.env.COMPOSER_REMOTE_EXTENSIONS_DIR; } + + private get dataDir() { + /* istanbul ignore next */ + if (!process.env.COMPOSER_EXTENSION_DATA_DIR) { + throw new Error('COMPOSER_EXTENSION_DATA_DIR must be set.'); + } + + return process.env.COMPOSER_EXTENSION_DATA_DIR; + } } const ExtensionManager = new ExtensionManagerImp(); diff --git a/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts b/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts index 3b67b42bb5..3aa883ba0a 100644 --- a/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts +++ b/Composer/packages/extension/src/storage/__tests__/extensionManifestStore.test.ts @@ -1,113 +1,57 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import fs from 'fs'; -import path from 'path'; +import { ExtensionManifestStore, ExtensionManifest } from '../extensionManifestStore'; -import { existsSync, writeJsonSync, readJsonSync } from 'fs-extra'; +const manifestPath = '../../../__manifest__.json'; -import { ExtensionManifestStore } from '../extensionManifestStore'; +const currentManifest = ({ + extension1: { + id: 'extension1', + }, +} as unknown) as ExtensionManifest; -jest.mock('fs-extra', () => ({ - existsSync: jest.fn(), - readJsonSync: jest.fn(), - writeJsonSync: jest.fn(), -})); +let store = new ExtensionManifestStore(manifestPath); -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 }); +beforeEach(() => { + store = new ExtensionManifestStore(manifestPath); + Object.entries(currentManifest).forEach(([id, data]) => { + store.updateExtensionConfig(id, data ?? {}); }); }); -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 }; - } - }); +describe('#getExtensionConfig', () => { + it('returns the extension metadata', () => { + expect(store.getExtensionConfig('extension1')).toEqual(currentManifest.extension1); }); +}); - 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', () => { + expect(store.getExtensions()).toEqual(currentManifest); }); +}); - describe('#getExtensions', () => { - it('returns all extension metadata', () => { - const store = new ExtensionManifestStore(manifestPath); +describe('#updateExtensionConfig', () => { + it('creates a new entry if config does not exist', () => { + const newExtension = { id: 'newExtension' }; + store.updateExtensionConfig('newExtension', newExtension); - expect(store.getExtensions()).toEqual(currentManifest); - }); + expect(store.getExtensionConfig('newExtension')).toEqual(newExtension); }); - 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); + it('updates the entry if config exist', () => { + store.updateExtensionConfig('extension1', { name: 'new name' }); - 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) - ); - }); + expect(store.getExtensionConfig('extension1')).toEqual({ id: 'extension1', name: 'new name' }); }); +}); - describe('#removeExtension', () => { - it('removes the extension from the manifest', () => { - const store = new ExtensionManifestStore(manifestPath); - store.removeExtension('extension1'); +describe('#removeExtension', () => { + it('removes the extension from the manifest', () => { + store.removeExtension('extension1'); - expect(writeJsonSync).toHaveBeenCalledWith(manifestPath, {}, expect.any(Object)); - expect(store.getExtensionConfig('extension1')).toBeUndefined(); - expect(store.getExtensions()).toEqual({}); - }); + expect(store.getExtensionConfig('extension1')).toBeUndefined(); + expect(store.getExtensions()).toEqual({}); }); }); diff --git a/Composer/packages/extension/src/storage/__tests__/store.test.ts b/Composer/packages/extension/src/storage/__tests__/store.test.ts new file mode 100644 index 0000000000..73c45f07ec --- /dev/null +++ b/Composer/packages/extension/src/storage/__tests__/store.test.ts @@ -0,0 +1,119 @@ +// 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 { Store } from '../store'; + +jest.mock('fs-extra', () => ({ + existsSync: jest.fn(), + readJsonSync: jest.fn(), + writeJsonSync: jest.fn(), +})); + +const testPath = path.resolve(__dirname, '../../../__manifest__.json'); + +beforeAll(() => { + // enable writing to disk for this test only + process.env.NODE_ENV = 'jest'; +}); + +afterAll(() => { + if (fs.existsSync(testPath)) { + fs.unlinkSync(testPath); + } + process.env.NODE_ENV = 'test'; +}); + +describe('when the store does not exist on disk', () => { + it('creates one with the default data', () => { + (existsSync as jest.Mock).mockReturnValue(false); + new Store(testPath, { defaultData: true }); + expect(writeJsonSync).toHaveBeenCalledWith(testPath, { defaultData: true }, { spaces: 2 }); + }); +}); + +describe('when the manifest already exists', () => { + let currentData = {}; + + beforeAll(() => { + if (fs.existsSync(testPath)) { + fs.unlinkSync(testPath); + } + + fs.writeFileSync(testPath, JSON.stringify({})); + }); + + beforeEach(() => { + currentData = { + some: 'data', + }; + + (existsSync as jest.Mock).mockReturnValue(true); + (readJsonSync as jest.Mock).mockImplementation((path) => { + if (path === testPath) { + return { ...currentData }; + } + }); + }); + + it('reads from the manifest', () => { + new Store(testPath, {}); + expect(readJsonSync).toHaveBeenCalledWith(testPath); + }); + + describe('#readAll', () => { + it('returns current data', () => { + const store = new Store(testPath, currentData); + (readJsonSync as jest.Mock).mockClear(); + expect(store.readAll()).toEqual({ some: 'data' }); + expect(readJsonSync).toHaveBeenCalled(); + + currentData = { some: 'data', new: 'data' }; + store.replace(currentData); + expect(store.readAll()).toEqual(currentData); + }); + }); + + describe('#read', () => { + it('can read a key from the store', () => { + const store = new Store(testPath, currentData); + expect(store.read('some')).toEqual('data'); + expect(store.read('foo')).toBeUndefined(); + }); + }); + + describe('#write', () => { + it('writes a single value into the store', () => { + const store = new Store(testPath, currentData); + (writeJsonSync as jest.Mock).mockClear(); + store.write('new', 'value'); + expect(writeJsonSync).toHaveBeenCalledWith(testPath, expect.objectContaining({ new: 'value' }), { spaces: 2 }); + }); + }); + + describe('#delete', () => { + it('removes a single value from the store', () => { + currentData = { ...currentData, new: 'value' }; + const store = new Store(testPath, currentData); + (writeJsonSync as jest.Mock).mockClear(); + store.delete('new'); + expect(writeJsonSync).toHaveBeenCalledWith(testPath, { some: 'data' }, { spaces: 2 }); + }); + }); + + describe('#replace', () => { + it('writes new data to disk', () => { + const store = new Store(testPath, {}); + + currentData = { new: 'data' }; + store.replace(currentData); + + expect(writeJsonSync).toHaveBeenCalledWith(testPath, currentData, { spaces: 2 }); + expect(store.readAll()).toEqual(currentData); + }); + }); +}); diff --git a/Composer/packages/extension/src/storage/extensionManifestStore.ts b/Composer/packages/extension/src/storage/extensionManifestStore.ts index 795cd53082..5d400f840f 100644 --- a/Composer/packages/extension/src/storage/extensionManifestStore.ts +++ b/Composer/packages/extension/src/storage/extensionManifestStore.ts @@ -1,37 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { existsSync, writeJsonSync, readJsonSync } from 'fs-extra'; import { ExtensionMap, ExtensionMetadata } from '@botframework-composer/types'; import logger from '../logger'; +import { Store } from './store'; + const log = logger.extend('extensions'); export type ExtensionManifest = ExtensionMap; const DEFAULT_MANIFEST: ExtensionManifest = {}; -/** In-memory representation of extensions.json as well as reads / writes data to disk. */ +/** In-memory representation of extensions.json */ export class ExtensionManifestStore { - private manifest: ExtensionManifest = DEFAULT_MANIFEST; + private store: Store; 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 + this.store = new Store(this.manifestPath, DEFAULT_MANIFEST, log); // 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(); + this.store.replace((this.manifest.extensions as unknown) as ExtensionManifest); } } @@ -44,44 +37,17 @@ export class ExtensionManifestStore { } public removeExtension(id: string) { - delete this.manifest[id]; - // sync changes to disk - this.writeManifestToDisk(); + this.store.delete(id); } // update extension config public updateExtensionConfig(id: string, newConfig: Partial) { const currentConfig = this.manifest[id]; - if (currentConfig) { - this.manifest[id] = Object.assign({}, currentConfig, newConfig); - } else { - this.manifest[id] = Object.assign({} as ExtensionMetadata, newConfig); - } - // sync changes to disk - this.writeManifestToDisk(); + this.store.write(id, Object.assign({} as ExtensionMetadata, currentConfig ?? {}, newConfig)); } - 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); - } + private get manifest() { + return this.store.readAll(); } } diff --git a/Composer/packages/extension/src/storage/store.ts b/Composer/packages/extension/src/storage/store.ts new file mode 100644 index 0000000000..63d8b409df --- /dev/null +++ b/Composer/packages/extension/src/storage/store.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import path from 'path'; + +import { existsSync, writeJsonSync, readJsonSync, unlinkSync } from 'fs-extra'; + +type StoreData = { [key: string]: unknown }; +export class Store { + private path: string; + private data: Partial; + + public constructor(storePath: string, private defaultValue: T, private _log?: debug.Debugger) { + this.path = storePath; + this.data = { ...defaultValue }; + + if (!existsSync(this.path)) { + this.log('%s does not exist yet. Writing file to path: %s', path.basename(this.path ?? ''), this.path); + this.writeToDisk(); + } else { + this.readFromDisk(); + } + } + + public readAll() { + this.readFromDisk(); + return this.data; + } + + public read(key: string): unknown | undefined { + this.readFromDisk(); + return this.data[key]; + } + + public write(key: string, value: unknown) { + this.data = { ...this.data, [key]: value }; + this.writeToDisk(); + } + + public delete(key: string) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [key]: _, ...newData } = this.data; + this.data = newData as Partial; + this.writeToDisk(); + } + + public replace(newData: Partial) { + this.data = { ...newData }; + this.writeToDisk(); + } + + public destroy() { + unlinkSync(this.path); + } + + private readFromDisk() { + if (process.env.NODE_ENV !== 'test') { + try { + const data: Partial = readJsonSync(this.path); + this.data = data ?? this.defaultValue; + } catch (e) { + this.log('Error reading %s: %O', this.path, e); + } + } + } + + private writeToDisk() { + if (process.env.NODE_ENV !== 'test') { + try { + writeJsonSync(this.path, this.data, { spaces: 2 }); + } catch (e) { + this.log('Error writing %s: %s', this.path, e); + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private log(formatter: any, ...args: any[]) { + if (this._log) { + this._log(formatter, ...args); + } + } +} diff --git a/Composer/packages/server/src/controllers/__tests__/publisher.test.ts b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts index 72d8fbcf63..da0b8825a1 100644 --- a/Composer/packages/server/src/controllers/__tests__/publisher.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/publisher.test.ts @@ -5,7 +5,7 @@ import path from 'path'; import { Request, Response } from 'express'; import rimraf from 'rimraf'; -import { ExtensionContext } from '@bfc/extension'; +import { ExtensionManager } from '@bfc/extension'; import { BotProjectService } from '../../services/project'; import { Path } from '../../utility/path'; @@ -41,7 +41,7 @@ beforeEach(async () => { }); beforeAll(async () => { - await ExtensionContext.loadPluginsFromFolder(pluginDir); + await ExtensionManager.loadFromDir(pluginDir, true); }); afterEach(() => { diff --git a/Composer/packages/server/src/server.ts b/Composer/packages/server/src/server.ts index a657aacce8..d9eeb146ab 100644 --- a/Composer/packages/server/src/server.ts +++ b/Composer/packages/server/src/server.ts @@ -47,7 +47,8 @@ export async function start(): Promise { ExtensionContext.useExpress(app); // load all installed plugins - setEnvDefault('COMPOSER_EXTENSION_DATA', path.resolve(__dirname, '../../../.composer/extensions.json')); + setEnvDefault('COMPOSER_EXTENSION_MANIFEST', path.resolve(__dirname, '../../../.composer/extensions.json')); + setEnvDefault('COMPOSER_EXTENSION_DATA_DIR', path.resolve(__dirname, '../../../.composer/extension-data')); setEnvDefault('COMPOSER_BUILTIN_EXTENSIONS_DIR', path.resolve(__dirname, '../../../../extensions')); // Composer/.composer/extensions setEnvDefault('COMPOSER_REMOTE_EXTENSIONS_DIR', path.resolve(__dirname, '../../../.composer/extensions')); diff --git a/Composer/yarn.lock b/Composer/yarn.lock index ee886a588e..4e3781b4dd 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -3520,8 +3520,8 @@ "@types/debug@^4.1.5": version "4.1.5" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@types/debug/-/@types/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" - integrity sha1-sU76iFK3do2JiQZhPCP2iHE+As0= + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" + integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" @@ -5648,8 +5648,8 @@ bindings@1.2.1: bl@^2.2.1, bl@^4.0.3: version "2.2.1" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" - integrity sha1-jBGntzBlXF1WiYzchxIk9A/ZAdU= + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" + integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== dependencies: readable-stream "^2.3.5" safe-buffer "^5.1.1" @@ -16628,8 +16628,8 @@ read-text-file@^1.1.0, read-text-file@~1.1.0: readable-stream@^2.3.5: version "2.3.7" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha1-Hsoc9xGu+BTAT2IlKjamL2yyO1c= + 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" diff --git a/Dockerfile b/Dockerfile index 31e113d226..64b3e5e345 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,6 @@ COPY --from=composerbasic /app .. ENV COMPOSER_BUILTIN_EXTENSIONS_DIR "/app/extensions" ENV COMPOSER_REMOTE_EXTENSIONS_DIR "/app/remote-extensions" -ENV COMPOSER_EXTENSION_DATA "/app/extensions.json" - -CMD ["yarn","start:server"] \ No newline at end of file +ENV COMPOSER_REMOTE_EXTENSION_DATA_DIR "/app/extension-data" +ENV COMPOSER_EXTENSION_MANIFEST "/app/extensions.json" +CMD ["yarn","start:server"] diff --git a/extensions/README.md b/extensions/README.md index 9587373b92..39fee282a2 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -43,12 +43,12 @@ Extension modules must come in one of the following forms: Currently, extensions can be loaded into Composer using 1 of 2 methods: - The extension is placed in the /extensions/ folder -- The extension is loaded directly via changes to Composer code, using `ExtensionContext.loadPlugin(name, extension)` +- The extension is installed via the extensions page The simplest form of a extension module is below: ```ts -export default async (composer: any): Promise => { +export default async (composer: ExtensionRegistration): Promise => { // call methods (see below) on the composer API // composer.useStorage(...); // composer.usePassportStrategy(...); diff --git a/extensions/sample-ui-plugin/src/node/index.ts b/extensions/sample-ui-plugin/src/node/index.ts index 3257526b49..6ff8666b52 100644 --- a/extensions/sample-ui-plugin/src/node/index.ts +++ b/extensions/sample-ui-plugin/src/node/index.ts @@ -21,6 +21,12 @@ function initialize(registration: ExtensionRegistration) { }; registration.addPublishMethod(plugin1); registration.addPublishMethod(plugin2); + + // test reading and writing data + registration.log('Reading from store:\n%O', registration.store.readAll()); + + registration.store.replace({ some: 'data' }); + registration.log('Reading from store:\n%O', registration.store.readAll()); } async function getStatus(config, project, user) { diff --git a/extensions/sample-ui-plugin/yarn.lock b/extensions/sample-ui-plugin/yarn.lock index aca45b26ac..12107b57d2 100644 --- a/extensions/sample-ui-plugin/yarn.lock +++ b/extensions/sample-ui-plugin/yarn.lock @@ -57,6 +57,7 @@ version "1.0.0" dependencies: "@botframework-composer/types" "*" + "@types/debug" "^4.1.5" "@types/passport" "^1.0.3" debug "^4.1.1" fs-extra "^9.0.1" @@ -233,6 +234,11 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" + integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== + "@types/express-serve-static-core@*": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084"