Skip to content

Commit 44727d8

Browse files
Merge pull request #3394 from NativeScript/vladimirov/extensions-lookup
feat(extensibility): Add hint how to get access to a command from extension
2 parents 00e7a30 + e411ca5 commit 44727d8

10 files changed

+404
-6
lines changed

lib/declarations.d.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ interface INodePackageManager {
3232
* @return {Promise<string>} The output of the uninstallation.
3333
*/
3434
search(filter: string[], config: IDictionary<string | boolean>): Promise<string>;
35+
36+
/**
37+
* Searches for npm packages in npms by keyword.
38+
* @param {string} keyword The keyword based on which the search action will be executed.
39+
* @returns {INpmsResult} The information about found npm packages.
40+
*/
41+
searchNpms(keyword: string): Promise<INpmsResult>;
42+
43+
/**
44+
* Gets information for a specified package from registry.npmjs.org.
45+
* @param {string} packageName The name of the package.
46+
* @returns {any} The full data from registry.npmjs.org for this package.
47+
*/
48+
getRegistryPackageData(packageName: string): Promise<any>;
3549
}
3650

3751
interface INpmInstallationManager {
@@ -306,6 +320,57 @@ interface IDependencyData {
306320
dependencies?: string[];
307321
}
308322

323+
interface INpmsResult {
324+
total: number;
325+
results: INpmsSingleResultData[];
326+
}
327+
328+
interface INpmsSingleResultData {
329+
package: INpmsPackageData;
330+
flags: INpmsFlags;
331+
score: INpmsScore;
332+
searchScore: number;
333+
}
334+
335+
interface INpmsPackageData {
336+
name: string;
337+
// unscoped in case package is not in a scope
338+
// scope name in case part of a scope "angular" for example for @angular/core
339+
scope: string;
340+
version: string;
341+
description: string;
342+
keywords: string[];
343+
date: string;
344+
links: { npm: string };
345+
author: { name: string };
346+
publisher: INpmsUser;
347+
maintainers: INpmsUser[];
348+
}
349+
350+
interface IUsername {
351+
username: string;
352+
}
353+
354+
interface INpmsUser extends IUsername {
355+
email: string;
356+
}
357+
358+
interface INpmsFlags {
359+
unstable: boolean;
360+
insecure: number;
361+
// Describes the reason for deprecation.
362+
deprecated: string;
363+
}
364+
365+
interface INpmsScore {
366+
final: number;
367+
detail: {
368+
quality: number;
369+
popularity: number;
370+
maintenance: number;
371+
}
372+
}
373+
309374
interface IStaticConfig extends Config.IStaticConfig { }
310375

311376
interface IConfiguration extends Config.IConfig {

lib/node-package-manager.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export class NodePackageManager implements INodePackageManager {
1010
private $hostInfo: IHostInfo,
1111
private $errors: IErrors,
1212
private $childProcess: IChildProcess,
13-
private $logger: ILogger) { }
13+
private $logger: ILogger,
14+
private $httpClient: Server.IHttpClient) { }
1415

1516
@exported("npm")
1617
public async install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise<INpmInstallResultInfo> {
@@ -110,6 +111,23 @@ export class NodePackageManager implements INodePackageManager {
110111
return JSON.parse(viewResult);
111112
}
112113

114+
public async searchNpms(keyword: string): Promise<INpmsResult> {
115+
// TODO: Fix the generation of url - in case it contains @ or / , the call may fail.
116+
const httpRequestResult = await this.$httpClient.httpRequest(`https://api.npms.io/v2/search?q=keywords:${keyword}`);
117+
const result: INpmsResult = JSON.parse(httpRequestResult.body);
118+
return result;
119+
}
120+
121+
public async getRegistryPackageData(packageName: string): Promise<any> {
122+
const url = `https://registry.npmjs.org/${packageName}`;
123+
this.$logger.trace(`Trying to get data from npm registry for package ${packageName}, url is: ${url}`);
124+
const responseData = (await this.$httpClient.httpRequest(url)).body;
125+
this.$logger.trace(`Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}`);
126+
const jsonData = JSON.parse(responseData);
127+
this.$logger.trace(`Successfully parsed data from npm registry for package ${packageName}.`);
128+
return jsonData;
129+
}
130+
113131
private getNpmExecutableName(): string {
114132
let npmExecutableName = "npm";
115133

lib/services/extensibility-service.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from "path";
22
import { cache, exported } from "../common/decorators";
33
import * as constants from "../constants";
4+
import { createRegExp, regExpEscape } from "../common/helpers";
45

56
export class ExtensibilityService implements IExtensibilityService {
67
private get pathToExtensions(): string {
@@ -107,6 +108,63 @@ export class ExtensibilityService implements IExtensibilityService {
107108
}
108109
}
109110

111+
public async getExtensionNameWhereCommandIsRegistered(inputOpts: IGetExtensionCommandInfoParams): Promise<IExtensionCommandInfo> {
112+
let allExtensions: INpmsSingleResultData[] = [];
113+
114+
try {
115+
const npmsResult = await this.$npm.searchNpms("nativescript:extension");
116+
allExtensions = npmsResult.results || [];
117+
} catch (err) {
118+
this.$logger.trace(`Unable to find extensions via npms. Error is: ${err}`);
119+
return null;
120+
}
121+
122+
const defaultCommandRegExp = new RegExp(`${regExpEscape(inputOpts.defaultCommandDelimiter)}.*`);
123+
const commandDelimiterRegExp = createRegExp(inputOpts.commandDelimiter, "g");
124+
125+
for (const extensionData of allExtensions) {
126+
const extensionName = extensionData.package.name;
127+
128+
try {
129+
// now get full package.json for the latest version of the package
130+
const registryData = await this.$npm.getRegistryPackageData(extensionName);
131+
const latestPackageData = registryData.versions[registryData["dist-tags"].latest];
132+
const commands: string[] = latestPackageData && latestPackageData.nativescript && latestPackageData.nativescript.commands;
133+
if (commands && commands.length) {
134+
// For each default command we need to add its short syntax in the array of commands.
135+
// For example in case there's a default command called devices list, the commands array will contain devices|*list.
136+
// However, in case the user executes just tns devices, CLI will still execute the tns devices list command.
137+
// So we need to add the devices command as well.
138+
_.filter(commands, command => command.indexOf(inputOpts.defaultCommandDelimiter) !== -1)
139+
.forEach(defaultCommand => {
140+
commands.push(defaultCommand.replace(defaultCommandRegExp, ""));
141+
});
142+
143+
const copyOfFullArgs = _.clone(inputOpts.inputStrings);
144+
while (copyOfFullArgs.length) {
145+
const currentCommand = copyOfFullArgs.join(inputOpts.commandDelimiter).toLowerCase();
146+
147+
if (_.some(commands, c => c.toLowerCase() === currentCommand)) {
148+
const beautifiedCommandName = currentCommand.replace(commandDelimiterRegExp, " ");
149+
return {
150+
extensionName,
151+
registeredCommandName: currentCommand,
152+
installationMessage: `The command ${beautifiedCommandName} is registered in extension ${extensionName}. You can install it by executing 'tns extension install ${extensionName}'`
153+
};
154+
}
155+
156+
copyOfFullArgs.splice(-1, 1);
157+
}
158+
}
159+
} catch (err) {
160+
// We do not want to stop the whole process in case we are unable to find data for one of the extensions.
161+
this.$logger.trace(`Unable to get data for ${extensionName}. Error is: ${err}`);
162+
}
163+
}
164+
165+
return null;
166+
}
167+
110168
private getPathToExtension(extensionName: string): string {
111169
return path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME, extensionName);
112170
}

test/ios-project-service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function createTestInjector(projectPath: string, projectName: string): IInjector
117117
testInjector.register("npm", NodePackageManager);
118118
testInjector.register("xCConfigService", XCConfigService);
119119
testInjector.register("settingsService", SettingsService);
120+
testInjector.register("httpClient", {});
120121

121122
return testInjector;
122123
}

test/npm-support.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ function createTestInjector(): IInjector {
9696
message: (): void => undefined
9797
})
9898
});
99+
testInjector.register("httpClient", {});
99100

