Skip to content

Commit

Permalink
Implement extensibility model for CLI (#2724)
Browse files Browse the repository at this point in the history
Implement extensibilty of CLI that allows anyone to add easily create packages that add new functionality to NativeScript CLI. The packages are installed in a specific directory, so they are persisted through CLI's updated.
The directory where extensions are installed contains a package.json and each extension is npm package installed there.
The extensions can be mainatined in two different ways:
- navigate to the directory where extensions are installed and use `npm` for install/uninstall/update of packages.
- use CLI's commands to update them: `tns extension install <name>`, `tns extension uninstall <name>`, `tns extension`

Implement extensibilityService that executes all operations and expose it to public API. In {N} CLI the extensions are loaded in the entry point, before parsing command line arguments. This way extensions can add new commands.
In Fusion, after CLI is required as a library, the `extensibilityService.loadExtensions` method should be called. It returns array of Promises - one for each installed extension.

Add help for the new commands, but do not link the new commands in other commands help for the moment.

Add unit tests for the new service.
  • Loading branch information
rosen-vladimirov authored Apr 21, 2017
1 parent a9afa64 commit 6ad0cef
Show file tree
Hide file tree
Showing 16 changed files with 1,104 additions and 86 deletions.
201 changes: 121 additions & 80 deletions PublicAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,17 @@ Public API

This document describes all methods that can be invoked when NativeScript CLI is required as library, i.e.

<table>
<tr>
<td>
JavaScript
</td>
<td>
TypeScript
</td>
</tr>
<tr>
<td>
<pre lang="javascript">
```JavaScript
const tns = require("nativescript");
</pre>
</td>
<td>
<pre lang="typescript">
import * as tns from "nativescript";
</pre>
</td>
</tr>

</table>
```

## Module projectService

`projectService` modules allow you to create new NativeScript application.

* `createProject(projectSettings: IProjectSettings): Promise<void>` - 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<void>` - 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.
Expand Down Expand Up @@ -73,71 +55,130 @@ interface IProjectSettings {
}
```

Sample usage:
<table>
<tr>
<td>
JavaScript
</td>
<td>
TypeScript
</td>
</tr>
<tr>
<td>
<pre lang="javascript">
* 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);
</pre>
</td>
<td>
<pre lang="typescript">
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);
</pre>
</td>
</tr>
</table>

* `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:
<table>
<tr>
<td>
JavaScript
</td>
<td>
TypeScript
</td>
</tr>
<tr>
<td>
<pre lang="javascript">
const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
</pre>
</td>
<td>
<pre lang="typescript">
### 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");
</pre>
</td>
</tr>
</table>
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<IExtensionData>} Information about installed extensions.
*/
installExtension(extensionName: string): Promise<IExtensionData>;
```
* 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<void>}
*/
uninstallExtension(extensionName: string): Promise<void>;
```
* 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<IExtensionData>[]} Array of promises, each is resolved with information about loaded extension.
*/
loadExtensions(): Promise<IExtensionData>[];
```
* 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.
Expand Down
35 changes: 35 additions & 0 deletions docs/man_pages/general/extension-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
extension install
==========

Usage | Synopsis
------|-------
General | `$ tns extension install <Extension>`

Installs specified extension. Each extension adds additional functionality that's accessible directly from NativeScript CLI.

### Attributes

* `<Extension>` is any of the following.
* A `<Name>` or `<Name>@<Version>` where `<Name>` is the name of a package that is published in the npm registry and `<Version>` is a valid version of this plugin.
* A `<Local Path>` to the directory which contains the extension, including its `package.json` file.
* A `<Local Path>` to a `.tar.gz` archive containing a directory with the extension and its `package.json` file.
* A `<URL>` which resolves to a `.tar.gz` archive containing a directory with the extension and its `package.json` file.
* A `<git Remote URL>` 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.
<% } %>
31 changes: 31 additions & 0 deletions docs/man_pages/general/extension-uninstall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
extension uninstall
==========

Usage | Synopsis
------|-------
General | `$ tns extension uninstall <Extension>`

Uninstalls specified extension. After that you will not be able to use the functionality that this extensions adds to NativeScript CLI.

### Attributes

* `<Extension>` 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.
<% } %>
25 changes: 25 additions & 0 deletions docs/man_pages/general/extension.md
Original file line number Diff line number Diff line change
@@ -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.
<% } %>
7 changes: 7 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
13 changes: 13 additions & 0 deletions lib/commands/extensibility/install-extension.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
24 changes: 24 additions & 0 deletions lib/commands/extensibility/list-extensions.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
14 changes: 14 additions & 0 deletions lib/commands/extensibility/uninstall-extension.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
Loading

0 comments on commit 6ad0cef

Please sign in to comment.