Skip to content

Commit 963d483

Browse files
feat(extensions): Allow generation of help content for extension commands
Each CLI extension may add new commands to CLI. Allow showing command help for these commands in case: 1. Commmand execution fails. 2. Users need more information for command - `tns <command> --help` 3. Users need full html help for command - `tns help <command>`. Each extension that wants to use this functionality will have to add `nativescript` key in its `package.json` and add `docs` key in it. The `docs` key must point to the directory where the help content (.md files) is located, relative to the root directory of the extension. When CLI needs to show command line help for extension's command, it will search the docs directories of all extensions. When CLI needs to show HTML help for the extension's commands, it will generate `html` directory right next to the docs dir in the extension. Move extensibility.d.ts from {N} to mobile-cli-lib just to ensure correct transpilation.
1 parent 3064e0d commit 963d483

File tree

4 files changed

+94
-107
lines changed

4 files changed

+94
-107
lines changed

lib/definitions/extensibility.d.ts

-56
This file was deleted.

lib/services/extensibility-service.ts

+43-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@ export class ExtensibilityService implements IExtensibilityService {
3535
const installResultInfo = await this.$npm.install(packageName, this.pathToExtensions, npmOpts);
3636
this.$logger.trace(`Finished installation of extension '${extensionName}'. Trying to load it now.`);
3737

38-
return { extensionName: installResultInfo.name };
38+
const packageJsonData = this.getExtensionPackageJsonData(installResultInfo.name);
39+
40+
const pathToExtension = this.getPathToExtension(extensionName);
41+
const docs = packageJsonData && packageJsonData.nativescript && packageJsonData.nativescript.docs && path.join(pathToExtension, packageJsonData.nativescript.docs);
42+
return {
43+
extensionName: installResultInfo.name,
44+
version: installResultInfo.version,
45+
docs,
46+
pathToExtension
47+
};
3948
}
4049

4150
@exported("extensibilityService")
@@ -49,8 +58,15 @@ export class ExtensibilityService implements IExtensibilityService {
4958
this.$logger.trace(`Finished uninstallation of extension '${extensionName}'.`);
5059
}
5160

