Skip to content

feat: interactive app creation #3969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions docs/man_pages/project/creation/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,49 @@ position: 1

Usage | Synopsis
---|---
Create from default JavaScript template | `$ tns create <App Name> [--path <Directory>] [--appid <App ID>]`
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>]`
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>]`
Copy from existing project | `$ tns create <App Name> [--path <Directory>] [--appid <App ID>]`
Create from custom template | `$ tns create <App Name> [--path <Directory>] [--appid <App ID>] --template <Template>`
Create from default JavaScript template | `$ tns create [<App Name>] [--js] [--path <Directory>] [--appid <App ID>]`
Create from default TypeScript template | `$ tns create [<App Name>] --ts [--path <Directory>] [--appid <App ID>]`
Create from default Angular template | `$ tns create [<App Name>] --ng [--path <Directory>] [--appid <App ID>]`
Create from default Vue.js template | `$ tns create [<App Name>] --vue [--path <Directory>] [--appid <App ID>]`
Create from custom template | `$ tns create [<App Name>] [--path <Directory>] [--appid <App ID>] --template <Template>`

Creates a new project for native development with NativeScript.
Interactively creates a new NativeScript app.

### Options
* `--path` - Specifies the directory where you want to create the project, if different from the current directory. The directory must be empty.
* `--appid` - Sets the application identifier for your project.
* `--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.<% } %>
* `--ng` - Sets the template for your project to the Angular template.
* `--tsc` - Sets the template for your project to the TypeScript template.
* `--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.<% } %>
* `--js`, `--javascript` - Sets the template for your project to the JavaScript template.
* `--ts`, `--tsc`, `--typescript` - Sets the template for your project to the TypeScript template.
* `--ng`, `--angular` - Sets the template for your project to the Angular template.
* `--vue`, `--vuejs` - Sets the template for your project to the Vue.js template.

### Attributes
* `<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.
* `<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.
For projects that target iOS, you can use uppercase or lowercase letters, numbers, and hyphens.<% } %>
* `<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`
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`.
* `<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.<% } %>

<% if(isHtml) { %>

### Templates Usage

Based on the selected options, the NativeScript CLI will use the project templates below:

Selected Option | Template
----------|----------
`Plain JavaScript - Hello World`, `--js`, `--javascript` | tns-template-hello-world
`Plain JavaScript - SideDrawer` | tns-template-drawer-navigation
`Plain JavaScript - Tabs` | tns-template-tab-navigation
`Plain TypeScript - Hello World`, `--ts`, `--tsc`, `--typescript` | tns-template-hello-world-ts
`Plain TypeScript - SideDrawer` | tns-template-drawer-navigation-ts
`Plain TypeScript - Tabs` | tns-template-tab-navigation-ts
`Angular - Hello World`, `--ng`, `--angular` | tns-template-hello-world-ng
`Angular - SideDrawer` | tns-template-drawer-navigation-ng
`Angular - Tabs` | tns-template-tab-navigation-ng
`Vue.js`, `--vue`, `--vuejs` | tns-template-blank-vue

### Related Commands

Command | Description
Expand Down
181 changes: 171 additions & 10 deletions lib/commands/create-project.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,205 @@
import * as constants from "../constants";
import * as path from "path";
import { isInteractive } from "../common/helpers";

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

private createdProjecData: ICreateProjectData;
private createdProjectData: ICreateProjectData;

constructor(private $projectService: IProjectService,
private $logger: ILogger,
private $errors: IErrors,
private $options: IOptions,
private $stringParameterBuilder: IStringParameterBuilder) { }
private $prompter: IPrompter,
private $stringParameter: ICommandParameter) { }

