UNPKG

ionic

Version:

A tool for creating and developing Ionic Framework mobile apps.

517 lines (512 loc) 25.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const cli_framework_1 = require("@ionic/cli-framework"); const format_1 = require("@ionic/cli-framework/utils/format"); const string_1 = require("@ionic/cli-framework/utils/string"); const utils_fs_1 = require("@ionic/utils-fs"); const Debug = require("debug"); const path = require("path"); const constants_1 = require("../constants"); const color_1 = require("../lib/color"); const command_1 = require("../lib/command"); const errors_1 = require("../lib/errors"); const executor_1 = require("../lib/executor"); const project_1 = require("../lib/project"); const shell_1 = require("../lib/shell"); const start_1 = require("../lib/start"); const emoji_1 = require("../lib/utils/emoji"); const debug = Debug('ionic:commands:start'); class StartCommand extends command_1.Command { constructor() { super(...arguments); this.canRemoveExisting = false; } async getMetadata() { return { name: 'start', type: 'global', summary: 'Create a new project', description: ` This command creates a working Ionic app. It installs dependencies for you and sets up your project. Running ${color_1.input('ionic start')} without any arguments will prompt you for information about your new project. The first argument is your app's ${color_1.input('name')}. Don't worry--you can always change this later. The ${color_1.input('--project-id')} is generated from ${color_1.input('name')} unless explicitly specified. The second argument is the ${color_1.input('template')} from which to generate your app. You can list all templates with the ${color_1.input('--list')} option. You can also specify a git repository URL for ${color_1.input('template')}, in which case the existing project will be cloned. Use the ${color_1.input('--type')} option to start projects using older versions of Ionic. For example, you can start an Ionic 3 project with ${color_1.input('--type=ionic-angular')}. Use ${color_1.input('--list')} to see all project types and templates. `, exampleCommands: [ '', '--list', 'myApp', 'myApp blank', 'myApp tabs --cordova', 'myApp tabs --capacitor', 'myApp super --type=ionic-angular', 'myApp blank --type=ionic1', 'cordovaApp tabs --cordova', '"My App" blank', '"Conference App" https://github.com/ionic-team/ionic-conference-app', ], inputs: [ { name: 'name', summary: `The name of your new project (e.g. ${color_1.input('myApp')}, ${color_1.input('"My App"')})`, validators: [cli_framework_1.validators.required], }, { name: 'template', summary: `The starter template to use (e.g. ${['blank', 'tabs'].map(t => color_1.input(t)).join(', ')}; use ${color_1.input('--list')} to see all)`, validators: [cli_framework_1.validators.required], }, ], options: [ { name: 'list', summary: 'List available starter templates', type: Boolean, aliases: ['l'], }, { name: 'type', summary: `Type of project to start (e.g. ${start_1.getStarterProjectTypes().map(type => color_1.input(type)).join(', ')})`, type: String, }, { name: 'cordova', summary: 'Include Cordova integration', type: Boolean, }, { name: 'capacitor', summary: 'Include Capacitor integration', type: Boolean, groups: ["experimental" /* EXPERIMENTAL */], }, { name: 'deps', summary: 'Do not install npm/yarn dependencies', type: Boolean, default: true, groups: ["advanced" /* ADVANCED */], }, { name: 'git', summary: 'Do not initialize a git repo', type: Boolean, default: true, groups: ["advanced" /* ADVANCED */], }, { name: 'link', summary: 'Connect your new app to Ionic', type: Boolean, groups: ["advanced" /* ADVANCED */], }, { name: 'id', summary: 'Specify an Ionic App ID to link', }, { name: 'project-id', summary: 'Specify a slug for your app (used for the directory name and package name)', groups: ["advanced" /* ADVANCED */], spec: { value: 'slug' }, }, { name: 'package-id', summary: 'Specify the bundle ID/application ID for your app (reverse-DNS notation)', groups: ["advanced" /* ADVANCED */], spec: { value: 'id' }, }, { name: 'tag', summary: `Specify a tag to use for the starters (e.g. ${['latest', 'testing', 'next'].map(t => color_1.input(t)).join(', ')})`, default: 'latest', groups: ["hidden" /* HIDDEN */], }, ], }; } async preRun(inputs, options) { const { promptToLogin } = await Promise.resolve().then(() => require('../lib/session')); start_1.verifyOptions(options, this.env); const appflowId = options['id'] ? String(options['id']) : undefined; if (appflowId) { if (!this.env.session.isLoggedIn()) { await promptToLogin(this.env); } } if (!inputs[0]) { if (appflowId) { const { AppClient } = await Promise.resolve().then(() => require('../lib/app')); const token = this.env.session.getUserToken(); const appClient = new AppClient(token, this.env); const tasks = this.createTaskChain(); tasks.next(`Looking up app ${color_1.input(appflowId)}`); const app = await appClient.load(appflowId); // TODO: can ask to clone via repo_url tasks.end(); this.env.log.info(`Using ${color_1.strong(app.name)} for ${color_1.input('name')} and ${color_1.strong(app.slug)} for ${color_1.input('--project-id')}.`); inputs[0] = app.name; options['project-id'] = app.slug; } else { if (this.env.flags.interactive) { this.env.log.nl(); this.env.log.msg(`${color_1.strong(`Every great app needs a name! ${emoji_1.emoji('😍', '')}`)}\n` + `Please enter the full name of your app. You can change this at any time. To bypass this prompt next time, supply ${color_1.input('name')}, the first argument to ${color_1.input('ionic start')}.\n\n`); } const name = await this.env.prompt({ type: 'input', name: 'name', message: 'Project name:', validate: v => cli_framework_1.validators.required(v), }); inputs[0] = name; } } const projectType = options['type'] ? String(options['type']) : await this.getProjectType(); if (!inputs[1]) { if (this.env.flags.interactive) { this.env.log.nl(); this.env.log.msg(`${color_1.strong(`Let's pick the perfect starter template! ${emoji_1.emoji('💪', '')}`)}\n` + `Starter templates are ready-to-go Ionic apps that come packed with everything you need to build your app. To bypass this prompt next time, supply ${color_1.input('template')}, the second argument to ${color_1.input('ionic start')}.\n\n`); } const template = await this.env.prompt({ type: 'list', name: 'template', message: 'Starter template:', choices: () => { const starterTemplateList = start_1.STARTER_TEMPLATES.filter(st => st.projectType === projectType); const cols = format_1.columnar(starterTemplateList.map(({ name, description }) => [color_1.input(name), description || '']), {}).split('\n'); if (starterTemplateList.length === 0) { throw new errors_1.FatalException(`No starter templates found for project type: ${color_1.input(projectType)}.`); } return starterTemplateList.map((starter, i) => { return { name: cols[i], short: starter.name, value: starter.name, }; }); }, }); inputs[1] = template; } const starterTemplate = start_1.STARTER_TEMPLATES.find(t => t.name === inputs[1] && t.projectType === projectType); if (starterTemplate && starterTemplate.type === 'repo') { inputs[1] = starterTemplate.repo; } const cloned = string_1.isValidURL(inputs[1]); if (this.project && this.project.details.context === 'app') { const confirm = await this.env.prompt({ type: 'confirm', name: 'confirm', message: 'You are already in an Ionic project directory. Do you really want to start another project here?', default: false, }); if (!confirm) { this.env.log.info('Not starting project within existing project.'); throw new errors_1.FatalException(); } } await this.validateProjectType(projectType); if (cloned) { if (!options['git']) { this.env.log.warn(`The ${color_1.input('--no-git')} option has no effect when cloning apps. Git must be used.`); } options['git'] = true; } if (options['v1'] || options['v2']) { throw new errors_1.FatalException(`The ${color_1.input('--v1')} and ${color_1.input('--v2')} flags have been removed.\n` + `Use the ${color_1.input('--type')} option. (see ${color_1.input('ionic start --help')})`); } if (options['app-name']) { this.env.log.warn(`The ${color_1.input('--app-name')} option has been removed. Use the ${color_1.input('name')} argument with double quotes: e.g. ${color_1.input('ionic start "My App"')}`); } if (options['display-name']) { this.env.log.warn(`The ${color_1.input('--display-name')} option has been removed. Use the ${color_1.input('name')} argument with double quotes: e.g. ${color_1.input('ionic start "My App"')}`); } if (options['bundle-id']) { this.env.log.warn(`The ${color_1.input('--bundle-id')} option has been deprecated. Please use ${color_1.input('--package-id')}.`); options['package-id'] = options['bundle-id']; } let projectId = options['project-id'] ? String(options['project-id']) : undefined; if (projectId) { await this.validateProjectId(projectId); } else { projectId = options['project-id'] = project_1.isValidProjectId(inputs[0]) ? inputs[0] : string_1.slugify(inputs[0]); } const projectDir = path.resolve(projectId); const packageId = options['package-id'] ? String(options['package-id']) : undefined; if (projectId) { await this.checkForExisting(projectDir); } if (cloned) { this.schema = { cloned: true, url: inputs[1], projectId, projectDir, }; } else { this.schema = { cloned: false, name: inputs[0], type: projectType, template: inputs[1], projectId, projectDir, packageId, appflowId, }; } } async getProjectType() { if (this.env.flags.interactive) { this.env.log.nl(); this.env.log.msg(`${color_1.strong(`Pick a framework! ${emoji_1.emoji('😁', '')}`)}\n\n` + `Please select the JavaScript framework to use for your new app. To bypass this prompt next time, supply a value for the ${color_1.input('--type')} option.\n\n`); } const frameworkChoice = await this.env.prompt({ type: 'list', name: 'frameworks', message: 'Framework:', default: 'angular', choices: () => { const cols = format_1.columnar(start_1.SUPPORTED_FRAMEWORKS.map(({ name, description }) => [color_1.input(name), description]), {}).split('\n'); return start_1.SUPPORTED_FRAMEWORKS.map((starterTemplate, i) => { return { name: cols[i], short: starterTemplate.name, value: starterTemplate.type, }; }); }, }); return frameworkChoice; } async run(inputs, options, runinfo) { const { pkgManagerArgs } = await Promise.resolve().then(() => require('../lib/utils/npm')); const { getTopLevel, isGitInstalled } = await Promise.resolve().then(() => require('../lib/git')); if (!this.schema) { throw new errors_1.FatalException(`Invalid start schema: cannot start app.`); } const { projectId, projectDir, packageId, appflowId } = this.schema; const tag = options['tag'] ? String(options['tag']) : 'latest'; let linkConfirmed = typeof appflowId === 'string'; const gitDesired = options['git'] ? true : false; const gitInstalled = await isGitInstalled(this.env); const gitTopLevel = await getTopLevel(this.env); let gitIntegration = gitDesired && gitInstalled && !gitTopLevel ? true : false; if (!gitInstalled) { const installationDocs = `See installation docs for git: ${color_1.strong('https://git-scm.com/book/en/v2/Getting-Started-Installing-Git')}`; if (appflowId) { throw new errors_1.FatalException(`Git CLI not found on your PATH.\n` + `Git must be installed to connect this app to Ionic. ${installationDocs}`); } if (this.schema.cloned) { throw new errors_1.FatalException(`Git CLI not found on your PATH.\n` + `Git must be installed to clone apps with ${color_1.input('ionic start')}. ${installationDocs}`); } } if (gitTopLevel && !this.schema.cloned) { this.env.log.info(`Existing git project found (${color_1.strong(gitTopLevel)}). Git operations are disabled.`); } const tasks = this.createTaskChain(); tasks.next(`Preparing directory ${color_1.input(format_1.prettyPath(projectDir))}`); if (this.canRemoveExisting) { await utils_fs_1.remove(projectDir); } await utils_fs_1.mkdir(projectDir); tasks.end(); if (this.schema.cloned) { await this.env.shell.run('git', ['clone', this.schema.url, projectDir, '--progress'], { stdio: 'inherit' }); } else { const starterTemplate = await this.findStarterTemplate(this.schema.template, this.schema.type, tag); await this.downloadStarterTemplate(projectDir, starterTemplate); } let project; if (this.project && this.project.details.context === 'multiapp' && !this.schema.cloned) { // We're in a multi-app setup, so the new config file isn't wanted. await utils_fs_1.unlink(path.resolve(projectDir, 'ionic.config.json')); project = await project_1.createProjectFromDetails({ context: 'multiapp', configPath: path.resolve(this.project.rootDirectory, constants_1.PROJECT_FILE), id: projectId, type: this.schema.type, errors: [] }, this.env); project.config.set('type', this.schema.type); project.config.set('root', path.relative(this.project.rootDirectory, projectDir)); } else { project = await project_1.createProjectFromDirectory(projectDir, { _: [] }, this.env, { logErrors: false }); } // start is weird, once the project directory is created, it becomes a // "project" command and so we replace the `Project` instance that was // autogenerated when the CLI booted up. This has worked thus far? this.namespace.root.project = project; if (!this.project) { throw new errors_1.FatalException('Error while loading project.'); } this.env.shell.alterPath = p => shell_1.prependNodeModulesBinToPath(projectDir, p); if (!this.schema.cloned) { if (typeof options['cordova'] === 'undefined' && !options['capacitor']) { const confirm = await this.env.prompt({ type: 'confirm', name: 'confirm', message: 'Integrate your new app with Cordova to target native iOS and Android?', default: false, }); if (confirm) { options['cordova'] = true; } } if (options['cordova']) { await executor_1.runCommand(runinfo, ['integrations', 'enable', 'cordova', '--quiet']); } if (options['capacitor']) { await executor_1.runCommand(runinfo, ['integrations', 'enable', 'capacitor', '--quiet', '--', this.schema.name, packageId ? packageId : 'io.ionic.starter']); } await this.project.personalize({ name: this.schema.name, projectId, packageId }); this.env.log.nl(); } const shellOptions = { cwd: projectDir, stdio: 'inherit' }; if (options['deps']) { this.env.log.msg('Installing dependencies may take several minutes.'); this.env.log.rawmsg(start_1.getAdvertisement()); const [installer, ...installerArgs] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install' }); await this.env.shell.run(installer, installerArgs, shellOptions); } if (!this.schema.cloned) { if (gitIntegration) { try { await this.env.shell.run('git', ['init'], shellOptions); // TODO: use initializeRepo()? } catch (e) { this.env.log.warn('Error encountered during repo initialization. Disabling further git operations.'); gitIntegration = false; } } if (options['link']) { const cmdArgs = ['link']; if (appflowId) { cmdArgs.push(appflowId); } cmdArgs.push('--name', this.schema.name); await executor_1.runCommand(runinfo, cmdArgs); linkConfirmed = true; } const manifestPath = path.resolve(projectDir, 'ionic.starter.json'); const manifest = await this.loadManifest(manifestPath); if (manifest) { await utils_fs_1.unlink(manifestPath); } if (gitIntegration) { try { await this.env.shell.run('git', ['add', '-A'], shellOptions); await this.env.shell.run('git', ['commit', '-m', 'Initial commit', '--no-gpg-sign'], shellOptions); } catch (e) { this.env.log.warn('Error encountered during commit. Disabling further git operations.'); gitIntegration = false; } } if (manifest) { await this.performManifestOps(manifest); } } this.env.log.nl(); await this.showNextSteps(projectDir, this.schema.cloned, linkConfirmed); } async checkForExisting(projectDir) { const projectExists = await utils_fs_1.pathExists(projectDir); if (projectExists) { const confirm = await this.env.prompt({ type: 'confirm', name: 'confirm', message: `${color_1.input(format_1.prettyPath(projectDir))} exists. ${color_1.failure('Overwrite?')}`, default: false, }); if (!confirm) { this.env.log.msg(`Not erasing existing project in ${color_1.input(format_1.prettyPath(projectDir))}.`); throw new errors_1.FatalException(); } this.canRemoveExisting = confirm; } } async findStarterTemplate(template, type, tag) { const starterTemplate = start_1.STARTER_TEMPLATES.find(t => t.projectType === type && t.name === template); if (starterTemplate && starterTemplate.type === 'managed') { return { ...starterTemplate, archive: `${start_1.STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starterTemplate.id}.tar.gz`, }; } const tasks = this.createTaskChain(); tasks.next('Looking up starter'); const starterList = await start_1.getStarterList(this.env.config, tag); const starter = starterList.starters.find(t => t.type === type && t.name === template); if (starter) { tasks.end(); return { name: starter.name, projectType: starter.type, archive: `${start_1.STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starter.id}.tar.gz`, }; } else { throw new errors_1.FatalException(`Unable to find starter template for ${color_1.input(template)}\n` + `If this is not a typo, please make sure it is a valid starter template within the starters repo: ${color_1.strong('https://github.com/ionic-team/starters')}`); } } async validateProjectType(type) { const projectTypes = start_1.getStarterProjectTypes(); if (!projectTypes.includes(type)) { throw new errors_1.FatalException(`${color_1.input(type)} is not a valid project type.\n` + `Please choose a different ${color_1.input('--type')}. Use ${color_1.input('ionic start --list')} to list all available starter templates.`); } } async validateProjectId(projectId) { if (!project_1.isValidProjectId(projectId)) { throw new errors_1.FatalException(`${color_1.input(projectId)} is not a valid package or directory name.\n` + `Please choose a different ${color_1.input('--project-id')}. Alphanumeric characters are always safe.`); } } async loadManifest(manifestPath) { try { return await start_1.readStarterManifest(manifestPath); } catch (e) { debug(`Error with manifest file ${color_1.strong(format_1.prettyPath(manifestPath))}: ${e}`); } } async performManifestOps(manifest) { if (manifest.welcome) { this.env.log.nl(); this.env.log.msg(`${color_1.strong('Starter Welcome')}:`); this.env.log.msg(manifest.welcome); } } async downloadStarterTemplate(projectDir, starterTemplate) { const { createRequest, download } = await Promise.resolve().then(() => require('../lib/utils/http')); const { tar } = await Promise.resolve().then(() => require('../lib/utils/archive')); const tasks = this.createTaskChain(); const task = tasks.next(`Downloading and extracting ${color_1.input(starterTemplate.name.toString())} starter`); debug('Tar extraction created for %s', projectDir); const ws = tar.extract({ cwd: projectDir }); const { req } = await createRequest('GET', starterTemplate.archive, this.env.config.getHTTPConfig()); await download(req, ws, { progress: (loaded, total) => task.progress(loaded, total) }); tasks.end(); } async showNextSteps(projectDir, cloned, linkConfirmed) { const steps = [ `Go to your ${cloned ? 'cloned' : 'newly created'} project: ${color_1.input(`cd ${format_1.prettyPath(projectDir)}`)}`, `Run ${color_1.input('ionic serve')} within the app directory to see your app`, `Build features and components: ${color_1.strong('https://ion.link/scaffolding-docs')}`, `Run your app on a hardware or virtual device: ${color_1.strong('https://ion.link/running-docs')}`, ]; if (linkConfirmed) { steps.push(`Push your code to Ionic Appflow to perform real-time updates, and more: ${color_1.input('git push ionic master')}`); } this.env.log.info(`${color_1.strong('Next Steps')}:\n${steps.map(s => `- ${s}`).join('\n')}`); } } exports.StartCommand = StartCommand;