From c595c0edfb5fb0121d1bf8b9758c8dfb5c42e79c Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Thu, 27 Sep 2018 14:55:50 +0300 Subject: [PATCH 1/4] feat: interactive app creation --- lib/commands/create-project.ts | 196 ++++++++++++++++++++++++++++++-- lib/common/logger.ts | 3 + lib/common/prompter.ts | 28 ++++- lib/constants.ts | 4 + lib/declarations.d.ts | 7 ++ lib/definitions/project.d.ts | 1 + lib/definitions/prompter.d.ts | 3 +- lib/options.ts | 23 ++++ lib/services/project-service.ts | 34 ++++-- npm-shrinkwrap.json | 76 ++++++++----- package.json | 4 +- test/project-commands.ts | 4 + test/stubs.ts | 3 + 13 files changed, 330 insertions(+), 56 deletions(-) diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index bc024dc6f5..85b7c74419 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -1,44 +1,220 @@ 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 NgFlavor = "Angular"; + private static VueFlavor = "Vue.js"; + private static TsFlavor = "Plain TypeScript"; + private static JsFlavor = "Plain JavaScript"; + 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 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 { - 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 ((!selectedTemplate || !projectName) && isInteractive()) { + this.printInteractiveCreationIntro(); + } + + if (!projectName && isInteractive()) { + 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()) { + 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: CreateProjectCommand.NgFlavor, description: "Learn more at https://angular.io/" }, + { key: CreateProjectCommand.VueFlavor, description: "Learn more at https://vuejs.org/" }, + { key: CreateProjectCommand.TsFlavor, description: "Learn more at https://www.typescriptlang.org/" }, + { key: CreateProjectCommand.JsFlavor, description: "Learn more at https://www.javascript.com/" }, + ]); + return flavorSelection; + } + + private printInteractiveCreationIntro() { + 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 CreateProjectCommand.NgFlavor: { + selectedFlavorTemplates.push(...this.getNgFlavors()); + break; + } + case CreateProjectCommand.VueFlavor: { + selectedFlavorTemplates.push({ value: "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0" }); + break; + } + case CreateProjectCommand.TsFlavor: { + selectedFlavorTemplates.push(...this.getTsTemplates()); + break; + } + case CreateProjectCommand.JsFlavor: { + 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?: string; + value: string; + description?: string; + }[] = []; + templates.push({ + key: CreateProjectCommand.HelloWorldTemplateKey, + value: "tns-template-hello-world", + description: CreateProjectCommand.HelloWorldTemplateDescription + }); + templates.push({ + key: CreateProjectCommand.DrawerTemplateKey, + value: "tns-template-drawer-navigation", + description: CreateProjectCommand.DrawerTemplateDescription + }); + templates.push({ + key: CreateProjectCommand.TabsTemplateKey, + value: "tns-template-tab-navigation", + description: CreateProjectCommand.TabsTemplateDescription + }); + return templates; + } + + private getTsTemplates() { + const templates: { + key?: string; + value: string; + description?: string; + }[] = []; + templates.push({ + key: CreateProjectCommand.HelloWorldTemplateKey, + value: "tns-template-hello-world-ts", + description: CreateProjectCommand.HelloWorldTemplateDescription + }); + templates.push({ + key: CreateProjectCommand.DrawerTemplateKey, + value: "tns-template-drawer-navigation-ts", + description: CreateProjectCommand.DrawerTemplateDescription + }); + templates.push({ + key: CreateProjectCommand.TabsTemplateKey, + value: "tns-template-tab-navigation-ts", + description: CreateProjectCommand.TabsTemplateDescription + }); + return templates; + } + + private getNgFlavors() { + const templates: { + key?: string; + value: string; + description?: string; + }[] = []; + templates.push({ + key: CreateProjectCommand.HelloWorldTemplateKey, + value: "tns-template-hello-world-ng", + description: CreateProjectCommand.HelloWorldTemplateDescription + }); + templates.push({ + key: CreateProjectCommand.DrawerTemplateKey, + value: "tns-template-drawer-navigation-ng", + description: CreateProjectCommand.DrawerTemplateDescription + }); + templates.push({ + key: CreateProjectCommand.TabsTemplateKey, + value: "tns-template-tab-navigation-ng", + description: CreateProjectCommand.TabsTemplateDescription + }); + + return templates; + } + public async postCommandAction(args: string[]): Promise { - 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 \``); diff --git a/lib/common/logger.ts b/lib/common/logger.ts index 108c6f159a..32cad3aca6 100644 --- a/lib/common/logger.ts +++ b/lib/common/logger.ts @@ -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: { @@ -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); } diff --git a/lib/common/prompter.ts b/lib/common/prompter.ts index 4ed78d89a9..5f5d24b3b8 100644 --- a/lib/common/prompter.ts +++ b/lib/common/prompter.ts @@ -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; @@ -15,6 +16,11 @@ export class Prompter implements IPrompter { } public async get(questions: prompt.Question[]): Promise { + _.each(questions, q => { + q.filter = ((selection: string) => { + return selection.split(this.descriptionSeparator)[0].trim(); + }); + }); try { this.muteStdout(); @@ -71,7 +77,7 @@ export class Prompter implements IPrompter { return result.inputString; } - public async promptForChoice(promptMessage: string, choices: any[]): Promise { + public async promptForChoice(promptMessage: string, choices: string[]): Promise { const schema: prompt.Question = { message: promptMessage, type: "list", @@ -83,6 +89,26 @@ export class Prompter implements IPrompter { return result.userAnswer; } + public async promptForDetailedChoice(promptMessage: string, choices: { key: string, description: string }[]): Promise { + 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 { const schema = { type: "confirm", diff --git a/lib/constants.ts b/lib/constants.ts index becb8280e6..b5b31bef87 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -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" @@ -100,7 +102,9 @@ 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 BUILD_OUTPUT_EVENT_NAME = "buildOutput"; export const CONNECTION_ERROR_EVENT_NAME = "connectionError"; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index f59d13ec5f..5b1285057a 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -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; diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index b873227812..83e6e360eb 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -54,6 +54,7 @@ interface ICreateProjectData extends IProjectDir, IProjectName { } interface IProjectService { + validateProjectName(opts: { projectName: string, force: boolean, pathToProject: string }) : Promise /** * Creates new NativeScript application. * @param {any} projectSettings Options describing new project - its name, appId, path and template from which to be created. diff --git a/lib/definitions/prompter.d.ts b/lib/definitions/prompter.d.ts index 6719a6f56d..56fed41351 100644 --- a/lib/definitions/prompter.d.ts +++ b/lib/definitions/prompter.d.ts @@ -5,7 +5,8 @@ declare global { get(schemas: prompt.Question[]): Promise; getPassword(prompt: string, options?: IAllowEmpty): Promise; getString(prompt: string, options?: IPrompterOptions): Promise; - promptForChoice(promptMessage: string, choices: any[]): Promise; + promptForChoice(promptMessage: string, choices: string[]): Promise; + promptForDetailedChoice(promptMessage: string, choices: { key: string, description: string }[]): Promise; confirm(prompt: string, defaultAction?: () => boolean): Promise; } } diff --git a/lib/options.ts b/lib/options.ts index 27b5fc97a4..cf91921349 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -62,8 +62,15 @@ export class Options { port: { type: OptionType.Number }, copyTo: { type: OptionType.String }, platformTemplate: { type: OptionType.String }, + js: { type: OptionType.Boolean }, + javascript: { type: OptionType.Boolean }, ng: { type: OptionType.Boolean }, + angular: { type: OptionType.Boolean }, + vue: { type: OptionType.Boolean }, + vuejs: { type: OptionType.Boolean }, tsc: { type: OptionType.Boolean }, + ts: { type: OptionType.Boolean }, + typescript: { type: OptionType.Boolean }, androidTypings: { type: OptionType.Boolean }, bundle: { type: OptionType.String }, all: { type: OptionType.Boolean }, @@ -228,6 +235,22 @@ export class Options { this.argv.watch = false; } + if (this.argv.ts || this.argv.typescript) { + this.argv.tsc = true; + } + + if (this.argv.angular) { + this.argv.ng = true; + } + + if (this.argv.vuejs) { + this.argv.vue = true; + } + + if (this.argv.javascript) { + this.argv.js = true; + } + // Default to "nativescript-dev-webpack" if only `--bundle` is passed if (this.argv.bundle !== undefined || this.argv.hmr) { this.argv.bundle = this.argv.bundle || "webpack"; diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 4e44481dee..683d467018 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -21,24 +21,35 @@ export class ProjectService implements IProjectService { private $staticConfig: IStaticConfig, private $npmInstallationManager: INpmInstallationManager) { } - @exported("projectService") - public async createProject(projectOptions: IProjectSettings): Promise { - let projectName = projectOptions.projectName; - + public async validateProjectName(opts: { projectName: string, force: boolean, pathToProject: string }) : Promise { + let projectName = opts.projectName; if (!projectName) { this.$errors.fail("You must specify when creating a new project."); } - projectName = await this.$projectNameService.ensureValidName(projectName, { force: projectOptions.force }); + projectName = await this.$projectNameService.ensureValidName(projectName, { force: opts.force }); + const projectDir = this.getValidProjectDir(opts.pathToProject, projectName); + if (this.$fs.exists(projectDir) && !this.$fs.isEmptyDir(projectDir)) { + this.$errors.fail("Path already exists and is not empty %s", projectDir); + } + + return projectName; + } - const selectedPath = path.resolve(projectOptions.pathToProject || "."); + private getValidProjectDir(pathToProject: string, projectName: string): string { + const selectedPath = path.resolve(pathToProject || "."); const projectDir = path.join(selectedPath, projectName); - this.$fs.createDirectory(projectDir); + return projectDir; + } - if (this.$fs.exists(projectDir) && !this.$fs.isEmptyDir(projectDir)) { - this.$errors.fail("Path already exists and is not empty %s", projectDir); - } + @exported("projectService") + public async createProject(projectOptions: IProjectSettings): Promise { + let projectName = projectOptions.projectName; + projectName = await this.validateProjectName({ projectName, force: projectOptions.force, pathToProject: projectOptions.pathToProject }); + const projectDir = this.getValidProjectDir(projectOptions.pathToProject, projectName); + + this.$fs.createDirectory(projectDir); const appId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX); this.createPackageJson(projectDir, appId); @@ -46,7 +57,8 @@ export class ProjectService implements IProjectService { const projectCreationData = await this.createProjectCore({ template: projectOptions.template, projectDir, ignoreScripts: projectOptions.ignoreScripts, appId: appId, projectName }); - this.$logger.printMarkdown("Project `%s` was successfully created.", projectCreationData.projectName); + this.$logger.info(); + this.$logger.printMarkdown("__Project `%s` was successfully created.__", projectName); return projectCreationData; } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9d8098af94..dab375007a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -437,9 +437,9 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "ansicolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz", - "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" }, "anymatch": { "version": "1.3.2", @@ -1010,12 +1010,12 @@ } }, "cardinal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-1.0.0.tgz", - "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", "requires": { - "ansicolors": "~0.2.1", - "redeyed": "~1.0.0" + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" } }, "caseless": { @@ -4691,32 +4691,46 @@ } }, "marked": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.12.tgz", - "integrity": "sha512-k4NaW+vS7ytQn6MgJn3fYpQt20/mOgYM5Ft9BYMfQJDz2QT6yEeS9XJ8k2Nw8JTeWK/znPPW2n3UJGzyYEiMoA==" + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.5.1.tgz", + "integrity": "sha512-iUkBZegCZou4AdwbKTwSW/lNDcz5OuRSl3qdcl31Ia0B2QPG0Jn+tKblh/9/eP9/6+4h27vpoh8wel/vQOV0vw==" }, "marked-terminal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-2.0.0.tgz", - "integrity": "sha1-Xq9Wi+ZvaGVBr6UqVYKAMQox3i0=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.1.1.tgz", + "integrity": "sha512-7UBFww1rdx0w9HehLMCVYa8/AxXaiDigDfMsJcj82/wgLQG9cj+oiMAVlJpeWD57VFJY2OYY+bKeEVIjIlxi+w==", "requires": { - "cardinal": "^1.0.0", - "chalk": "^1.1.3", + "cardinal": "^2.1.1", + "chalk": "^2.4.1", "cli-table": "^0.3.1", "lodash.assign": "^4.2.0", "node-emoji": "^1.4.1" }, "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, "chalk": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" } } } @@ -6436,17 +6450,17 @@ } }, "redeyed": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", - "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", "requires": { - "esprima": "~3.0.0" + "esprima": "~4.0.0" }, "dependencies": { "esprima": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", - "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" } } }, diff --git a/package.json b/package.json index 5bc92a63e9..cc43e7b619 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "lockfile": "1.0.3", "lodash": "4.17.10", "log4js": "1.0.1", - "marked": "0.3.12", - "marked-terminal": "2.0.0", + "marked": "0.5.1", + "marked-terminal": "3.1.1", "minimatch": "3.0.2", "mkdirp": "0.5.1", "mute-stream": "0.0.5", diff --git a/test/project-commands.ts b/test/project-commands.ts index 20722bceb8..628a1d2283 100644 --- a/test/project-commands.ts +++ b/test/project-commands.ts @@ -10,6 +10,10 @@ let isProjectCreated: boolean; const dummyArgs = ["dummyArgsString"]; class ProjectServiceMock implements IProjectService { + async validateProjectName(opts: { projectName: string, force: boolean, pathToProject: string }) : Promise { + return null; + } + async createProject(projectOptions: IProjectSettings): Promise { selectedTemplateName = projectOptions.template; isProjectCreated = true; diff --git a/test/stubs.ts b/test/stubs.ts index abd3abb21b..fff75943d8 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -540,6 +540,9 @@ export class PrompterStub implements IPrompter { async promptForChoice(promptMessage: string, choices: any[]): Promise { throw unreachable(); } + async promptForDetailedChoice(promptMessage: string, choices: any[]): Promise { + throw unreachable(); + } async confirm(prompt: string, defaultAction?: () => boolean): Promise { throw unreachable(); } From b470c6cefbbc76a37c37306c1a83646ee587a21a Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Wed, 3 Oct 2018 13:35:50 +0300 Subject: [PATCH 2/4] test: add a few tests for the interactive app creation --- test/project-commands.ts | 103 ++++++++++++++++++++++++++++++++------- test/stubs.ts | 11 +++-- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/test/project-commands.ts b/test/project-commands.ts index 628a1d2283..bd887d5fd3 100644 --- a/test/project-commands.ts +++ b/test/project-commands.ts @@ -1,20 +1,31 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { CreateProjectCommand } from "../lib/commands/create-project"; -import { StringParameterBuilder } from "../lib/common/command-params"; +import { StringCommandParameter } from "../lib/common/command-params"; +import helpers = require("../lib/common/helpers"); import * as constants from "../lib/constants"; import { assert } from "chai"; +import { PrompterStub } from "./stubs"; + +const NgFlavor = "Angular"; +const VueFlavor = "Vue.js"; +const TsFlavor = "Plain TypeScript"; +const JsFlavor = "Plain JavaScript"; let selectedTemplateName: string; let isProjectCreated: boolean; +let createProjectCalledWithForce: boolean; +let validateProjectCallsCount: number; const dummyArgs = ["dummyArgsString"]; class ProjectServiceMock implements IProjectService { - async validateProjectName(opts: { projectName: string, force: boolean, pathToProject: string }) : Promise { + async validateProjectName(opts: { projectName: string, force: boolean, pathToProject: string }): Promise { + validateProjectCallsCount++; return null; } async createProject(projectOptions: IProjectSettings): Promise { + createProjectCalledWithForce = projectOptions.force; selectedTemplateName = projectOptions.template; isProjectCreated = true; return null; @@ -45,7 +56,8 @@ function createTestInjector() { template: undefined }); testInjector.register("createCommand", CreateProjectCommand); - testInjector.register("stringParameterBuilder", StringParameterBuilder); + testInjector.register("stringParameter", StringCommandParameter); + testInjector.register("prompter", PrompterStub); return testInjector; } @@ -55,9 +67,34 @@ describe("Project commands tests", () => { let options: IOptions; let createProjectCommand: ICommand; + function setupAnswers(opts: { + projectNameAnswer?: string, + flavorAnswer?: string, + templateAnswer?: string, + }) { + const prompterStub = testInjector.resolve("$prompter"); + const choices: IDictionary = {}; + if (opts.projectNameAnswer) { + choices["First, what will be the name of your app?"] = opts.projectNameAnswer; + } + if (opts.flavorAnswer) { + choices[opts.projectNameAnswer ? "Next" : "First" + ", which flavor would you like to use?"] = opts.flavorAnswer; + } + if (opts.templateAnswer) { + choices[opts.projectNameAnswer ? "Finally" : "Next" + ", which template would you like to start from?"] = opts.templateAnswer; + } + + prompterStub.expect({ + choices + }); + } + beforeEach(() => { testInjector = createTestInjector(); + helpers.isInteractive = () => true; isProjectCreated = false; + validateProjectCallsCount = 0; + createProjectCalledWithForce = false; selectedTemplateName = undefined; options = testInjector.resolve("$options"); createProjectCommand = testInjector.resolve("$createCommand"); @@ -70,6 +107,8 @@ describe("Project commands tests", () => { await createProjectCommand.execute(dummyArgs); assert.isTrue(isProjectCreated); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); it("should not fail when using only --tsc.", async () => { @@ -78,6 +117,8 @@ describe("Project commands tests", () => { await createProjectCommand.execute(dummyArgs); assert.isTrue(isProjectCreated); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); it("should not fail when using only --template.", async () => { @@ -86,6 +127,8 @@ describe("Project commands tests", () => { await createProjectCommand.execute(dummyArgs); assert.isTrue(isProjectCreated); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); it("should set the template name correctly when used --ng.", async () => { @@ -94,6 +137,8 @@ describe("Project commands tests", () => { await createProjectCommand.execute(dummyArgs); assert.deepEqual(selectedTemplateName, constants.ANGULAR_NAME); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); it("should set the template name correctly when used --tsc.", async () => { @@ -102,36 +147,60 @@ describe("Project commands tests", () => { await createProjectCommand.execute(dummyArgs); assert.deepEqual(selectedTemplateName, constants.TYPESCRIPT_NAME); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); + }); + + it("should fail when --ng and --template are used simultaneously.", async () => { + options.ng = true; + options.template = "ng"; + + await assert.isRejected(createProjectCommand.execute(dummyArgs)); + }); + + it("should fail when --tsc and --template are used simultaneously.", async () => { + options.tsc = true; + options.template = "tsc"; + + await assert.isRejected(createProjectCommand.execute(dummyArgs)); }); - it("should not set the template name when --ng is not used.", async () => { - options.ng = false; + it("should ask for a template when ng flavor is selected.", async () => { + setupAnswers({ flavorAnswer: NgFlavor, templateAnswer: "Hello World" }); await createProjectCommand.execute(dummyArgs); - assert.isUndefined(selectedTemplateName); + assert.deepEqual(selectedTemplateName, "tns-template-hello-world-ng"); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); - it("should not set the template name when --tsc is not used.", async () => { - options.tsc = false; + it("should ask for a template when ts flavor is selected.", async () => { + setupAnswers({ flavorAnswer: TsFlavor, templateAnswer: "Hello World" }); await createProjectCommand.execute(dummyArgs); - assert.isUndefined(selectedTemplateName); + assert.deepEqual(selectedTemplateName, "tns-template-hello-world-ts"); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); - it("should fail when --ng and --template are used simultaneously.", async () => { - options.ng = true; - options.template = "ng"; + it("should ask for a template when js flavor is selected.", async () => { + setupAnswers({ flavorAnswer: JsFlavor, templateAnswer: "Hello World" }); - await assert.isRejected(createProjectCommand.execute(dummyArgs)); + await createProjectCommand.execute(dummyArgs); + + assert.deepEqual(selectedTemplateName, "tns-template-hello-world"); + assert.equal(validateProjectCallsCount, 1); + assert.isTrue(createProjectCalledWithForce); }); - it("should fail when --tsc and --template are used simultaneously.", async () => { - options.tsc = true; - options.template = "tsc"; + it("should select the default vue template when the vue flavor is selected.", async () => { + setupAnswers({ flavorAnswer: VueFlavor }); - await assert.isRejected(createProjectCommand.execute(dummyArgs)); + await createProjectCommand.execute(dummyArgs); + + assert.deepEqual(selectedTemplateName, "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0"); }); }); }); diff --git a/test/stubs.ts b/test/stubs.ts index fff75943d8..560179770b 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -514,11 +514,13 @@ export class LockFile { export class PrompterStub implements IPrompter { private strings: IDictionary = {}; private passwords: IDictionary = {}; + private choices: IDictionary = {}; - expect(options?: { strings: IDictionary, passwords: IDictionary }) { + expect(options?: { strings?: IDictionary, passwords?: IDictionary, choices?: IDictionary }) { if (options) { this.strings = options.strings || this.strings; this.passwords = options.passwords || this.passwords; + this.choices = options.choices || this.choices; } } @@ -540,8 +542,11 @@ export class PrompterStub implements IPrompter { async promptForChoice(promptMessage: string, choices: any[]): Promise { throw unreachable(); } - async promptForDetailedChoice(promptMessage: string, choices: any[]): Promise { - throw unreachable(); + async promptForDetailedChoice(question: string, choices: any[]): Promise { + chai.assert.ok(question in this.choices, `PrompterStub didn't expect to be asked: ${question}`); + const result = this.choices[question]; + delete this.choices[question]; + return result; } async confirm(prompt: string, defaultAction?: () => boolean): Promise { throw unreachable(); From 1cb03237912f4709c6dca41b53033b8c28c23d4f Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Wed, 3 Oct 2018 15:00:27 +0300 Subject: [PATCH 3/4] refactor: fix pr comments --- lib/commands/create-project.ts | 97 ++++++++++++++------------------- lib/constants.ts | 4 ++ lib/project-data.ts | 4 +- lib/services/project-service.ts | 39 +++++++------ test/project-commands.ts | 13 ++--- 5 files changed, 70 insertions(+), 87 deletions(-) diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index 85b7c74419..cf4a3a86df 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -5,16 +5,13 @@ import { isInteractive } from "../common/helpers"; export class CreateProjectCommand implements ICommand { public enableHooks = false; public allowedParameters: ICommandParameter[] = [this.$stringParameter]; - private static NgFlavor = "Angular"; - private static VueFlavor = "Vue.js"; - private static TsFlavor = "Plain TypeScript"; - private static JsFlavor = "Plain JavaScript"; 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 createdProjectData: ICreateProjectData; @@ -49,11 +46,8 @@ export class CreateProjectCommand implements ICommand { selectedTemplate = this.$options.template; } - if ((!selectedTemplate || !projectName) && isInteractive()) { - this.printInteractiveCreationIntro(); - } - if (!projectName && isInteractive()) { + this.printInteractiveCreationIntroIfNeeded(); projectName = await this.$prompter.getString(`${getNextInteractiveAdverb()}, what will be the name of your app?`, { allowEmpty: false }); this.$logger.info(); } @@ -61,6 +55,7 @@ export class CreateProjectCommand implements ICommand { 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()); } @@ -84,22 +79,25 @@ export class CreateProjectCommand implements ICommand { private async interactiveFlavorSelection(adverb: string) { const flavorSelection = await this.$prompter.promptForDetailedChoice(`${adverb}, which flavor would you like to use?`, [ - { key: CreateProjectCommand.NgFlavor, description: "Learn more at https://angular.io/" }, - { key: CreateProjectCommand.VueFlavor, description: "Learn more at https://vuejs.org/" }, - { key: CreateProjectCommand.TsFlavor, description: "Learn more at https://www.typescriptlang.org/" }, - { key: CreateProjectCommand.JsFlavor, description: "Learn more at https://www.javascript.com/" }, + { 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 printInteractiveCreationIntro() { - this.$logger.info(); - this.$logger.printMarkdown(`# Let’s create a NativeScript app!`); - this.$logger.printMarkdown(` + 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) { @@ -110,19 +108,19 @@ or --js flags.) }[] = []; let selectedTemplate: string; switch (flavorSelection) { - case CreateProjectCommand.NgFlavor: { + case constants.NgFlavorName: { selectedFlavorTemplates.push(...this.getNgFlavors()); break; } - case CreateProjectCommand.VueFlavor: { + case constants.VueFlavorName: { selectedFlavorTemplates.push({ value: "https://github.com/NativeScript/template-blank-vue/tarball/0.9.0" }); break; } - case CreateProjectCommand.TsFlavor: { + case constants.TsFlavorName: { selectedFlavorTemplates.push(...this.getTsTemplates()); break; } - case CreateProjectCommand.JsFlavor: { + case constants.JsFlavorName: { selectedFlavorTemplates.push(...this.getJsTemplates()); break; } @@ -141,74 +139,61 @@ or --js flags.) } private getJsTemplates() { - const templates: { - key?: string; - value: string; - description?: string; - }[] = []; - templates.push({ + const templates = [{ key: CreateProjectCommand.HelloWorldTemplateKey, - value: "tns-template-hello-world", + value: constants.RESERVED_TEMPLATE_NAMES.javascript, description: CreateProjectCommand.HelloWorldTemplateDescription - }); - templates.push({ + }, + { key: CreateProjectCommand.DrawerTemplateKey, value: "tns-template-drawer-navigation", description: CreateProjectCommand.DrawerTemplateDescription - }); - templates.push({ + }, + { key: CreateProjectCommand.TabsTemplateKey, value: "tns-template-tab-navigation", description: CreateProjectCommand.TabsTemplateDescription - }); + }]; + return templates; } private getTsTemplates() { - const templates: { - key?: string; - value: string; - description?: string; - }[] = []; - templates.push({ + const templates = [{ key: CreateProjectCommand.HelloWorldTemplateKey, - value: "tns-template-hello-world-ts", + value: constants.RESERVED_TEMPLATE_NAMES.typescript, description: CreateProjectCommand.HelloWorldTemplateDescription - }); - templates.push({ + }, + { key: CreateProjectCommand.DrawerTemplateKey, value: "tns-template-drawer-navigation-ts", description: CreateProjectCommand.DrawerTemplateDescription - }); - templates.push({ + }, + { key: CreateProjectCommand.TabsTemplateKey, value: "tns-template-tab-navigation-ts", description: CreateProjectCommand.TabsTemplateDescription - }); + }]; + return templates; } private getNgFlavors() { - const templates: { - key?: string; - value: string; - description?: string; - }[] = []; - templates.push({ + const templates = [{ key: CreateProjectCommand.HelloWorldTemplateKey, - value: "tns-template-hello-world-ng", + value: constants.RESERVED_TEMPLATE_NAMES.angular, description: CreateProjectCommand.HelloWorldTemplateDescription - }); - templates.push({ + }, + { key: CreateProjectCommand.DrawerTemplateKey, value: "tns-template-drawer-navigation-ng", description: CreateProjectCommand.DrawerTemplateDescription - }); - templates.push({ + }, + { key: CreateProjectCommand.TabsTemplateKey, value: "tns-template-tab-navigation-ng", description: CreateProjectCommand.TabsTemplateDescription - }); + }]; return templates; } diff --git a/lib/constants.ts b/lib/constants.ts index b5b31bef87..7d8b1ad087 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -106,6 +106,10 @@ 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"; diff --git a/lib/project-data.ts b/lib/project-data.ts index 7cf8bbd8c6..58aa01a03a 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -20,11 +20,11 @@ export class ProjectData implements IProjectData { isDefaultProjectType: true }, { - type: "Angular", + type: constants.NgFlavorName, requiredDependencies: ["@angular/core", "nativescript-angular"] }, { - type: "Vue.js", + type: constants.VueFlavorName, requiredDependencies: ["nativescript-vue"] }, { diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 683d467018..dad691ebf2 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -36,17 +36,9 @@ export class ProjectService implements IProjectService { return projectName; } - private getValidProjectDir(pathToProject: string, projectName: string): string { - const selectedPath = path.resolve(pathToProject || "."); - const projectDir = path.join(selectedPath, projectName); - - return projectDir; - } - @exported("projectService") public async createProject(projectOptions: IProjectSettings): Promise { - let projectName = projectOptions.projectName; - projectName = await this.validateProjectName({ projectName, force: projectOptions.force, pathToProject: projectOptions.pathToProject }); + const projectName = await this.validateProjectName({ projectName: projectOptions.projectName, force: projectOptions.force, pathToProject: projectOptions.pathToProject }); const projectDir = this.getValidProjectDir(projectOptions.pathToProject, projectName); this.$fs.createDirectory(projectDir); @@ -63,6 +55,24 @@ export class ProjectService implements IProjectService { return projectCreationData; } + @exported("projectService") + public isValidNativeScriptProject(pathToProject?: string): boolean { + try { + const projectData = this.$projectDataService.getProjectData(pathToProject); + + return !!projectData && !!projectData.projectDir && !!(projectData.projectIdentifiers.ios && projectData.projectIdentifiers.android); + } catch (e) { + return false; + } + } + + private getValidProjectDir(pathToProject: string, projectName: string): string { + const selectedPath = path.resolve(pathToProject || "."); + const projectDir = path.join(selectedPath, projectName); + + return projectDir; + } + private async createProjectCore(projectCreationSettings: IProjectCreationSettings): Promise { const { template, projectDir, appId, projectName, ignoreScripts } = projectCreationSettings; @@ -110,17 +120,6 @@ export class ProjectService implements IProjectService { return { projectName, projectDir }; } - @exported("projectService") - public isValidNativeScriptProject(pathToProject?: string): boolean { - try { - const projectData = this.$projectDataService.getProjectData(pathToProject); - - return !!projectData && !!projectData.projectDir && !!(projectData.projectIdentifiers.ios && projectData.projectIdentifiers.android); - } catch (e) { - return false; - } - } - private async extractTemplate(projectDir: string, templateData: ITemplateData): Promise { this.$fs.ensureDirectoryExists(projectDir); diff --git a/test/project-commands.ts b/test/project-commands.ts index bd887d5fd3..a05aa11300 100644 --- a/test/project-commands.ts +++ b/test/project-commands.ts @@ -7,11 +7,6 @@ import * as constants from "../lib/constants"; import { assert } from "chai"; import { PrompterStub } from "./stubs"; -const NgFlavor = "Angular"; -const VueFlavor = "Vue.js"; -const TsFlavor = "Plain TypeScript"; -const JsFlavor = "Plain JavaScript"; - let selectedTemplateName: string; let isProjectCreated: boolean; let createProjectCalledWithForce: boolean; @@ -166,7 +161,7 @@ describe("Project commands tests", () => { }); it("should ask for a template when ng flavor is selected.", async () => { - setupAnswers({ flavorAnswer: NgFlavor, templateAnswer: "Hello World" }); + setupAnswers({ flavorAnswer: constants.NgFlavorName, templateAnswer: "Hello World" }); await createProjectCommand.execute(dummyArgs); @@ -176,7 +171,7 @@ describe("Project commands tests", () => { }); it("should ask for a template when ts flavor is selected.", async () => { - setupAnswers({ flavorAnswer: TsFlavor, templateAnswer: "Hello World" }); + setupAnswers({ flavorAnswer: constants.TsFlavorName, templateAnswer: "Hello World" }); await createProjectCommand.execute(dummyArgs); @@ -186,7 +181,7 @@ describe("Project commands tests", () => { }); it("should ask for a template when js flavor is selected.", async () => { - setupAnswers({ flavorAnswer: JsFlavor, templateAnswer: "Hello World" }); + setupAnswers({ flavorAnswer: constants.JsFlavorName, templateAnswer: "Hello World" }); await createProjectCommand.execute(dummyArgs); @@ -196,7 +191,7 @@ describe("Project commands tests", () => { }); it("should select the default vue template when the vue flavor is selected.", async () => { - setupAnswers({ flavorAnswer: VueFlavor }); + setupAnswers({ flavorAnswer: constants.VueFlavorName }); await createProjectCommand.execute(dummyArgs); From ea8f05634e178b61e9ebbe425d7215e9c27db584 Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Fri, 5 Oct 2018 17:27:24 +0300 Subject: [PATCH 4/4] chore: fix pr comments --- docs/man_pages/project/creation/create.md | 40 +++++++++++++++++------ test/project-commands.ts | 35 +++++++++++++++----- test/stubs.ts | 15 +++++---- 3 files changed, 65 insertions(+), 25 deletions(-) diff --git a/docs/man_pages/project/creation/create.md b/docs/man_pages/project/creation/create.md index 2eeb379bf1..db80f7225d 100644 --- a/docs/man_pages/project/creation/create.md +++ b/docs/man_pages/project/creation/create.md @@ -7,29 +7,49 @@ position: 1 Usage | Synopsis ---|--- -Create from default JavaScript template | `$ tns create [--path ] [--appid ]` -Create from default TypeScript template | `$ tns create --template typescript [--path ] [--appid ]` OR `$ tns create --tsc [--path ] [--appid ]` OR `$ tns create --template tsc [--path ] [--appid ]` -Create from default Angular template | `$ tns create --template angular [--path ] [--appid ]` OR `$ tns create --template ng [--path ] [--appid ]` OR `$ tns create --ng [--path ] [--appid ]` -Copy from existing project | `$ tns create [--path ] [--appid ]` -Create from custom template | `$ tns create [--path ] [--appid ] --template