61+
public getInstalledExtensionsData(): IExtensionData[] {
62+
const installedExtensions = this.getInstalledExtensions();
63+
return _.keys(installedExtensions).map(installedExtension => {
64+
return this.getInstalledExtensionData(installedExtension);
65+
});
66+
}
67+
5268
@exported("extensibilityService")
53-
public loadExtensions(): Promise<any>[] {
69+
public loadExtensions(): Promise<IExtensionData>[] {
5470
this.$logger.trace("Loading extensions.");
5571

5672
let dependencies: IStringDictionary = null;
@@ -74,14 +90,26 @@ export class ExtensibilityService implements IExtensibilityService {
7490
return null;
7591
}
7692

93+
private getInstalledExtensionData(extensionName: string): IExtensionData {
94+
const packageJsonData = this.getExtensionPackageJsonData(extensionName);
95+
const pathToExtension = this.getPathToExtension(extensionName);
96+
const docs = packageJsonData && packageJsonData.nativescript && packageJsonData.nativescript.docs && path.join(pathToExtension, packageJsonData.nativescript.docs);
97+
return {
98+
extensionName: packageJsonData.name,
99+
version: packageJsonData.version,
100+
docs,
101+
pathToExtension
102+
};
103+
}
104+
77105
@exported("extensibilityService")
78106
public async loadExtension(extensionName: string): Promise<IExtensionData> {
79107
try {
80108
await this.assertExtensionIsInstalled(extensionName);
81109

82-
const pathToExtension = path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME, extensionName);
110+
const pathToExtension = this.getPathToExtension(extensionName);
83111
this.$requireService.require(pathToExtension);
84-
return { extensionName };
112+
return this.getInstalledExtensionData(extensionName);
85113
} catch (error) {
86114
this.$logger.warn(`Error while loading ${extensionName} is: ${error.message}`);
87115
const err = <IExtensionLoadingError>new Error(`Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds. Error: ${error.message}`);
@@ -90,6 +118,17 @@ export class ExtensibilityService implements IExtensibilityService {
90118
}
91119
}
92120

121+
private getPathToExtension(extensionName: string): string {
122+
return path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME, extensionName);
123+
}
124+
125+
private getExtensionPackageJsonData(extensionName: string): any {
126+
const pathToExtension = this.getPathToExtension(extensionName);
127+
const pathToPackageJson = path.join(pathToExtension, constants.PACKAGE_JSON_FILE_NAME);
128+
const jsonData = this.$fs.readJson(pathToPackageJson);
129+
return jsonData;
130+
}
131+
93132
private async assertExtensionIsInstalled(extensionName: string): Promise<void> {
94133
this.$logger.trace(`Asserting extension ${extensionName} is installed.`);
95134
const installedExtensions = this.$fs.readDirectory(path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME));

test/services/extensibility-service.ts

+50-46
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ describe("extensibilityService", () => {
1818

1919
const getTestInjector = (): IInjector => {
2020
const testInjector = new Yok();
21-
testInjector.register("fs", {});
21+
testInjector.register("fs", {
22+
readJson: (pathToFile: string): any => ({})
23+
});
2224
testInjector.register("logger", stubs.LoggerStub);
2325
testInjector.register("npm", {});
2426
testInjector.register("settingsService", SettingsService);
@@ -28,6 +30,33 @@ describe("extensibilityService", () => {
2830
return testInjector;
2931
};
3032

33+
const getExpectedInstallationPathForExtension = (testInjector: IInjector, extensionName: string): string => {
34+
const settingsService = testInjector.resolve<ISettingsService>("settingsService");
35+
const profileDir = settingsService.getProfileDir();
36+
37+
return path.join(profileDir, "extensions", "node_modules", extensionName);
38+
};
39+
40+
const mockFsReadJson = (testInjector: IInjector, extensionNames: string[]): void => {
41+
const fs = testInjector.resolve<IFileSystem>("fs");
42+
fs.readJson = (filename: string, encoding?: string): any => {
43+
const extensionName = _.find(extensionNames, extName => filename.indexOf(extName) !== -1);
44+
if (extensionName) {
45+
return {
46+
name: extensionName,
47+
version: "1.0.0"
48+
};
49+
}
50+
51+
const dependencies: any = {};
52+
_.each(extensionNames, name => {
53+
dependencies[name] = "1.0.0";
54+
});
55+
56+
return { dependencies };
57+
};
58+
};
59+
3160
describe("installExtension", () => {
3261
describe("fails", () => {
3362
it("when extensions dir does not exist and trying to create it fails", async () => {
@@ -133,17 +162,20 @@ describe("extensibilityService", () => {
133162
it("returns the name of the installed extension", async () => {
134163
const extensionName = "extension1";
135164
const testInjector = getTestInjector();
165+
136166
const fs: IFileSystem = testInjector.resolve("fs");
137167
fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionName;
138168

139169
fs.readDirectory = (dir: string): string[] => [extensionName];
140170

171+
fs.readJson = () => ({ name: extensionName, version: "1.0.0" });
172+
141173
const npm: INodePackageManager = testInjector.resolve("npm");
142-
npm.install = async (packageName: string, pathToSave: string, config?: any): Promise<any> => ({ name: extensionName });
174+
npm.install = async (packageName: string, pathToSave: string, config?: any): Promise<any> => ({ name: extensionName, version: "1.0.0" });
143175

144176
const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService);
145177
const actualResult = await extensibilityService.installExtension(extensionName);
146-
assert.deepEqual(actualResult, { extensionName });
178+
assert.deepEqual(actualResult, { extensionName, version: "1.0.0", docs: undefined, pathToExtension: getExpectedInstallationPathForExtension(testInjector, extensionName) });
147179
});
148180
});
149181

@@ -160,16 +192,16 @@ describe("extensibilityService", () => {
160192
return extensionNames;
161193
};
162194

163-
fs.readJson = (filename: string, encoding?: string): any => {
164-
const dependencies: any = {};
165-
_.each(extensionNames, name => {
166-
dependencies[name] = "1.0.0";
167-
});
168-
169-
return { dependencies };
170-
};
195+
mockFsReadJson(testInjector, extensionNames);
171196

172-
const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => ({ extensionName }));
197+
const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => (
198+
{
199+
extensionName,
200+
version: "1.0.0",
201+
pathToExtension: getExpectedInstallationPathForExtension(testInjector, extensionName),
202+
docs: undefined
203+
}
204+
));
173205

174206
const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService);
175207
const actualResult = await Promise.all(extensibilityService.loadExtensions());
@@ -194,14 +226,7 @@ describe("extensibilityService", () => {
194226
}
195227
};
196228

197-
fs.readJson = (filename: string, encoding?: string): any => {
198-
const dependencies: any = {};
199-
_.each(extensionNames, name => {
200-
dependencies[name] = "1.0.0";
201-
});
202-
203-
return { dependencies };
204-
};
229+
mockFsReadJson(testInjector, extensionNames);
205230

206231
let isNpmInstallCalled = false;
207232
const npm: INodePackageManager = testInjector.resolve("npm");
@@ -211,7 +236,7 @@ describe("extensibilityService", () => {
211236
return { name: packageName };
212237
};
213238

214-
const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => ({ extensionName }));
239+
const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => ({ extensionName, version: "1.0.0", pathToExtension: getExpectedInstallationPathForExtension(testInjector, extensionName), docs: undefined }));
215240

216241
const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService);
217242
const actualResult = await Promise.all(extensibilityService.loadExtensions());
@@ -230,14 +255,7 @@ describe("extensibilityService", () => {
230255
return extensionNames;
231256
};
232257

233-
fs.readJson = (filename: string, encoding?: string): any => {
234-
const dependencies: any = {};
235-
_.each(extensionNames, name => {
236-
dependencies[name] = "1.0.0";
237-
});
238-
239-
return { dependencies };
240-
};
258+
mockFsReadJson(testInjector, extensionNames);
241259

242260
const requireService: IRequireService = testInjector.resolve("requireService");
243261
requireService.require = (module: string) => {
@@ -246,7 +264,7 @@ describe("extensibilityService", () => {
246264
}
247265
};
248266

249-
const expectedResults: any[] = _.map(extensionNames, extensionName => ({ extensionName }));
267+
const expectedResults: any[] = _.map(extensionNames, extensionName => ({ extensionName, version: "1.0.0", pathToExtension: getExpectedInstallationPathForExtension(testInjector, extensionName), docs: undefined }));
250268
expectedResults[0] = new Error("Unable to load extension extension1. You will not be able to use the functionality that it adds. Error: Unable to load module.");
251269
const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService);
252270
const promises = extensibilityService.loadExtensions();
@@ -269,14 +287,7 @@ describe("extensibilityService", () => {
269287
const fs: IFileSystem = testInjector.resolve("fs");
270288
const expectedErrorMessage = `Unable to read ${constants.NODE_MODULES_FOLDER_NAME} dir.`;
271289
fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) === "extensions" || path.basename(pathToCheck) === constants.PACKAGE_JSON_FILE_NAME;
272-
fs.readJson = (filename: string, encoding?: string): any => {
273-
const dependencies: any = {};
274-
_.each(extensionNames, name => {
275-
dependencies[name] = "1.0.0";
276-
});
277-
278-
return { dependencies };
279-
};
290+
mockFsReadJson(testInjector, extensionNames);
280291

281292
let isReadDirCalled = false;
282293
fs.readDirectory = (dir: string): string[] => {
@@ -310,14 +321,7 @@ describe("extensibilityService", () => {
310321
"expected 'extension3' to deeply equal 'extension1'"];
311322
const fs: IFileSystem = testInjector.resolve("fs");
312323
fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) === "extensions" || path.basename(pathToCheck) === constants.PACKAGE_JSON_FILE_NAME;
313-
fs.readJson = (filename: string, encoding?: string): any => {
314-
const dependencies: any = {};
315-
_.each(extensionNames, name => {
316-
dependencies[name] = "1.0.0";
317-
});
318-
319-
return { dependencies };
320-
};
324+
mockFsReadJson(testInjector, extensionNames);
321325

322326
let isReadDirCalled = false;
323327
fs.readDirectory = (dir: string): string[] => {

0 commit comments

Comments
 (0)