diff --git a/PublicAPI.md b/PublicAPI.md index e29610ab9a..0434108d54 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -3,35 +3,17 @@ Public API This document describes all methods that can be invoked when NativeScript CLI is required as library, i.e. - - - - - - - - - - -
- JavaScript - - TypeScript -
-
+```JavaScript
 const tns = require("nativescript");
-
-
-
-import * as tns from "nativescript";
-
-
+``` ## Module projectService `projectService` modules allow you to create new NativeScript application. -* `createProject(projectSettings: IProjectSettings): Promise` - Creates new NativeScript application. By passing `projectSettings` argument you specify the name of the application, the template that will be used, etc.: +### createProject +* Description: `createProject(projectSettings: IProjectSettings): Promise` - Creates new NativeScript application. By passing `projectSettings` argument you specify the name of the application, the template that will be used, etc.: + ```TypeScript /** * Describes available settings when creating new NativeScript application. @@ -73,71 +55,130 @@ interface IProjectSettings { } ``` -Sample usage: - - - - - - - - - -
- JavaScript - - TypeScript -
-
+* Sample usage:
+```JavaScript
 const projectSettings = {
 	projectName: "my-ns-app",
-    template: "ng",
-    pathToProject: "/home/my-user/project-dir"
+	template: "ng",
+	pathToProject: "/home/my-user/project-dir"
 };
 
 tns.projectService.createProject(projectSettings)
 	.then(() => console.log("Project successfully created."))
-    .catch((err) => console.log("Unable to create project, reason: ", err);
-
-
-
-const projectSettings: IProjectSettings = {
-	projectName: "my-ns-app",
-    template: "ng",
-    pathToProject: "/home/my-user/project-dir"
-};
+	.catch((err) => console.log("Unable to create project, reason: ", err);
+```
 