100101
return testInjector;
101102
}

test/platform-commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ function createTestInjector() {
160160
message: (): void => undefined
161161
})
162162
});
163+
testInjector.register("extensibilityService", {});
163164

164165
return testInjector;
165166
}

test/plugins-service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ function createTestInjector() {
110110
message: (): void => undefined
111111
})
112112
});
113-
113+
testInjector.register("httpClient", {});
114+
testInjector.register("extensibilityService", {});
114115
return testInjector;
115116
}
116117

test/project-service.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import * as ProjectHelperLib from "../lib/common/project-helper";
99
import { StaticConfig } from "../lib/config";
1010
import * as NpmLib from "../lib/node-package-manager";
1111
import { NpmInstallationManager } from "../lib/npm-installation-manager";
12-
import * as HttpClientLib from "../lib/common/http-client";
1312
import { FileSystem } from "../lib/common/file-system";
1413
import * as path from "path";
1514
import temp = require("temp");
@@ -145,7 +144,7 @@ class ProjectIntegrationTest {
145144

146145
this.testInjector.register("npmInstallationManager", NpmInstallationManager);
147146
this.testInjector.register("npm", NpmLib.NodePackageManager);
148-
this.testInjector.register("httpClient", HttpClientLib.HttpClient);
147+
this.testInjector.register("httpClient", {});
149148

150149
this.testInjector.register("options", Options);
151150
this.testInjector.register("hostInfo", HostInfo);

0 commit comments

Comments
 (0)