Skip to content

Commit 456a0d6

Browse files
Expose createProject to public API (#2483)
* Expose createProject to public API In order to allow creating a new NativeScript project when {N} CLI is required as a library, we should add createProject method to public API. This requires some modifications in method's implementation - we cannot rely on `$options` as it is not populated correctly when the CLI is used as a library. Pass object describing the project that we have to create. Fix unit tests accordingly and uncomment (and fix) all tests in project-service. Add unit tests that will check if a method is really exposed in public API. Add PublicAPI.md file, which describes all methods that can be used when using CLI as a library. * Require only modules we need from mobile-cli-lib
1 parent af70f1e commit 456a0d6

File tree

10 files changed

+330
-154
lines changed

10 files changed

+330
-154
lines changed

PublicAPI.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
Public API
2+
==
3+
4+
This document describes all methods that can be invoked when NativeScript CLI is required as library, i.e.
5+
6+
<table>
7+
<tr>
8+
<td>
9+
JavaScript
10+
</td>
11+
<td>
12+
TypeScript
13+
</td>
14+
</tr>
15+
<tr>
16+
<td>
17+
<pre lang="javascript">
18+
const tns = require("nativescript");
19+
</pre>
20+
</td>
21+
<td>
22+
<pre lang="typescript">
23+
import * as tns from "nativescript";
24+
</pre>
25+
</td>
26+
</tr>
27+
28+
</table>
29+
30+
## Module projectService
31+
32+
`projectService` modules allow you to create new NativeScript application.
33+
34+
* `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.:
35+
```TypeScript
36+
/**
37+
* Describes available settings when creating new NativeScript application.
38+
*/
39+
interface IProjectSettings {
40+
/**
41+
* Name of the newly created application.
42+
*/
43+
projectName: string;
44+
45+
/**
46+
* Selected template from which to create the project. If not specified, defaults to hello-world template.
47+
* Template can be any npm package, local dir, github url, .tgz file.
48+
* If it is set to `angular` or `ng`, default NativeScript Angular Hello World template will be used.
49+
* If it is set to `typescript` or `tsc`, default NativeScript TypeScript Hello World template will be used.
50+
*/
51+
template?: string;
52+
53+
/**
54+
* Application identifier for the newly created application. If not specified, defaults to org.nativescript.<projectName>.
55+
*/
56+
appId?: string;
57+
58+
/**
59+
* Path where the project will be created. If not specified, defaults to current working dir.
60+
*/
61+
pathToProject?: string;
62+
63+
/**
64+
* Defines if invalid application name can be used for project creation.
65+
*/
66+
force?: boolean;
67+
68+
/**
69+
* Defines whether the `npm install` command should be executed with `--ignore-scripts` option.
70+
* When it is passed, all scripts (postinstall for example) will not be executed.
71+
*/
72+
ignoreScripts?: boolean;
73+
}
74+
```
75+
76+
Sample usage:
77+
<table>
78+
<tr>
79+
<td>
80+
JavaScript
81+
</td>
82+
<td>
83+
TypeScript
84+
</td>
85+
</tr>
86+
<tr>
87+
<td>
88+
<pre lang="javascript">
89+
const projectSettings = {
90+
projectName: "my-ns-app",
91+
template: "ng",
92+
pathToProject: "/home/my-user/project-dir"
93+
};
94+
95+
tns.projectService.createProject(projectSettings)
96+
.then(() => console.log("Project successfully created."))
97+
.catch((err) => console.log("Unable to create project, reason: ", err);
98+
</pre>
99+
</td>
100+
<td>
101+
<pre lang="typescript">
102+
const projectSettings: IProjectSettings = {
103+
projectName: "my-ns-app",
104+
template: "ng",
105+
pathToProject: "/home/my-user/project-dir"
106+
};
107+
108+
tns.projectService.createProject(projectSettings)
109+
.then(() => console.log("Project successfully created."))
110+
.catch((err) => console.log("Unable to create project, reason: ", err);
111+
</pre>
112+
</td>
113+
</tr>
114+
</table>
115+
116+
## How to add a new method to Public API
117+
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.
118+
For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`.
119+
120+
More information how to add a method to public API is available [here](https://github.com/telerik/mobile-cli-lib#how-to-make-a-method-public).
121+
After that add each method that you've exposed to the tests in `tests/nativescript-cli-lib.ts` file. There you'll find an object describing each publicly available module and the methods that you can call.

lib/bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ $injector.require("nativescript-cli", "./nativescript-cli");
77

88
$injector.require("projectData", "./project-data");
99
$injector.require("projectDataService", "./services/project-data-service");
10-
$injector.require("projectService", "./services/project-service");
10+
$injector.requirePublic("projectService", "./services/project-service");
1111
$injector.require("androidProjectService", "./services/android-project-service");
1212
$injector.require("iOSProjectService", "./services/ios-project-service");
1313
$injector.require("iOSProvisionService", "./services/ios-provision-service");

lib/commands/create-project.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ export class CreateProjectCommand implements ICommand {
2323
selectedTemplate = this.$options.template;
2424
}
2525

26-
await this.$projectService.createProject(args[0], selectedTemplate);
26+
await this.$projectService.createProject({
27+
projectName: args[0],
28+
template: selectedTemplate,
29+
appId: this.$options.appid,
30+
pathToProject: this.$options.path,
31+
force: this.$options.force,
32+
ignoreScripts: this.$options.ignoreScripts
33+
});
2734
}
2835
}
2936