-tns.projectService.createProject(projectSettings)
-	.then(() => console.log("Project successfully created."))
-    .catch((err) => console.log("Unable to create project, reason: ", err);
-
-
- -* `isValidNativeScriptProject(projectDir: string): boolean` - Checks if the specified path is a valid NativeScript project. Returns `true` in case the directory is a valid project, `false` otherwise. - -Sample usage: - - - - - - - - - -
- JavaScript - - TypeScript -
-
-const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
-
-
-
+### isValidNativeScriptProject
+* Definition: `isValidNativeScriptProject(projectDir: string): boolean` - Checks if the specified path is a valid NativeScript project. Returns `true` in case the directory is a valid project, `false` otherwise.
+
+* Sample usage:
+```JavaScript
 const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
-
-
+console.log(isValidProject); // true or false +``` + +## extensibilityService +`extensibilityService` module gives access to methods for working with CLI's extensions - list, install, uninstall, load them. The extensions add new functionality to CLI, so once an extension is loaded, all methods added to it's public API are accessible directly through CLI when it is used as a library. Extensions may also add new commands, so they are accessible through command line when using NativeScript CLI. + +A common interface describing the results of a method is `IExtensionData`: +```TypeScript +/** + * Describes each extension. + */ +interface IExtensionData { + /** + * The name of the extension. + */ + extensionName: string; +} +``` + +### installExtension +Installs specified extension and loads it in the current process, so the functionality that it adds can be used immediately. + +* Definition: +```TypeScript +/** + * Installs and loads specified extension. + * @param {string} extensionName Name of the extension to be installed. It may contain version as well, i.e. myPackage, myPackage@1.0.0, myPackage.tgz, https://github.com/myOrganization/myPackage/tarball/master, https://github.com/myOrganization/myPackage etc. + * @returns {Promise} Information about installed extensions. + */ +installExtension(extensionName: string): Promise; +``` + +* Usage: +```JavaScript +tns.extensibilityService.installExtension("extension-package") + .then(extensionData => console.log(`Successfully installed extension ${extensionData.extensionName}.`)) + .catch(err => console.log("Failed to install extension.")); +``` + +### uninstallExtension +Uninstalls specified extensions, so its functionality will no longer be available through CLI. + +* Definition: +```TypeScript +/** + * Uninstalls extension from the installation. + * @param {string} extensionName Name of the extension to be uninstalled. + * @returns {Promise} + */ +uninstallExtension(extensionName: string): Promise; +``` + +* Usage: +```JavaScript +tns.extensibilityService.uninstallExtension("extension-package") + .then(() => console.log("Successfully uninstalled extension.")) + .catch(err => console.log("Failed to uninstall extension.")); +``` + +### getInstalledExtensions +Gets information about all installed extensions. + +* Definition: +```TypeScript +/** + * Gets information about installed dependencies - names and versions. + * @returns {IStringDictionary} + */ +getInstalledExtensions(): IStringDictionary; +``` + +* Usage: +```JavaScript +const installedExtensions = tns.extensibilityService.getInstalledExtensions(); +for (let extensionName in installedExtensions) { + const version = installedExtensions[extensionName]; + console.log(`The extension ${extensionName} is installed with version ${version}.`); +} +``` + +### loadExtensions +Loads all currently installed extensions. The method returns array of Promises, one for each installed extension. In case any of the extensions cannot be loaded, only its Promise is rejected. + +* Definition +```TypeScript +/** + * Loads all extensions, so their methods and commands can be used from CLI. + * For each of the extensions, a new Promise is returned. It will be rejected in case the extension cannot be loaded. However other promises will not be reflected by this failure. + * In case a promise is rejected, the error will have additional property (extensionName) that shows which is the extension that cannot be loaded in the process. + * @returns {Promise[]} Array of promises, each is resolved with information about loaded extension. + */ +loadExtensions(): Promise[]; +``` + +* Usage: +```JavaScript +const loadExtensionsPromises = tns.extensibilityService.loadExtensions(); +for (let promise of loadExtensionsPromises) { + promise.then(extensionData => console.log(`Loaded extension: ${extensionData.extensionName}.`), + err => { + console.log(`Failed to load extension: ${err.extensionName}`); + console.log(err); + }); +} +``` ## How to add a new method to Public API CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification. diff --git a/docs/man_pages/general/extension-install.md b/docs/man_pages/general/extension-install.md new file mode 100644 index 0000000000..0c98c4a3d6 --- /dev/null +++ b/docs/man_pages/general/extension-install.md @@ -0,0 +1,35 @@ +extension install +========== + +Usage | Synopsis +------|------- +General | `$ tns extension install ` + +Installs specified extension. Each extension adds additional functionality that's accessible directly from NativeScript CLI. + +### Attributes + +* `` is any of the following. + * A `` or `@` where `` is the name of a package that is published in the npm registry and `` is a valid version of this plugin. + * A `` to the directory which contains the extension, including its `package.json` file. + * A `` to a `.tar.gz` archive containing a directory with the extension and its `package.json` file. + * A `` which resolves to a `.tar.gz` archive containing a directory with the extension and its `package.json` file. + * A `` which resolves to a `.tar.gz` archive containing a directory with the extension and its `package.json` file. + +<% if(isHtml) { %> +### Related Commands + +Command | Description +----------|---------- +[extension](extension.html) | Prints information about all installed extensions. +[extension-uninstall](extension-uninstall.html) | Uninstalls specified extension. +[autocomplete-status](autocomplete-status.html) | Prints the current status of your command-line completion settings. +[autocomplete-enable](autocomplete-enable.html) | Configures your current command-line completion settings. +[autocomplete-disable](autocomplete-disable.html) | Disables command-line completion for bash and zsh shells. +[usage-reporting](usage-reporting.html) | Configures anonymous usage reporting for the NativeScript CLI. +[error-reporting](error-reporting.html) | Configures anonymous error reporting for the NativeScript CLI. +[doctor](doctor.html) | Checks your system for configuration problems which might prevent the NativeScript CLI from working properly. +[proxy](proxy.html) | Displays proxy settings. +[proxy clear](proxy-clear.html) | Clears proxy settings. +[proxy set](proxy-set.html) | Sets proxy settings. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/general/extension-uninstall.md b/docs/man_pages/general/extension-uninstall.md new file mode 100644 index 0000000000..8c7be09c88 --- /dev/null +++ b/docs/man_pages/general/extension-uninstall.md @@ -0,0 +1,31 @@ +extension uninstall +========== + +Usage | Synopsis +------|------- +General | `$ tns extension uninstall ` + +Uninstalls specified extension. After that you will not be able to use the functionality that this extensions adds to NativeScript CLI. + +### Attributes + +* `` is the name of the extension as listed in its `package.json` file. + +<% if(isHtml) { %> +### Related Commands + +Command | Description +----------|---------- +[extension](extension.html) | Prints information about all installed extensions. +[extension-uninstall](extension-uninstall.html) | Uninstalls specified extension. +[extension-install](extension-install.html) | Installs specified extension. +[autocomplete-status](autocomplete-status.html) | Prints the current status of your command-line completion settings. +[autocomplete-enable](autocomplete-enable.html) | Configures your current command-line completion settings. +[autocomplete-disable](autocomplete-disable.html) | Disables command-line completion for bash and zsh shells. +[usage-reporting](usage-reporting.html) | Configures anonymous usage reporting for the NativeScript CLI. +[error-reporting](error-reporting.html) | Configures anonymous error reporting for the NativeScript CLI. +[doctor](doctor.html) | Checks your system for configuration problems which might prevent the NativeScript CLI from working properly. +[proxy](proxy.html) | Displays proxy settings. +[proxy clear](proxy-clear.html) | Clears proxy settings. +[proxy set](proxy-set.html) | Sets proxy settings. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/general/extension.md b/docs/man_pages/general/extension.md new file mode 100644 index 0000000000..d0ed3b5413 --- /dev/null +++ b/docs/man_pages/general/extension.md @@ -0,0 +1,25 @@ +extension +========== + +Usage | Synopsis +------|------- +General | `$ tns extension` + +Prints information about all installed extensions. + +<% if(isHtml) { %> +### Related Commands + +Command | Description +----------|---------- +[extension-install](extension-install.html) | Installs specified extension. +[autocomplete-status](autocomplete-status.html) | Prints the current status of your command-line completion settings. +[autocomplete-enable](autocomplete-enable.html) | Configures your current command-line completion settings. +[autocomplete-disable](autocomplete-disable.html) | Disables command-line completion for bash and zsh shells. +[usage-reporting](usage-reporting.html) | Configures anonymous usage reporting for the NativeScript CLI. +[error-reporting](error-reporting.html) | Configures anonymous error reporting for the NativeScript CLI. +[doctor](doctor.html) | Checks your system for configuration problems which might prevent the NativeScript CLI from working properly. +[proxy](proxy.html) | Displays proxy settings. +[proxy clear](proxy-clear.html) | Clears proxy settings. +[proxy set](proxy-set.html) | Sets proxy settings. +<% } %> \ No newline at end of file diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 5c7a73533d..cb26b7ef7d 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -125,3 +125,10 @@ $injector.require("projectChangesService", "./services/project-changes-service") $injector.require("emulatorPlatformService", "./services/emulator-platform-service"); $injector.require("staticConfig", "./config"); + +$injector.require("requireService", "./services/require-service"); + +$injector.requireCommand("extension|*list", "./commands/extensibility/list-extensions"); +$injector.requireCommand("extension|install", "./commands/extensibility/install-extension"); +$injector.requireCommand("extension|uninstall", "./commands/extensibility/uninstall-extension"); +$injector.requirePublic("extensibilityService", "./services/extensibility-service"); diff --git a/lib/commands/extensibility/install-extension.ts b/lib/commands/extensibility/install-extension.ts new file mode 100644 index 0000000000..91bea2a76a --- /dev/null +++ b/lib/commands/extensibility/install-extension.ts @@ -0,0 +1,13 @@ +export class InstallExtensionCommand implements ICommand { + constructor(private $extensibilityService: IExtensibilityService, + private $stringParameterBuilder: IStringParameterBuilder, + private $logger: ILogger) { } + + public async execute(args: string[]): Promise { + const extensionData = await this.$extensibilityService.installExtension(args[0]); + this.$logger.info(`Successfully installed extension ${extensionData.extensionName}.`); + } + + allowedParameters: ICommandParameter[] = [this.$stringParameterBuilder.createMandatoryParameter("You have to provide a valid name for extension that you want to install.")]; +} +$injector.registerCommand("extension|install", InstallExtensionCommand); diff --git a/lib/commands/extensibility/list-extensions.ts b/lib/commands/extensibility/list-extensions.ts new file mode 100644 index 0000000000..aabc3f9c13 --- /dev/null +++ b/lib/commands/extensibility/list-extensions.ts @@ -0,0 +1,24 @@ +import * as helpers from "../../common/helpers"; + +export class ListExtensionsCommand implements ICommand { + constructor(private $extensibilityService: IExtensibilityService, + private $logger: ILogger) { } + + public async execute(args: string[]): Promise { + const installedExtensions = this.$extensibilityService.getInstalledExtensions(); + if (_.keys(installedExtensions).length) { + this.$logger.info("Installed extensions:"); + const data = _.map(installedExtensions, (version, name) => { + return [name, version]; + }); + + const table = helpers.createTable(["Name", "Version"], data); + this.$logger.out(table.toString()); + } else { + this.$logger.info("No extensions installed."); + } + } + + allowedParameters: ICommandParameter[] = []; +} +$injector.registerCommand("extension|*list", ListExtensionsCommand); diff --git a/lib/commands/extensibility/uninstall-extension.ts b/lib/commands/extensibility/uninstall-extension.ts new file mode 100644 index 0000000000..76ad4f882c --- /dev/null +++ b/lib/commands/extensibility/uninstall-extension.ts @@ -0,0 +1,14 @@ +export class UninstallExtensionCommand implements ICommand { + constructor(private $extensibilityService: IExtensibilityService, + private $stringParameterBuilder: IStringParameterBuilder, + private $logger: ILogger) { } + + public async execute(args: string[]): Promise { + const extensionName = args[0]; + await this.$extensibilityService.uninstallExtension(extensionName); + this.$logger.info(`Successfully uninstalled extension ${extensionName}`); + } + + allowedParameters: ICommandParameter[] = [this.$stringParameterBuilder.createMandatoryParameter("You have to provide a valid name for extension that you want to uninstall.")]; +} +$injector.registerCommand("extension|uninstall", UninstallExtensionCommand); diff --git a/lib/definitions/extensibility.d.ts b/lib/definitions/extensibility.d.ts new file mode 100644 index 0000000000..3aa9dba5cd --- /dev/null +++ b/lib/definitions/extensibility.d.ts @@ -0,0 +1,48 @@ +/** + * Describes each extension. + */ +interface IExtensionData { + /** + * The name of the extension. + */ + extensionName: string; +} + +/** + * Defines methods for working with CLI's extensions. + */ +interface IExtensibilityService { + /** + * Installs and loads specified extension. + * @param {string} extensionName Name of the extension to be installed. It may contain version as well, i.e. myPackage, myPackage@1.0.0, + * myPackage.tgz, https://github.com/myOrganization/myPackage/tarball/master, https://github.com/myOrganization/myPackage, etc. + * @returns {Promise} Information about installed extensions. + */ + installExtension(extensionName: string): Promise; + + /** + * Uninstalls extension from the installation. + * @param {string} extensionName Name of the extension to be uninstalled. + * @returns {Promise} + */ + uninstallExtension(extensionName: string): Promise; + + /** + * Loads all extensions, so their methods and commands can be used from CLI. + * For each of the extensions, a new Promise is returned. It will be rejected in case the extension cannot be loaded. However other promises will not be reflected by this failure. + * In case a promise is rejected, the error will have additional property (extensionName) that shows which is the extension that cannot be loaded in the process. + * @returns {Promise[]} Array of promises, each is resolved with information about loaded extension. + */ + loadExtensions(): Promise[]; + + /** + * Gets information about installed dependencies - names and versions. + * @returns {IStringDictionary} + */ + getInstalledExtensions(): IStringDictionary; +} + +/** + * Describes the error that will be raised when a problem with extension is detected. + */ +interface IExtensionLoadingError extends Error, IExtensionData { } \ No newline at end of file diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 8f5e22fc8d..638d675f7c 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -211,7 +211,7 @@ interface IPlatformSpecificData { provision: any; /** - * Target SDK for Android.s + * Target SDK for Android. */ sdk: string; } diff --git a/lib/definitions/require.d.ts b/lib/definitions/require.d.ts new file mode 100644 index 0000000000..83a40e595a --- /dev/null +++ b/lib/definitions/require.d.ts @@ -0,0 +1,12 @@ +/** + * Describes methods available in the require. + */ +interface IRequireService { + + /** + * Wrapper for the Node.js `require` method. + * @param {string} module Module to be required. + * @returns {any} The result of the require action. + */ + require(module: string): any; +} diff --git a/lib/nativescript-cli.ts b/lib/nativescript-cli.ts index 3d7434c688..143c5268d5 100644 --- a/lib/nativescript-cli.ts +++ b/lib/nativescript-cli.ts @@ -5,14 +5,25 @@ shelljs.config.fatal = true; import { installUncaughtExceptionListener } from "./common/errors"; installUncaughtExceptionListener(process.exit); +import { settlePromises } from "./common/helpers"; + (async () => { - let config: Config.IConfig = $injector.resolve("$config"); - let err: IErrors = $injector.resolve("$errors"); + const config: Config.IConfig = $injector.resolve("$config"); + const err: IErrors = $injector.resolve("$errors"); err.printCallStack = config.DEBUG; - let commandDispatcher: ICommandDispatcher = $injector.resolve("commandDispatcher"); + const logger: ILogger = $injector.resolve("logger"); + + const extensibilityService: IExtensibilityService = $injector.resolve("extensibilityService"); + try { + await settlePromises(extensibilityService.loadExtensions()); + } catch (err) { + logger.trace("Unable to load extensions. Error is: ", err); + } + + const commandDispatcher: ICommandDispatcher = $injector.resolve("commandDispatcher"); - let messages: IMessagesService = $injector.resolve("$messagesService"); + const messages: IMessagesService = $injector.resolve("$messagesService"); messages.pathsToMessageJsonFiles = [/* Place client-specific json message file paths here */]; if (process.argv[2] === "completion") { diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts new file mode 100644 index 0000000000..756e037f00 --- /dev/null +++ b/lib/services/extensibility-service.ts @@ -0,0 +1,137 @@ +import * as path from "path"; +import { cache, exported } from "../common/decorators"; +import * as constants from "../constants"; + +export class ExtensibilityService implements IExtensibilityService { + private get pathToExtensions(): string { + return path.join(this.$options.profileDir, "extensions"); + } + + private get pathToPackageJson(): string { + return path.join(this.pathToExtensions, constants.PACKAGE_JSON_FILE_NAME); + } + + constructor(private $fs: IFileSystem, + private $logger: ILogger, + private $npm: INodePackageManager, + private $options: IOptions, + private $requireService: IRequireService) { + } + + @exported("extensibilityService") + public async installExtension(extensionName: string): Promise { + this.$logger.trace(`Start installation of extension '${extensionName}'.`); + + await this.assertPackageJsonExists(); + + const npmOpts: any = { + save: true, + ["save-exact"]: true + }; + + const localPath = path.resolve(extensionName); + const packageName = this.$fs.exists(localPath) ? localPath : extensionName; + + const realName = (await this.$npm.install(packageName, this.pathToExtensions, npmOpts))[0]; + this.$logger.trace(`Finished installation of extension '${extensionName}'. Trying to load it now.`); + + // In case the extension is already installed, the $npm.install method will not return the name of the package. + // Fallback to the original value. + // NOTE: This will not be required once $npm.install starts working correctly. + return await this.loadExtension(realName || extensionName); + } + + @exported("extensibilityService") + public async uninstallExtension(extensionName: string): Promise { + this.$logger.trace(`Start uninstallation of extension '${extensionName}'.`); + + await this.assertPackageJsonExists(); + + await this.$npm.uninstall(extensionName, { save: true }, this.pathToExtensions); + + this.$logger.trace(`Finished uninstallation of extension '${extensionName}'.`); + } + + @exported("extensibilityService") + public loadExtensions(): Promise[] { + this.$logger.trace("Loading extensions."); + + let dependencies: IStringDictionary = null; + + try { + dependencies = this.getInstalledExtensions(); + } catch (err) { + this.$logger.trace(`Error while getting installed dependencies: ${err.message}. No extensions will be loaded.`); + } + + return _.keys(dependencies) + .map(name => this.loadExtension(name)); + } + + @exported("extensibilityService") + public getInstalledExtensions(): IStringDictionary { + if (this.$fs.exists(this.pathToPackageJson)) { + return this.$fs.readJson(this.pathToPackageJson).dependencies; + } + + return null; + } + + private async loadExtension(extensionName: string): Promise { + try { + await this.assertExtensionIsInstalled(extensionName); + + const pathToExtension = path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME, extensionName); + this.$requireService.require(pathToExtension); + return { extensionName }; + } catch (error) { + this.$logger.warn(`Error while loading ${extensionName} is: ${error.message}`); + const err = new Error(`Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + err.extensionName = extensionName; + throw err; + } + } + + private async assertExtensionIsInstalled(extensionName: string): Promise { + this.$logger.trace(`Asserting extension ${extensionName} is installed.`); + const installedExtensions = this.$fs.readDirectory(path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME)); + + if (installedExtensions.indexOf(extensionName) === -1) { + this.$logger.trace(`Extension ${extensionName} is not installed, starting installation.`); + await this.installExtension(extensionName); + } + + this.$logger.trace(`Extension ${extensionName} is installed.`); + } + + @cache() + private assertExtensionsDirExists(): void { + if (!this.$fs.exists(this.pathToExtensions)) { + this.$fs.createDirectory(this.pathToExtensions); + } + } + + @cache() + private assertPackageJsonExists(): void { + this.assertExtensionsDirExists(); + + if (!this.$fs.exists(this.pathToPackageJson)) { + this.$logger.trace(`Creating ${this.pathToPackageJson}.`); + + // create default package.json + this.$fs.writeJson(this.pathToPackageJson, { + name: "nativescript-extensibility", + version: "1.0.0", + description: "The place where all packages that extend CLI will be installed.", + license: "Apache-2.0", + readme: "The place where all packages that extend CLI will be installed.", + repository: "none", + dependencies: {} + }); + + this.$logger.trace(`Created ${this.pathToPackageJson}.`); + } + } +} + +$injector.register("extensibilityService", ExtensibilityService); diff --git a/lib/services/require-service.ts b/lib/services/require-service.ts new file mode 100644 index 0000000000..6902389e02 --- /dev/null +++ b/lib/services/require-service.ts @@ -0,0 +1,7 @@ +export class RequireService implements IRequireService { + public require(module: string): any { + return require(module); + } +} + +$injector.register("requireService", RequireService); diff --git a/test/nativescript-cli-lib.ts b/test/nativescript-cli-lib.ts index f4fe159e97..394eebd871 100644 --- a/test/nativescript-cli-lib.ts +++ b/test/nativescript-cli-lib.ts @@ -16,7 +16,8 @@ describe("nativescript-cli-lib", () => { deviceEmitter: null, projectService: ["createProject", "isValidNativeScriptProject"], localBuildService: ["build"], - deviceLogProvider: null + deviceLogProvider: null, + extensibilityService: ["loadExtensions", "getInstalledExtensions", "installExtension", "uninstallExtension"] }; const pathToEntryPoint = path.join(__dirname, "..", "lib", "nativescript-cli-lib.js").replace(/\\/g, "\\\\"); diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts new file mode 100644 index 0000000000..3e02472ee8 --- /dev/null +++ b/test/services/extensibility-service.ts @@ -0,0 +1,612 @@ +import { ExtensibilityService } from "../../lib/services/extensibility-service"; +import { Yok } from "../../lib/common/yok"; +import * as stubs from "../stubs"; +import { assert } from "chai"; +import * as constants from "../../lib/constants"; +import * as path from "path"; + +describe("extensibilityService", () => { + const getTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("fs", {}); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register("npm", {}); + testInjector.register("options", { + profileDir: "profileDir" + }); + testInjector.register("requireService", { + require: (pathToRequire: string): any => undefined + }); + return testInjector; + }; + + describe("installExtension", () => { + describe("fails", () => { + it("when extensions dir does not exist and trying to create it fails", async () => { + const expectedErrorMessage = "Unable to create dir"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => false; + fs.createDirectory = (dirToCreate: string): void => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await assert.isRejected(extensibilityService.installExtension("extensionToInstall"), expectedErrorMessage); + }); + + it("when extensions dir exists, but default package.json is missing and trying to create it fails", async () => { + const expectedErrorMessage = "Unable to write json"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== constants.PACKAGE_JSON_FILE_NAME; + fs.writeJson = (pathToFile: string, content: any) => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await assert.isRejected(extensibilityService.installExtension("extensionToInstall"), expectedErrorMessage); + }); + + it("when npm install fails", async () => { + const expectedErrorMessage = "Unable to install package"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => true; + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await assert.isRejected(extensibilityService.installExtension("extensionToInstall"), expectedErrorMessage); + }); + }); + + describe("passes correct arguments to npm install", () => { + const getArgsPassedToNpmInstallDuringInstallExtensionCall = async (userSpecifiedValue: string, testInjector?: IInjector): Promise => { + testInjector = testInjector || getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => true; + + fs.readDirectory = (dir: string): string[] => [userSpecifiedValue]; + + const npm: INodePackageManager = testInjector.resolve("npm"); + let argsPassedToNpmInstall: any = {}; + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { + argsPassedToNpmInstall.packageName = packageName; + argsPassedToNpmInstall.pathToSave = pathToSave; + argsPassedToNpmInstall.config = config; + return [userSpecifiedValue]; + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await extensibilityService.installExtension(userSpecifiedValue); + + return argsPassedToNpmInstall; + }; + + const assertPackageNamePassedToNpmInstall = async (userSpecifiedValue: string, expectedValue: string): Promise => { + const argsPassedToNpmInstall = await getArgsPassedToNpmInstallDuringInstallExtensionCall(userSpecifiedValue); + assert.deepEqual(argsPassedToNpmInstall.packageName, expectedValue); + }; + + it("passes full path for installation, when trying to install local package (user specifies relative path)", async () => { + const extensionName = "../extension1"; + await assertPackageNamePassedToNpmInstall(extensionName, path.resolve(extensionName)); + }); + + it("passes the value specified by user for installation, when the local path does not exist", async () => { + const extensionName = "extension1"; + await assertPackageNamePassedToNpmInstall(extensionName, path.resolve(extensionName)); + }); + + it("passes save and save-exact options to npm install", async () => { + const extensionName = "extension1"; + const argsPassedToNpmInstall = await getArgsPassedToNpmInstallDuringInstallExtensionCall(extensionName); + const expectedNpmConfg: any = { save: true }; + expectedNpmConfg["save-exact"] = true; + assert.deepEqual(argsPassedToNpmInstall.config, expectedNpmConfg); + }); + + it("passes full path to extensions dir for installation", async () => { + const extensionName = "extension1"; + const testInjector = getTestInjector(); + const options: IOptions = testInjector.resolve("options"); + options.profileDir = "my-profile-dir"; + + const expectedDirForInstallation = path.join(options.profileDir, "extensions"); + const argsPassedToNpmInstall = await getArgsPassedToNpmInstallDuringInstallExtensionCall(extensionName, testInjector); + assert.deepEqual(argsPassedToNpmInstall.pathToSave, expectedDirForInstallation); + }); + }); + + it("returns the name of the installed extension", async () => { + const extensionName = "extension1"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionName; + + fs.readDirectory = (dir: string): string[] => [extensionName]; + + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => [extensionName]; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const actualResult = await extensibilityService.installExtension(extensionName); + assert.deepEqual(actualResult, { extensionName }); + }); + + it("throws error that has extensionName property when unable to load extension", async () => { + const expectedErrorMessage = "Require failed"; + + const extensionName = "extension1"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionName; + + fs.readDirectory = (dir: string): string[] => [extensionName]; + + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => [extensionName]; + + const requireService: IRequireService = testInjector.resolve("requireService"); + requireService.require = (pathToRequire: string) => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + let isErrorRaised = false; + try { + await extensibilityService.installExtension(extensionName); + } catch (err) { + isErrorRaised = true; + assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + assert.deepEqual(err.extensionName, extensionName); + } + + assert.isTrue(isErrorRaised); + }); + }); + + describe("loadExtensions", () => { + describe("returns correct results for each extension", () => { + it("resolves all Promises with correct values when all extensions can be loaded", async () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + const extensionNames = ["extension1", "extension2", "extension3"]; + fs.exists = (pathToCheck: string): boolean => true; + fs.readDirectory = (dir: string): string[] => { + assert.deepEqual(path.basename(dir), constants.NODE_MODULES_FOLDER_NAME); + // Simulates extensions are installed in node_modules + return extensionNames; + }; + + fs.readJson = (filename: string, encoding?: string): any => { + const dependencies: any = {}; + _.each(extensionNames, name => { + dependencies[name] = "1.0.0"; + }); + + return { dependencies }; + }; + + const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => ({ extensionName })); + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const actualResult = await Promise.all(extensibilityService.loadExtensions()); + assert.deepEqual(actualResult, expectedResults); + }); + + it("installs extensions that are available in package.json, but are not available in node_modules", async () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + const extensionNames = ["extension1", "extension2", "extension3"]; + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionNames[0]; + + let isFirstReadDirExecution = true; + fs.readDirectory = (dir: string): string[] => { + assert.deepEqual(path.basename(dir), constants.NODE_MODULES_FOLDER_NAME); + // Simulates extensions are installed in node_modules + if (isFirstReadDirExecution) { + isFirstReadDirExecution = false; + return extensionNames.filter(ext => ext !== "extension1"); + } else { + return extensionNames; + } + }; + + fs.readJson = (filename: string, encoding?: string): any => { + const dependencies: any = {}; + _.each(extensionNames, name => { + dependencies[name] = "1.0.0"; + }); + + return { dependencies }; + }; + + let isNpmInstallCalled = false; + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { + assert.deepEqual(packageName, extensionNames[0]); + isNpmInstallCalled = true; + return [packageName]; + }; + + const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => ({ extensionName })); + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const actualResult = await Promise.all(extensibilityService.loadExtensions()); + assert.deepEqual(actualResult, expectedResults); + assert.isTrue(isNpmInstallCalled); + }); + + it("rejects only promises for extensions that cannot be loaded", async () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + const extensionNames = ["extension1", "extension2", "extension3"]; + fs.exists = (pathToCheck: string): boolean => true; + fs.readDirectory = (dir: string): string[] => { + assert.deepEqual(path.basename(dir), constants.NODE_MODULES_FOLDER_NAME); + // Simulates extensions are installed in node_modules + return extensionNames; + }; + + fs.readJson = (filename: string, encoding?: string): any => { + const dependencies: any = {}; + _.each(extensionNames, name => { + dependencies[name] = "1.0.0"; + }); + + return { dependencies }; + }; + + const requireService: IRequireService = testInjector.resolve("requireService"); + requireService.require = (module: string) => { + if (path.basename(module) === extensionNames[0]) { + throw new Error("Unable to load module."); + } + }; + + const expectedResults: any[] = _.map(extensionNames, extensionName => ({ extensionName })); + expectedResults[0] = new Error("Unable to load extension extension1. You will not be able to use the functionality that it adds."); + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const promises = extensibilityService.loadExtensions(); + assert.deepEqual(promises.length, extensionNames.length); + + for (let index = 0; index < promises.length; index++) { + const loadExtensionPromise = promises[index]; + await loadExtensionPromise + .then(result => assert.deepEqual(result, expectedResults[index]), + err => { + assert.deepEqual(err.message, expectedResults[index].message); + assert.deepEqual(err.extensionName, extensionNames[index]); + }); + }; + }); + + it("rejects all promises when unable to read node_modules dir (simulate EPERM error)", async () => { + const testInjector = getTestInjector(); + const extensionNames = ["extension1", "extension2", "extension3"]; + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) === "extensions" || path.basename(pathToCheck) === constants.PACKAGE_JSON_FILE_NAME; + fs.readJson = (filename: string, encoding?: string): any => { + const dependencies: any = {}; + _.each(extensionNames, name => { + dependencies[name] = "1.0.0"; + }); + + return { dependencies }; + }; + + let isReadDirCalled = false; + fs.readDirectory = (dir: string): string[] => { + isReadDirCalled = true; + assert.deepEqual(path.basename(dir), constants.NODE_MODULES_FOLDER_NAME); + throw new Error(`Unable to read ${constants.NODE_MODULES_FOLDER_NAME} dir.`); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const promises = extensibilityService.loadExtensions(); + + for (let index = 0; index < promises.length; index++) { + const loadExtensionPromise = promises[index]; + await loadExtensionPromise.then(res => { throw new Error("Shouldn't get here!"); }, + err => { + const extensionName = extensionNames[index]; + assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + assert.deepEqual(err.extensionName, extensionName); + }); + }; + + assert.deepEqual(promises.length, extensionNames.length); + assert.isTrue(isReadDirCalled, "readDirectory should have been called for the extensions."); + }); + + it("rejects all promises when unable to install extensions to extension dir (simulate EPERM error)", async () => { + const testInjector = getTestInjector(); + const extensionNames = ["extension1", "extension2", "extension3"]; + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) === "extensions" || path.basename(pathToCheck) === constants.PACKAGE_JSON_FILE_NAME; + fs.readJson = (filename: string, encoding?: string): any => { + const dependencies: any = {}; + _.each(extensionNames, name => { + dependencies[name] = "1.0.0"; + }); + + return { dependencies }; + }; + + let isReadDirCalled = false; + fs.readDirectory = (dir: string): string[] => { + isReadDirCalled = true; + assert.deepEqual(path.basename(dir), constants.NODE_MODULES_FOLDER_NAME); + return []; + }; + + let isNpmInstallCalled = false; + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { + assert.deepEqual(packageName, extensionNames[0]); + isNpmInstallCalled = true; + throw new Error(`Unable to install to ${constants.NODE_MODULES_FOLDER_NAME} dir.`); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const promises = extensibilityService.loadExtensions(); + + for (let index = 0; index < promises.length; index++) { + const loadExtensionPromise = promises[index]; + await loadExtensionPromise.then(res => { + console.log("######### res = ", res); throw new Error("Shouldn't get here!"); + }, + err => { + const extensionName = extensionNames[index]; + assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + assert.deepEqual(err.extensionName, extensionName); + }); + }; + + assert.deepEqual(promises.length, extensionNames.length); + assert.isTrue(isNpmInstallCalled, "Npm install should have been called for the extensions."); + assert.isTrue(isReadDirCalled, "readDirectory should have been called for the extensions."); + }); + + it("does not return any promises when its unable to create extensions dir", () => { + + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => false; + const expectedErrorMessage = "Unable to create dir"; + fs.createDirectory = (dirToCreate: string): void => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const promises = extensibilityService.loadExtensions(); + + assert.deepEqual(promises.length, 0); + }); + + it("does not return any promises when its unable to read extensions package.json", () => { + + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => true; + const expectedErrorMessage = "Unable to read json"; + fs.readJson = (filename: string, encoding?: string): any => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const promises = extensibilityService.loadExtensions(); + + assert.deepEqual(promises.length, 0); + }); + + it("does not fail when package.json in extension dir does not exist", async () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => { + // Add the assert here, so we are sure the only call to fs.exists is for package.json of the extensions dir. + assert.deepEqual(path.basename(pathToCheck), constants.PACKAGE_JSON_FILE_NAME); + return false; + }; + + const expectedResults: IExtensionData[] = []; + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const actualResult = await Promise.all(extensibilityService.loadExtensions()); + assert.deepEqual(actualResult, expectedResults, "When there's no package.json in extensions dir, there's nothing for loading."); + }); + + it("does not fail when unable to read extensions dir package.json", async () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => { + // Add the assert here, so we are sure the only call to fs.exists is for package.json of the extensions dir. + assert.deepEqual(path.basename(pathToCheck), constants.PACKAGE_JSON_FILE_NAME); + return true; + }; + + fs.readJson = (filename: string, encoding?: string): any => { + throw new Error("Unable to read JSON"); + }; + + const expectedResults: IExtensionData[] = []; + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + const actualResult = await Promise.all(extensibilityService.loadExtensions()); + assert.deepEqual(actualResult, expectedResults, "When unable to read package.json in extensions dir, there's nothing for loading."); + }); + + }); + }); + + describe("uninstallExtension", () => { + describe("fails", () => { + it("when extensions dir does not exist and trying to create it fails", async () => { + const expectedErrorMessage = "Unable to create dir"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => false; + fs.createDirectory = (dirToCreate: string): void => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await assert.isRejected(extensibilityService.uninstallExtension("extensionToInstall"), expectedErrorMessage); + }); + + it("when extensions dir exists, but default package.json is missing and trying to create it fails", async () => { + const expectedErrorMessage = "Unable to write json"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== constants.PACKAGE_JSON_FILE_NAME; + fs.writeJson = (pathToFile: string, content: any) => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await assert.isRejected(extensibilityService.uninstallExtension("extensionToInstall"), expectedErrorMessage); + }); + + it("when npm uninstall fails", async () => { + const expectedErrorMessage = "Unable to install package"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => true; + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.uninstall = async (packageName: string, config?: any, path?: string): Promise => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await assert.isRejected(extensibilityService.uninstallExtension("extensionToInstall"), expectedErrorMessage); + }); + }); + + describe("passes correct arguments to npm uninstall", () => { + const getArgsPassedToNpmUninstallDuringUninstallExtensionCall = async (userSpecifiedValue: string, testInjector?: IInjector): Promise => { + testInjector = testInjector || getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => true; + + fs.readDirectory = (dir: string): string[] => [userSpecifiedValue]; + + const npm: INodePackageManager = testInjector.resolve("npm"); + let argsPassedToNpmInstall: any = {}; + npm.uninstall = async (packageName: string, config?: any, path?: string): Promise => { + argsPassedToNpmInstall.packageName = packageName; + argsPassedToNpmInstall.pathToSave = path; + argsPassedToNpmInstall.config = config; + return [userSpecifiedValue]; + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await extensibilityService.uninstallExtension(userSpecifiedValue); + + return argsPassedToNpmInstall; + }; + + const assertPackageNamePassedToNpmUninstall = async (userSpecifiedValue: string, expectedValue: string): Promise => { + const argsPassedToNpmInstall = await getArgsPassedToNpmUninstallDuringUninstallExtensionCall(userSpecifiedValue); + assert.deepEqual(argsPassedToNpmInstall.packageName, expectedValue); + }; + + it("passes the value specified by user for installation", async () => { + const extensionName = "extension1"; + await assertPackageNamePassedToNpmUninstall(extensionName, extensionName); + + const relativePathToExtension = "../extension1"; + await assertPackageNamePassedToNpmUninstall(relativePathToExtension, relativePathToExtension); + }); + + it("passes save option to npm uninstall", async () => { + const extensionName = "extension1"; + const argsPassedToNpmUninstall = await getArgsPassedToNpmUninstallDuringUninstallExtensionCall(extensionName); + const expectedNpmConfg: any = { save: true }; + assert.deepEqual(argsPassedToNpmUninstall.config, expectedNpmConfg); + }); + + it("passes full path to extensions dir for uninstallation", async () => { + const extensionName = "extension1"; + const testInjector = getTestInjector(); + const options: IOptions = testInjector.resolve("options"); + options.profileDir = "my-profile-dir"; + + const expectedDirForInstallation = path.join(options.profileDir, "extensions"); + const argsPassedToNpmUninstall = await getArgsPassedToNpmUninstallDuringUninstallExtensionCall(extensionName, testInjector); + assert.deepEqual(argsPassedToNpmUninstall.pathToSave, expectedDirForInstallation); + }); + }); + + it("executes successfully uninstall operation", async () => { + const extensionName = "extension1"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionName; + + fs.readDirectory = (dir: string): string[] => [extensionName]; + + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.uninstall = async (packageName: string, config?: any, path?: string): Promise => [extensionName]; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + await extensibilityService.uninstallExtension(extensionName); + }); + + }); + + describe("getInstalledExtensions", () => { + it("fails when unable to read package.json from extensions dir", () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string) => true; + const expectedErrorMessage = "Failed to read package.json"; + fs.readJson = (filename: string, encoding?: string): any => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + assert.throws(() => extensibilityService.getInstalledExtensions(), expectedErrorMessage); + }); + + it("returns null when there's no package.json dir", () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string) => false; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + assert.isNull(extensibilityService.getInstalledExtensions()); + }); + + it("returns undefined when package.json does not have dependencies section", () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string) => true; + fs.readJson = (filename: string, encoding?: string): any => { + return {}; + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + assert.isUndefined(extensibilityService.getInstalledExtensions()); + }); + + it("returns dependencies section of package.json", () => { + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string) => true; + const dependencies = { + "dep1": "1.0.0", + "dep2": "~1.0.0", + "dep3": "^1.0.0" + }; + + fs.readJson = (filename: string, encoding?: string): any => { + return { dependencies }; + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + assert.deepEqual(extensibilityService.getInstalledExtensions(), dependencies); + }); + }); +});