Skip to content

Commit fc5dba3

Browse files
author
Dimitar Tachev
authored
Merge pull request #3969 from NativeScript/tachev/interactive-create
feat: interactive app creation
2 parents b6d6063 + ea8f056 commit fc5dba3

15 files changed

+468
-97
lines changed

docs/man_pages/project/creation/create.md

+30-10
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,49 @@ position: 1
77

88
Usage | Synopsis
99
---|---
10-
Create from default JavaScript template | `$ tns create <App Name> [--path <Directory>] [--appid <App ID>]`
11-
Create from default TypeScript template | `$ tns create <App Name> --template typescript [--path <Directory>] [--appid <App ID>]` OR `$ tns create <App Name> --tsc [--path <Directory>] [--appid <App ID>]` OR `$ tns create <App Name> --template tsc [--path <Directory>] [--appid <App ID>]`
12-
Create from default Angular template | `$ tns create <App Name> --template angular [--path <Directory>] [--appid <App ID>]` OR `$ tns create <App Name> --template ng [--path <Directory>] [--appid <App ID>]` OR `$ tns create <App Name> --ng [--path <Directory>] [--appid <App ID>]`
13-
Copy from existing project | `$ tns create <App Name> [--path <Directory>] [--appid <App ID>]`
14-
Create from custom template | `$ tns create <App Name> [--path <Directory>] [--appid <App ID>] --template <Template>`
10+
Create from default JavaScript template | `$ tns create [<App Name>] [--js] [--path <Directory>] [--appid <App ID>]`
11+
Create from default TypeScript template | `$ tns create [<App Name>] --ts [--path <Directory>] [--appid <App ID>]`
12+
Create from default Angular template | `$ tns create [<App Name>] --ng [--path <Directory>] [--appid <App ID>]`
13+
Create from default Vue.js template | `$ tns create [<App Name>] --vue [--path <Directory>] [--appid <App ID>]`
14+
Create from custom template | `$ tns create [<App Name>] [--path <Directory>] [--appid <App ID>] --template <Template>`
1515

16-
Creates a new project for native development with NativeScript.
16+
Interactively creates a new NativeScript app.
1717

1818
### Options
1919
* `--path` - Specifies the directory where you want to create the project, if different from the current directory. The directory must be empty.
2020
* `--appid` - Sets the application identifier for your project.
21-
* `--template` - Specifies a valid npm package which you want to use to create your project. If `--template` is not set, the NativeScript CLI creates the project from the default JavaScript hello-world template.<% if(isHtml) { %> If one or more application assets are missing from the `App_Resources` directory in the package, the CLI adds them using the assets available in the default hello-world template.<% } %>
22-
* `--ng` - Sets the template for your project to the Angular template.
23-
* `--tsc` - Sets the template for your project to the TypeScript template.
21+
* `--template` - Specifies a valid npm package which you want to use to create your project. If `--template` is not set, the NativeScript CLI will ask you to pick one from a predefined list afterwards.<% if(isHtml) { %> If one or more application assets are missing from the `App_Resources` directory in the package, the CLI adds them using the assets available in the default hello-world template.<% } %>
22+
* `--js`, `--javascript` - Sets the template for your project to the JavaScript template.
23+
* `--ts`, `--tsc`, `--typescript` - Sets the template for your project to the TypeScript template.
24+
* `--ng`, `--angular` - Sets the template for your project to the Angular template.
25+
* `--vue`, `--vuejs` - Sets the template for your project to the Vue.js template.
2426