lib/definitions/project.d.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,49 @@
1+
/**
2+
* Describes available settings when creating new NativeScript application.
3+
*/
4+
interface IProjectSettings {
5+
/**
6+
* Name of the newly created application.
7+
*/
8+
projectName: string;
9+
10+
/**
11+
* Selected template from which to create the project. If not specified, defaults to hello-world template.
12+
* Template can be any npm package, local dir, github url, .tgz file.
13+
* If it is set to `angular` or `ng`, default NativeScript Angular Hello World template will be used.
14+
* If it is set to `typescript` or `tsc`, default NativeScript TypeScript Hello World template will be used.
15+
*/
16+
template?: string;
17+
18+
/**
19+
* Application identifier for the newly created application. If not specified, defaults to org.nativescript.<projectName>.
20+
*/
21+
appId?: string;
22+
23+
/**
24+
* Path where the project will be created. If not specified, defaults to current working dir.
25+
*/
26+
pathToProject?: string;
27+
28+
/**
29+
* Defines if invalid application name can be used for project creation.
30+
*/
31+
force?: boolean;
32+
33+
/**
34+
* Defines whether the `npm install` command should be executed with `--ignore-scripts` option.
35+
* When it is passed, all scripts (postinstall for example) will not be executed.
36+
*/
37+
ignoreScripts?: boolean;
38+
}
139

240
interface IProjectService {
3-
createProject(projectName: string, selectedTemplate?: string): Promise<void>;
41+
/**
42+
* Creates new NativeScript application.
43+
* @param {any} projectSettings Options describing new project - its name, appId, path and template from which to be created.
44+
* @returns {Promise<void>}
45+
*/
46+
createProject(projectSettings: IProjectSettings): Promise<void>;
447
}
548