public async execute(args: string[]): Promise<void> {
if ((this.$options.tsc || this.$options.ng) && this.$options.template) {
this.$errors.fail("You cannot use --ng or --tsc options together with --template.");
const interactiveAdverbs = ["First", "Next", "Finally"];
const getNextInteractiveAdverb = () => {
return interactiveAdverbs.shift() || "Next";
};

if ((this.$options.tsc || this.$options.ng || this.$options.vue || this.$options.js) && this.$options.template) {
this.$errors.fail("You cannot use a flavor option like --ng, --vue, --tsc and --js together with --template.");
}

let projectName = args[0];
let selectedTemplate: string;
if (this.$options.tsc) {
if (this.$options.js) {
selectedTemplate = constants.JAVASCRIPT_NAME;
} else if (this.$options.tsc) {
selectedTemplate = constants.TYPESCRIPT_NAME;
} else if (this.$options.ng) {
selectedTemplate = constants.ANGULAR_NAME;
} else if (this.$options.vue) {
selectedTemplate = constants.VUE_NAME;
} else {
selectedTemplate = this.$options.template;
}

this.createdProjecData = await this.$projectService.createProject({
projectName: args[0],
if (!projectName && isInteractive()) {
this.printInteractiveCreationIntroIfNeeded();
projectName = await this.$prompter.getString(`${getNextInteractiveAdverb()}, what will be the name of your app?`, { allowEmpty: false });
this.$logger.info();
}

projectName = await this.$projectService.validateProjectName({ projectName: projectName, force: this.$options.force, pathToProject: this.$options.path });

if (!selectedTemplate && isInteractive()) {
this.printInteractiveCreationIntroIfNeeded();
selectedTemplate = await this.interactiveFlavorAndTemplateSelection(getNextInteractiveAdverb(), getNextInteractiveAdverb());
}

this.createdProjectData = await this.$projectService.createProject({
projectName: projectName,
template: selectedTemplate,
appId: this.$options.appid,
pathToProject: this.$options.path,
force: this.$options.force,
// its already validated above
force: true,
ignoreScripts: this.$options.ignoreScripts
});
}

private async interactiveFlavorAndTemplateSelection(flavorAdverb: string, templateAdverb: string) {
const selectedFlavor = await this.interactiveFlavorSelection(flavorAdverb);
const selectedTemplate: string = await this.interactiveTemplateSelection(selectedFlavor, templateAdverb);

return selectedTemplate;
}

private async interactiveFlavorSelection(adverb: string) {
const flavorSelection = await this.$prompter.promptForDetailedChoice(`${adverb}, which flavor would you like to use?`, [
{ key: constants.NgFlavorName, description: "Learn more at https://angular.io/" },
{ key: constants.VueFlavorName, description: "Learn more at https://vuejs.org/" },
{ key: constants.TsFlavorName, description: "Learn more at https://www.typescriptlang.org/" },
{ key: constants.JsFlavorName, description: "Learn more at https://www.javascript.com/" },
]);
return flavorSelection;
}

private printInteractiveCreationIntroIfNeeded() {
if (!this.isInteractionIntroShown) {
this.isInteractionIntroShown = true;
this.$logger.info();
this.$logger.printMarkdown(`# Let’s create a NativeScript app!`);
this.$logger.printMarkdown(`
Answer the following questions to help us build the right app for you. (Note: you
can skip this prompt next time using the --template option, or the --ng, --vue, --ts,
or --js flags.)
`);
}
}

private async interactiveTemplateSelection(flavorSelection: string, adverb: string) {
const selectedFlavorTemplates: {
key?: string;
value: string;
description?: string;
}[] = [];
let selectedTemplate: string;
switch (flavorSelection) {
case constants.NgFlavorName: {
selectedFlavorTemplates.push(...this.getNgFlavors());
break;
}
case constants.VueFlavorName: {
selectedFlavorTemplates.push({ value: "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0" });
break;
}
case constants.TsFlavorName: {
selectedFlavorTemplates.push(...this.getTsTemplates());
break;
}
case constants.JsFlavorName: {
selectedFlavorTemplates.push(...this.getJsTemplates());
break;
}
}
if (selectedFlavorTemplates.length > 1) {
this.$logger.info();
const templateChoices = selectedFlavorTemplates.map((template) => {
return { key: template.key, description: template.description };
});
const selectedTemplateKey = await this.$prompter.promptForDetailedChoice(`${adverb}, which template would you like to start from?`, templateChoices);
selectedTemplate = selectedFlavorTemplates.find(t => t.key === selectedTemplateKey).value;
} else {
selectedTemplate = selectedFlavorTemplates[0].value;
}
return selectedTemplate;
}

private getJsTemplates() {
const templates = [{
key: CreateProjectCommand.HelloWorldTemplateKey,
value: constants.RESERVED_TEMPLATE_NAMES.javascript,
description: CreateProjectCommand.HelloWorldTemplateDescription
},
{
key: CreateProjectCommand.DrawerTemplateKey,
value: "tns-template-drawer-navigation",
description: CreateProjectCommand.DrawerTemplateDescription
},
{
key: CreateProjectCommand.TabsTemplateKey,
value: "tns-template-tab-navigation",
description: CreateProjectCommand.TabsTemplateDescription
}];

return templates;
}

private getTsTemplates() {
const templates = [{
key: CreateProjectCommand.HelloWorldTemplateKey,
value: constants.RESERVED_TEMPLATE_NAMES.typescript,
description: CreateProjectCommand.HelloWorldTemplateDescription
},
{
key: CreateProjectCommand.DrawerTemplateKey,
value: "tns-template-drawer-navigation-ts",
description: CreateProjectCommand.DrawerTemplateDescription
},
{
key: CreateProjectCommand.TabsTemplateKey,
value: "tns-template-tab-navigation-ts",
description: CreateProjectCommand.TabsTemplateDescription
}];

return templates;
}

private getNgFlavors() {
const templates = [{
key: CreateProjectCommand.HelloWorldTemplateKey,
value: constants.RESERVED_TEMPLATE_NAMES.angular,
description: CreateProjectCommand.HelloWorldTemplateDescription
},
{
key: CreateProjectCommand.DrawerTemplateKey,
value: "tns-template-drawer-navigation-ng",
description: CreateProjectCommand.DrawerTemplateDescription
},
{
key: CreateProjectCommand.TabsTemplateKey,
value: "tns-template-tab-navigation-ng",
description: CreateProjectCommand.TabsTemplateDescription
}];

return templates;
}

public async postCommandAction(args: string[]): Promise<void> {
const { projectDir } = this.createdProjecData;
const { projectDir } = this.createdProjectData;
const relativePath = path.relative(process.cwd(), projectDir);
this.$logger.printMarkdown(`Now you can navigate to your project with \`$ cd ${relativePath}\``);
this.$logger.printMarkdown(`After that you can run it on device/emulator by executing \`$ tns run <platform>\``);
Expand Down
3 changes: 3 additions & 0 deletions lib/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export class Logger implements ILogger {
const opts = {
unescape: true,
link: chalk.red,
strong: chalk.green.bold,
firstHeading: chalk.blue.bold,
tableOptions: {
chars: { 'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
style: {
Expand All @@ -141,6 +143,7 @@ export class Logger implements ILogger {
};

marked.setOptions({ renderer: new TerminalRenderer(opts) });

const formattedMessage = marked(util.format.apply(null, args));
this.write(formattedMessage);
}
Expand Down
28 changes: 27 additions & 1 deletion lib/common/prompter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ReadStream } from "tty";
const MuteStream = require("mute-stream");

export class Prompter implements IPrompter {
private descriptionSeparator = "|";
private ctrlcReader: readline.ReadLine;
private muteStreamInstance: any = null;

Expand All @@ -15,6 +16,11 @@ export class Prompter implements IPrompter {
}

public async get(questions: prompt.Question[]): Promise<any> {
_.each(questions, q => {
q.filter = ((selection: string) => {
return selection.split(this.descriptionSeparator)[0].trim();
});
});
try {
this.muteStdout();

Expand Down Expand Up @@ -71,7 +77,7 @@ export class Prompter implements IPrompter {
return result.inputString;
}

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

public async promptForDetailedChoice(promptMessage: string, choices: { key: string, description: string }[]): Promise<string> {
const longestKeyLength = choices.concat().sort(function (a, b) { return b.key.length - a.key.length; })[0].key.length;
const inquirerChoices = choices.map((choice) => {
return {
name: `${_.padEnd(choice.key, longestKeyLength)} ${this.descriptionSeparator} ${choice.description}`,
short: choice.key
};
});

const schema: prompt.Question = {
message: promptMessage,
type: "list",
name: "userAnswer",
choices: inquirerChoices
};

const result = await this.get([schema]);
return result.userAnswer;
}

public async confirm(prompt: string, defaultAction?: () => boolean): Promise<boolean> {
const schema = {
type: "confirm",
Expand Down
8 changes: 8 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export class ReleaseType {

export const RESERVED_TEMPLATE_NAMES: IStringDictionary = {
"default": "tns-template-hello-world",
"javascript": "tns-template-hello-world",
"tsc": "tns-template-hello-world-ts",
"vue": "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0",
"typescript": "tns-template-hello-world-ts",
"ng": "tns-template-hello-world-ng",
"angular": "tns-template-hello-world-ng"
Expand All @@ -100,8 +102,14 @@ class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationTyp
}

export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass();
export const VUE_NAME = "vue";
export const ANGULAR_NAME = "angular";
export const JAVASCRIPT_NAME = "javascript";
export const TYPESCRIPT_NAME = "typescript";
export const NgFlavorName = "Angular";
export const VueFlavorName = "Vue.js";
export const TsFlavorName = "Plain TypeScript";
export const JsFlavorName = "Plain JavaScript";
export const BUILD_OUTPUT_EVENT_NAME = "buildOutput";
export const CONNECTION_ERROR_EVENT_NAME = "connectionError";
export const USER_INTERACTION_NEEDED_EVENT_NAME = "userInteractionNeeded";
Expand Down
7 changes: 7 additions & 0 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,14 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
frameworkVersion: string;
ipa: string;
tsc: boolean;
ts: boolean;
typescript: boolean;
ng: boolean;
angular: boolean;
vue: boolean;
vuejs: boolean;
js: boolean;
javascript: boolean;
androidTypings: boolean;
production: boolean; //npm flag
syncAllFiles: boolean;
Expand Down
1 change: 1 addition & 0 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface ICreateProjectData extends IProjectDir, IProjectName {
}

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