2527
### Attributes
26-
* `<App Name>` is the name of project. The specified name must meet the requirements of all platforms that you want to target. <% if(isConsole) { %>For more information about the `<App Name>` requirements, run `$ tns help create`<% } %><% if(isHtml) { %>For projects that target Android, you can use uppercase or lowercase letters, numbers, and underscores. The name must start with a letter.
28+
* `<App Name>` is the name of project. The specified name must meet the requirements of all platforms that you want to target. If not specified, the NativeScript CLI will ask you for it afterwards. <% if(isConsole) { %>For more information about the `<App Name>` requirements, run `$ tns help create`<% } %><% if(isHtml) { %>For projects that target Android, you can use uppercase or lowercase letters, numbers, and underscores. The name must start with a letter.
2729
For projects that target iOS, you can use uppercase or lowercase letters, numbers, and hyphens.<% } %>
2830
* `<App ID>` is the application identifier for your project. It must be a domain name in reverse and must meet the requirements of all platforms that you want to target. If not specified, the application identifier is set to `org.nativescript.<App name>` <% if(isConsole) { %>For more information about the `<App ID>` requirements, run `$ tns help create`<% } %><% if(isHtml) { %>For projects that target Android, you can use uppercase or lowercase letters, numbers, and underscores in the strings of the reversed domain name, separated by a dot. Strings must be separated by a dot and must start with a letter. For example: `com.nativescript.My_Andro1d_App`
2931
For projects that target iOS, you can use uppercase or lowercase letters, numbers, and hyphens in the strings of the reversed domain name. Strings must be separated by a dot. For example: `com.nativescript.My-i0s-App`.
3032
* `<Template>` is a valid npm package which you want to use as template for your app. You can specify the package by name in the npm registry or by local path or GitHub URL to a directory or .tar.gz containing a package.json file. The contents of the package will be copied to the `app` directory of your project.<% } %>
3133

3234
<% if(isHtml) { %>
35+
36+
### Templates Usage
37+
38+
Based on the selected options, the NativeScript CLI will use the project templates below:
39+
40+
Selected Option | Template
41+
----------|----------
42+
`Plain JavaScript - Hello World`, `--js`, `--javascript` | tns-template-hello-world
43+
`Plain JavaScript - SideDrawer` | tns-template-drawer-navigation
44+
`Plain JavaScript - Tabs` | tns-template-tab-navigation
45+
`Plain TypeScript - Hello World`, `--ts`, `--tsc`, `--typescript` | tns-template-hello-world-ts
46+
`Plain TypeScript - SideDrawer` | tns-template-drawer-navigation-ts
47+
`Plain TypeScript - Tabs` | tns-template-tab-navigation-ts
48+
`Angular - Hello World`, `--ng`, `--angular` | tns-template-hello-world-ng
49+
`Angular - SideDrawer` | tns-template-drawer-navigation-ng
50+
`Angular - Tabs` | tns-template-tab-navigation-ng
51+
`Vue.js`, `--vue`, `--vuejs` | tns-template-blank-vue
52+
3353
### Related Commands
3454

3555
Command | Description

lib/commands/create-project.ts

+171-10
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,205 @@
11
import * as constants from "../constants";
22
import * as path from "path";
3+
import { isInteractive } from "../common/helpers";
34

45
export class CreateProjectCommand implements ICommand {
56
public enableHooks = false;
6-
public allowedParameters: ICommandParameter[] = [this.$stringParameterBuilder.createMandatoryParameter("Project name cannot be empty.")];
7+
public allowedParameters: ICommandParameter[] = [this.$stringParameter];
8+
private static HelloWorldTemplateKey = "Hello World";
9+
private static HelloWorldTemplateDescription = "A Hello World app";
10+
private static DrawerTemplateKey = "SideDrawer";
11+
private static DrawerTemplateDescription = "An app with pre-built pages that uses a drawer for navigation";
12+
private static TabsTemplateKey = "Tabs";
13+
private static TabsTemplateDescription = "An app with pre-built pages that uses tabs for navigation";
14+
private isInteractionIntroShown = false;
715

8-
private createdProjecData: ICreateProjectData;
16+
private createdProjectData: ICreateProjectData;
917

1018
constructor(private $projectService: IProjectService,
1119
private $logger: ILogger,
1220
private $errors: IErrors,
1321
private $options: IOptions,
14-
private $stringParameterBuilder: IStringParameterBuilder) { }
22+
private $prompter: IPrompter,
23+
private $stringParameter: ICommandParameter) { }
1524

1625
public async execute(args: string[]): Promise<void> {
17-
if ((this.$options.tsc || this.$options.ng) && this.$options.template) {
18-
this.$errors.fail("You cannot use --ng or --tsc options together with --template.");
26+
const interactiveAdverbs = ["First", "Next", "Finally"];
27+
const getNextInteractiveAdverb = () => {
28+
return interactiveAdverbs.shift() || "Next";
29+
};
30+
31+
if ((this.$options.tsc || this.$options.ng || this.$options.vue || this.$options.js) && this.$options.template) {
32+
this.$errors.fail("You cannot use a flavor option like --ng, --vue, --tsc and --js together with --template.");
1933
}
2034

35+
let projectName = args[0];
2136
let selectedTemplate: string;
22-
if (this.$options.tsc) {
37+
if (this.$options.js) {
38+
selectedTemplate = constants.JAVASCRIPT_NAME;
39+
} else if (this.$options.tsc) {
2340
selectedTemplate = constants.TYPESCRIPT_NAME;
2441
} else if (this.$options.ng) {
2542
selectedTemplate = constants.ANGULAR_NAME;
43+
} else if (this.$options.vue) {
44+
selectedTemplate = constants.VUE_NAME;
2645
} else {
2746
selectedTemplate = this.$options.template;
2847
}
2948

30-
this.createdProjecData = await this.$projectService.createProject({
31-
projectName: args[0],
49+
if (!projectName && isInteractive()) {
50+
this.printInteractiveCreationIntroIfNeeded();
51+
projectName = await this.$prompter.getString(`${getNextInteractiveAdverb()}, what will be the name of your app?`, { allowEmpty: false });
52+
this.$logger.info();
53+
}
54+
55+
projectName = await this.$projectService.validateProjectName({ projectName: projectName, force: this.$options.force, pathToProject: this.$options.path });
56+
57+
if (!selectedTemplate && isInteractive()) {
58+
this.printInteractiveCreationIntroIfNeeded();
59+
selectedTemplate = await this.interactiveFlavorAndTemplateSelection(getNextInteractiveAdverb(), getNextInteractiveAdverb());
60+
}
61+
62+
this.createdProjectData = await this.$projectService.createProject({
63+
projectName: projectName,
3264
template: selectedTemplate,
3365
appId: this.$options.appid,
3466
pathToProject: this.$options.path,
35-
force: this.$options.force,
67+
// its already validated above
68+
force: true,
3669
ignoreScripts: this.$options.ignoreScripts
3770
});
3871
}
3972

73+
private async interactiveFlavorAndTemplateSelection(flavorAdverb: string, templateAdverb: string) {
74+
const selectedFlavor = await this.interactiveFlavorSelection(flavorAdverb);
75+
const selectedTemplate: string = await this.interactiveTemplateSelection(selectedFlavor, templateAdverb);
76+
77+
return selectedTemplate;
78+
}
79+
80+
private async interactiveFlavorSelection(adverb: string) {
81+
const flavorSelection = await this.$prompter.promptForDetailedChoice(`${adverb}, which flavor would you like to use?`, [
82+
{ key: constants.NgFlavorName, description: "Learn more at https://angular.io/" },
83+
{ key: constants.VueFlavorName, description: "Learn more at https://vuejs.org/" },
84+
{ key: constants.TsFlavorName, description: "Learn more at https://www.typescriptlang.org/" },
85+
{ key: constants.JsFlavorName, description: "Learn more at https://www.javascript.com/" },
86+
]);
87+
return flavorSelection;
88+
}
89+
90+
private printInteractiveCreationIntroIfNeeded() {
91+
if (!this.isInteractionIntroShown) {
92+
this.isInteractionIntroShown = true;
93+
this.$logger.info();
94+
this.$logger.printMarkdown(`# Let’s create a NativeScript app!`);
95+
this.$logger.printMarkdown(`
96+
Answer the following questions to help us build the right app for you. (Note: you
97+
can skip this prompt next time using the --template option, or the --ng, --vue, --ts,
98+
or --js flags.)
99+
`);
100+
}
101+
}
102+
103+
private async interactiveTemplateSelection(flavorSelection: string, adverb: string) {
104+
const selectedFlavorTemplates: {
105+
key?: string;
106+
value: string;
107+
description?: string;
108+
}[] = [];
109+
let selectedTemplate: string;
110+
switch (flavorSelection) {
111+
case constants.NgFlavorName: {
112+
selectedFlavorTemplates.push(...this.getNgFlavors());
113+
break;
114+
}
115+
case constants.VueFlavorName: {
116+
selectedFlavorTemplates.push({ value: "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0" });
117+
break;
118+
}
119+
case constants.TsFlavorName: {
120+
selectedFlavorTemplates.push(...this.getTsTemplates());
121+
break;
122+
}
123+
case constants.JsFlavorName: {
124+
selectedFlavorTemplates.push(...this.getJsTemplates());
125+
break;
126+
}
127+
}
128+
if (selectedFlavorTemplates.length > 1) {
129+
this.$logger.info();
130+
const templateChoices = selectedFlavorTemplates.map((template) => {
131+
return { key: template.key, description: template.description };
132+
});
133+
const selectedTemplateKey = await this.$prompter.promptForDetailedChoice(`${adverb}, which template would you like to start from?`, templateChoices);
134+
selectedTemplate = selectedFlavorTemplates.find(t => t.key === selectedTemplateKey).value;
135+
} else {
136+
selectedTemplate = selectedFlavorTemplates[0].value;
137+
}
138+
return selectedTemplate;
139+
}
140+
141+
private getJsTemplates() {
142+
const templates = [{
143+
key: CreateProjectCommand.HelloWorldTemplateKey,
144+
value: constants.RESERVED_TEMPLATE_NAMES.javascript,
145+
description: CreateProjectCommand.HelloWorldTemplateDescription
146+
},
147+
{
148+
key: CreateProjectCommand.DrawerTemplateKey,
149+
value: "tns-template-drawer-navigation",
150+
description: CreateProjectCommand.DrawerTemplateDescription
151+
},
152+
{
153+
key: CreateProjectCommand.TabsTemplateKey,
154+
value: "tns-template-tab-navigation",
155+
description: CreateProjectCommand.TabsTemplateDescription
156+
}];
157+
158+
return templates;
159+
}
160+
161+
private getTsTemplates() {
162+
const templates = [{
163+
key: CreateProjectCommand.HelloWorldTemplateKey,
164+
value: constants.RESERVED_TEMPLATE_NAMES.typescript,
165+
description: CreateProjectCommand.HelloWorldTemplateDescription
166+
},
167+
{
168+
key: CreateProjectCommand.DrawerTemplateKey,
169+
value: "tns-template-drawer-navigation-ts",
170+
description: CreateProjectCommand.DrawerTemplateDescription
171+
},
172+
{
173+
key: CreateProjectCommand.TabsTemplateKey,
174+
value: "tns-template-tab-navigation-ts",
175+
description: CreateProjectCommand.TabsTemplateDescription
176+
}];
177+
178+
return templates;
179+
}
180+
181+
private getNgFlavors() {
182+
const templates = [{
183+
key: CreateProjectCommand.HelloWorldTemplateKey,
184+
value: constants.RESERVED_TEMPLATE_NAMES.angular,
185+
description: CreateProjectCommand.HelloWorldTemplateDescription
186+
},
187+
{
188+
key: CreateProjectCommand.DrawerTemplateKey,
189+
value: "tns-template-drawer-navigation-ng",
190+
description: CreateProjectCommand.DrawerTemplateDescription
191+
},
192+
{
193+
key: CreateProjectCommand.TabsTemplateKey,
194+
value: "tns-template-tab-navigation-ng",
195+
description: CreateProjectCommand.TabsTemplateDescription
196+
}];
197+
198+
return templates;
199+
}
200+
40201
public async postCommandAction(args: string[]): Promise<void> {
41-
const { projectDir } = this.createdProjecData;
202+
const { projectDir } = this.createdProjectData;
42203
const relativePath = path.relative(process.cwd(), projectDir);
43204
this.$logger.printMarkdown(`Now you can navigate to your project with \`$ cd ${relativePath}\``);
44205
this.$logger.printMarkdown(`After that you can run it on device/emulator by executing \`$ tns run <platform>\``);

lib/common/logger.ts

+3
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export class Logger implements ILogger {
128128
const opts = {
129129
unescape: true,
130130
link: chalk.red,
131+
strong: chalk.green.bold,
132+
firstHeading: chalk.blue.bold,
131133
tableOptions: {
132134
chars: { 'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
133135
style: {
@@ -141,6 +143,7 @@ export class Logger implements ILogger {
141143
};
142144

143145
marked.setOptions({ renderer: new TerminalRenderer(opts) });
146+
144147
const formattedMessage = marked(util.format.apply(null, args));
145148
this.write(formattedMessage);
146149
}

lib/common/prompter.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ReadStream } from "tty";
55
const MuteStream = require("mute-stream");
66

77
export class Prompter implements IPrompter {
8+
private descriptionSeparator = "|";
89
private ctrlcReader: readline.ReadLine;
910
private muteStreamInstance: any = null;
1011

@@ -15,6 +16,11 @@ export class Prompter implements IPrompter {
1516
}
1617

1718
public async get(questions: prompt.Question[]): Promise<any> {
19+
_.each(questions, q => {
20+
q.filter = ((selection: string) => {
21+
return selection.split(this.descriptionSeparator)[0].trim();
22+
});
23+
});
1824
try {
1925
this.muteStdout();
2026

@@ -71,7 +77,7 @@ export class Prompter implements IPrompter {
7177
return result.inputString;
7278
}
7379

74-
public async promptForChoice(promptMessage: string, choices: any[]): Promise<string> {
80+
public async promptForChoice(promptMessage: string, choices: string[]): Promise<string> {
7581
const schema: prompt.Question = {
7682
message: promptMessage,
7783
type: "list",
@@ -83,6 +89,26 @@ export class Prompter implements IPrompter {
8389
return result.userAnswer;
8490
}
8591

92+
public async promptForDetailedChoice(promptMessage: string, choices: { key: string, description: string }[]): Promise<string> {
93+
const longestKeyLength = choices.concat().sort(function (a, b) { return b.key.length - a.key.length; })[0].key.length;
94+
const inquirerChoices = choices.map((choice) => {
95+
return {
96+
name: `${_.padEnd(choice.key, longestKeyLength)} ${this.descriptionSeparator} ${choice.description}`,
97+
short: choice.key
98+
};
99+
});
100+
101+
const schema: prompt.Question = {
102+
message: promptMessage,
103+
type: "list",
104+
name: "userAnswer",
105+
choices: inquirerChoices
106+
};
107+
108+
const result = await this.get([schema]);
109+
return result.userAnswer;
110+
}
111+
86112
public async confirm(prompt: string, defaultAction?: () => boolean): Promise<boolean> {
87113
const schema = {
88114
type: "confirm",

lib/constants.ts

+8
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ export class ReleaseType {
7676

7777
export const RESERVED_TEMPLATE_NAMES: IStringDictionary = {
7878
"default": "tns-template-hello-world",
79+
"javascript": "tns-template-hello-world",
7980
"tsc": "tns-template-hello-world-ts",
81+
"vue": "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0",
8082
"typescript": "tns-template-hello-world-ts",
8183
"ng": "tns-template-hello-world-ng",
8284
"angular": "tns-template-hello-world-ng"
@@ -100,8 +102,14 @@ class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationTyp
100102
}
101103

102104
export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass();
105+
export const VUE_NAME = "vue";
103106
export const ANGULAR_NAME = "angular";
107+
export const JAVASCRIPT_NAME = "javascript";
104108
export const TYPESCRIPT_NAME = "typescript";
109+
export const NgFlavorName = "Angular";
110+
export const VueFlavorName = "Vue.js";
111+
export const TsFlavorName = "Plain TypeScript";
112+
export const JsFlavorName = "Plain JavaScript";
105113
export const BUILD_OUTPUT_EVENT_NAME = "buildOutput";
106114
export const CONNECTION_ERROR_EVENT_NAME = "connectionError";
107115
export const USER_INTERACTION_NEEDED_EVENT_NAME = "userInteractionNeeded";

lib/declarations.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,14 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
508508
frameworkVersion: string;
509509
ipa: string;
510510
tsc: boolean;
511+
ts: boolean;
512+
typescript: boolean;
511513
ng: boolean;
514+
angular: boolean;
515+
vue: boolean;
516+
vuejs: boolean;
517+
js: boolean;
518+
javascript: boolean;
512519
androidTypings: boolean;
513520
production: boolean; //npm flag
514521
syncAllFiles: boolean;

lib/definitions/project.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ interface ICreateProjectData extends IProjectDir, IProjectName {
5454
}
5555

5656
interface IProjectService {
57+
validateProjectName(opts: { projectName: string, force: boolean, pathToProject: string }) : Promise<string>
5758
/**
5859
* Creates new NativeScript application.
5960
* @param {any} projectSettings Options describing new project - its name, appId, path and template from which to be created.

0 commit comments

Comments
 (0)