649
interface IProjectData {

lib/nativescript-cli-lib-bootstrap.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ $injector.overrideAlreadyRequiredModule = true;
55
// Temporary!!! Should not require appbuilder's entry point of mobile-cli-lib,
66
// but once we separate logics in mobile-cli-lib, we should be able to require only specific bootstrap.
77
// Use this hack for now, as this will allow requiring {N} CLI as library directly and executing some device specific operations.
8-
require("./common/appbuilder/proton-bootstrap");
8+
$injector.requirePublic("companionAppsService", "./common/appbuilder/services/livesync/companion-apps-service");
9+
$injector.requirePublicClass("deviceEmitter", "./common/appbuilder/device-emitter");
10+
$injector.requirePublicClass("deviceLogProvider", "./common/appbuilder/device-log-provider");

lib/services/project-service.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as constants from "../constants";
22
import * as path from "path";
33
import * as shelljs from "shelljs";
4+
import { exportedPromise } from "../common/decorators";
45

56
export class ProjectService implements IProjectService {
67

@@ -11,22 +12,29 @@ export class ProjectService implements IProjectService {
1112
private $projectDataService: IProjectDataService,
1213
private $projectHelper: IProjectHelper,
1314
private $projectNameService: IProjectNameService,
14-
private $projectTemplatesService: IProjectTemplatesService,
15-
private $options: IOptions) { }
15+
private $projectTemplatesService: IProjectTemplatesService) { }
16+
17+
@exportedPromise("projectService")
18+
public async createProject(projectOptions: IProjectSettings): Promise<void> {
19+
let projectName = projectOptions.projectName,
20+
selectedTemplate = projectOptions.template;
1621

17-
public async createProject(projectName: string, selectedTemplate?: string): Promise<void> {
1822
if (!projectName) {
1923
this.$errors.fail("You must specify <App name> when creating a new project.");
2024
}
21-
projectName = await this.$projectNameService.ensureValidName(projectName, { force: this.$options.force });
2225

23-
let projectDir = path.join(path.resolve(this.$options.path || "."), projectName);
26+
projectName = await this.$projectNameService.ensureValidName(projectName, { force: projectOptions.force });
27+
28+
const selectedPath = path.resolve(projectOptions.pathToProject || ".");
29+
const projectDir = path.join(selectedPath, projectName);
30+
2431
this.$fs.createDirectory(projectDir);
32+
2533
if (this.$fs.exists(projectDir) && !this.$fs.isEmptyDir(projectDir)) {
2634
this.$errors.fail("Path already exists and is not empty %s", projectDir);
2735
}
2836

29-
let projectId = this.$options.appid || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);
37+
let projectId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);
3038
this.createPackageJson(projectDir, projectId);
3139

3240
this.$logger.trace(`Creating a new NativeScript project with name ${projectName} and id ${projectId} at location ${projectDir}`);
@@ -45,7 +53,7 @@ export class ProjectService implements IProjectService {
4553
this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); //merging dependencies from template (dev && prod)
4654
this.removeMergedDependencies(projectDir, templatePackageJsonData);
4755

48-
await this.$npm.install(projectDir, projectDir, { "ignore-scripts": this.$options.ignoreScripts });
56+
await this.$npm.install(projectDir, projectDir, { "ignore-scripts": projectOptions.ignoreScripts });
4957

5058
let templatePackageJson = this.$fs.readJson(path.join(templatePath, "package.json"));
5159
await this.$npm.uninstall(templatePackageJson.name, { save: true }, projectDir);

test/nativescript-cli-lib.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,34 @@ describe("nativescript-cli-lib", () => {
1212
assert.deepEqual(jsonContent.main, expectedEntryPoint);
1313
});
1414

15-
it("resolves publicly available module - deviceEmitter, when it is required", () => {
16-
const pathToEntryPoint = path.join(__dirname, "..", "lib", "nativescript-cli-lib.js").replace(/\\/g, "\\\\");
17-
// HACK: If we try to require the entry point directly, the below code will fail as mocha requires all test files before starting the tests.
18-
// When the files are required, $injector.register adds each dependency to $injector's cache.
19-
// For example $injector.register("errors", Errors) will add the errors module with its resolver (Errors) to $injector's cache.
20-
// Calling $injector.require("errors", <path to errors file>), that's executed in our bootstrap, will fail, as the module errors is already in the cache.
21-
// In order to workaround this problem, start new process and assert there. This way all files will not be required in it and $injector.require(...) will work correctly.
22-
childProcess.execSync(`"${process.execPath}" ${nodeArgs.join(" ")} -e "var assert = require('chai').assert; var result = require('${pathToEntryPoint}'); assert.ok(result.deviceEmitter);"`);
15+
const publicApi: any = {
16+
deviceEmitter: null,
17+
projectService: ["createProject"]
18+
};
19+
20+
const pathToEntryPoint = path.join(__dirname, "..", "lib", "nativescript-cli-lib.js").replace(/\\/g, "\\\\");
21+
22+
_.each(publicApi, (methods: string[], moduleName: string) => {
23+
24+
it(`resolves publicly available module - ${moduleName}${methods && methods.length ? " and its publicly available methods: " + methods.join(", ") : ""}`, () => {
25+
// HACK: If we try to require the entry point directly, the below code will fail as mocha requires all test files before starting the tests.
26+
// When the files are required, $injector.register adds each dependency to $injector's cache.
27+
// For example $injector.register("errors", Errors) will add the errors module with its resolver (Errors) to $injector's cache.
28+
// Calling $injector.require("errors", <path to errors file>), that's executed in our bootstrap, will fail, as the module errors is already in the cache.
29+
// In order to workaround this problem, start new process and assert there. This way all files will not be required in it and $injector.require(...) will work correctly.
30+
let testMethod = `"${process.execPath}" ${nodeArgs.join(" ")} -e "` +
31+
"var assert = require('chai').assert;" +
32+
`var result = require('${pathToEntryPoint}');` +
33+
`assert.ok(result.${moduleName});`;
34+
35+
_.each(methods, method => {
36+
testMethod += `assert.ok(result.${moduleName}.${method});`;
37+
});
38+
39+
testMethod += '"'; // Really important - close the " of node -e ""
40+
41+
childProcess.execSync(testMethod);
42+
});
43+
2344
});
2445
});

test/project-commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ let isProjectCreated: boolean;
1010
let dummyArgs = ["dummyArgsString"];
1111

1212
class ProjectServiceMock implements IProjectService {
13-
async createProject(projectName: string, selectedTemplate?: string): Promise<void> {
14-
selectedTemplateName = selectedTemplate;
13+
async createProject(projectOptions: IProjectSettings): Promise<void> {
14+
selectedTemplateName = projectOptions.template;
1515
isProjectCreated = true;
1616
}
1717
}

0 commit comments

Comments
 (0)