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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Composer/packages/electron-server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion Composer/packages/extension/src/__tests__/setupEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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__');
58 changes: 0 additions & 58 deletions Composer/packages/extension/src/extensionContext.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 25 additions & 3 deletions Composer/packages/extension/src/extensionRegistration.ts
Original file line number Diff line number Diff line change
@@ -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<any> | null = null;

constructor(context: typeof ExtensionContext, name: string, description: string) {
this.context = context;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
29 changes: 26 additions & 3 deletions Composer/packages/extension/src/manager/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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({});
});
});
Loading