UNPKG

25.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const cli_framework_1 = require("@ionic/cli-framework");
4const format_1 = require("@ionic/cli-framework/utils/format");
5const string_1 = require("@ionic/cli-framework/utils/string");
6const utils_fs_1 = require("@ionic/utils-fs");
7const Debug = require("debug");
8const path = require("path");
9const constants_1 = require("../constants");
10const color_1 = require("../lib/color");
11const command_1 = require("../lib/command");
12const errors_1 = require("../lib/errors");
13const executor_1 = require("../lib/executor");
14const project_1 = require("../lib/project");
15const shell_1 = require("../lib/shell");
16const start_1 = require("../lib/start");
17const emoji_1 = require("../lib/utils/emoji");
18const debug = Debug('ionic:commands:start');
19class StartCommand extends command_1.Command {
20 constructor() {
21 super(...arguments);
22 this.canRemoveExisting = false;
23 }
24 async getMetadata() {
25 return {
26 name: 'start',
27 type: 'global',
28 summary: 'Create a new project',
29 description: `
30This command creates a working Ionic app. It installs dependencies for you and sets up your project.
31
32Running ${color_1.input('ionic start')} without any arguments will prompt you for information about your new project.
33
34The 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.
35
36The 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.
37
38Use 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.
39 `,
40 exampleCommands: [
41 '',
42 '--list',
43 'myApp',
44 'myApp blank',
45 'myApp tabs --cordova',
46 'myApp tabs --capacitor',
47 'myApp super --type=ionic-angular',
48 'myApp blank --type=ionic1',
49 'cordovaApp tabs --cordova',
50 '"My App" blank',
51 '"Conference App" https://github.com/ionic-team/ionic-conference-app',
52 ],
53 inputs: [
54 {
55 name: 'name',
56 summary: `The name of your new project (e.g. ${color_1.input('myApp')}, ${color_1.input('"My App"')})`,
57 validators: [cli_framework_1.validators.required],
58 },
59 {
60 name: 'template',
61 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)`,
62 validators: [cli_framework_1.validators.required],
63 },
64 ],
65 options: [
66 {
67 name: 'list',
68 summary: 'List available starter templates',
69 type: Boolean,
70 aliases: ['l'],
71 },
72 {
73 name: 'type',
74 summary: `Type of project to start (e.g. ${start_1.getStarterProjectTypes().map(type => color_1.input(type)).join(', ')})`,
75 type: String,
76 },
77 {
78 name: 'cordova',
79 summary: 'Include Cordova integration',
80 type: Boolean,
81 },
82 {
83 name: 'capacitor',
84 summary: 'Include Capacitor integration',
85 type: Boolean,
86 groups: ["experimental" /* EXPERIMENTAL */],
87 },
88 {
89 name: 'deps',
90 summary: 'Do not install npm/yarn dependencies',
91 type: Boolean,
92 default: true,
93 groups: ["advanced" /* ADVANCED */],
94 },
95 {
96 name: 'git',
97 summary: 'Do not initialize a git repo',
98 type: Boolean,
99 default: true,
100 groups: ["advanced" /* ADVANCED */],
101 },
102 {
103 name: 'link',
104 summary: 'Connect your new app to Ionic',
105 type: Boolean,
106 groups: ["advanced" /* ADVANCED */],
107 },
108 {
109 name: 'id',
110 summary: 'Specify an Ionic App ID to link',
111 },
112 {
113 name: 'project-id',
114 summary: 'Specify a slug for your app (used for the directory name and package name)',
115 groups: ["advanced" /* ADVANCED */],
116 spec: { value: 'slug' },
117 },
118 {
119 name: 'package-id',
120 summary: 'Specify the bundle ID/application ID for your app (reverse-DNS notation)',
121 groups: ["advanced" /* ADVANCED */],
122 spec: { value: 'id' },
123 },
124 {
125 name: 'tag',
126 summary: `Specify a tag to use for the starters (e.g. ${['latest', 'testing', 'next'].map(t => color_1.input(t)).join(', ')})`,
127 default: 'latest',
128 groups: ["hidden" /* HIDDEN */],
129 },
130 ],
131 };
132 }
133 async preRun(inputs, options) {
134 const { promptToLogin } = await Promise.resolve().then(() => require('../lib/session'));
135 start_1.verifyOptions(options, this.env);
136 const appflowId = options['id'] ? String(options['id']) : undefined;
137 if (appflowId) {
138 if (!this.env.session.isLoggedIn()) {
139 await promptToLogin(this.env);
140 }
141 }
142 if (!inputs[0]) {
143 if (appflowId) {
144 const { AppClient } = await Promise.resolve().then(() => require('../lib/app'));
145 const token = this.env.session.getUserToken();
146 const appClient = new AppClient(token, this.env);
147 const tasks = this.createTaskChain();
148 tasks.next(`Looking up app ${color_1.input(appflowId)}`);
149 const app = await appClient.load(appflowId);
150 // TODO: can ask to clone via repo_url
151 tasks.end();
152 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')}.`);
153 inputs[0] = app.name;
154 options['project-id'] = app.slug;
155 }
156 else {
157 if (this.env.flags.interactive) {
158 this.env.log.nl();
159 this.env.log.msg(`${color_1.strong(`Every great app needs a name! ${emoji_1.emoji('😍', '')}`)}\n` +
160 `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`);
161 }
162 const name = await this.env.prompt({
163 type: 'input',
164 name: 'name',
165 message: 'Project name:',
166 validate: v => cli_framework_1.validators.required(v),
167 });
168 inputs[0] = name;
169 }
170 }
171 const projectType = options['type'] ? String(options['type']) : await this.getProjectType();
172 if (!inputs[1]) {
173 if (this.env.flags.interactive) {
174 this.env.log.nl();
175 this.env.log.msg(`${color_1.strong(`Let's pick the perfect starter template! ${emoji_1.emoji('💪', '')}`)}\n` +
176 `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`);
177 }
178 const template = await this.env.prompt({
179 type: 'list',
180 name: 'template',
181 message: 'Starter template:',
182 choices: () => {
183 const starterTemplateList = start_1.STARTER_TEMPLATES.filter(st => st.projectType === projectType);
184 const cols = format_1.columnar(starterTemplateList.map(({ name, description }) => [color_1.input(name), description || '']), {}).split('\n');
185 if (starterTemplateList.length === 0) {
186 throw new errors_1.FatalException(`No starter templates found for project type: ${color_1.input(projectType)}.`);
187 }
188 return starterTemplateList.map((starter, i) => {
189 return {
190 name: cols[i],
191 short: starter.name,
192 value: starter.name,
193 };
194 });
195 },
196 });
197 inputs[1] = template;
198 }
199 const starterTemplate = start_1.STARTER_TEMPLATES.find(t => t.name === inputs[1] && t.projectType === projectType);
200 if (starterTemplate && starterTemplate.type === 'repo') {
201 inputs[1] = starterTemplate.repo;
202 }
203 const cloned = string_1.isValidURL(inputs[1]);
204 if (this.project && this.project.details.context === 'app') {
205 const confirm = await this.env.prompt({
206 type: 'confirm',
207 name: 'confirm',
208 message: 'You are already in an Ionic project directory. Do you really want to start another project here?',
209 default: false,
210 });
211 if (!confirm) {
212 this.env.log.info('Not starting project within existing project.');
213 throw new errors_1.FatalException();
214 }
215 }
216 await this.validateProjectType(projectType);
217 if (cloned) {
218 if (!options['git']) {
219 this.env.log.warn(`The ${color_1.input('--no-git')} option has no effect when cloning apps. Git must be used.`);
220 }
221 options['git'] = true;
222 }
223 if (options['v1'] || options['v2']) {
224 throw new errors_1.FatalException(`The ${color_1.input('--v1')} and ${color_1.input('--v2')} flags have been removed.\n` +
225 `Use the ${color_1.input('--type')} option. (see ${color_1.input('ionic start --help')})`);
226 }
227 if (options['app-name']) {
228 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"')}`);
229 }
230 if (options['display-name']) {
231 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"')}`);
232 }
233 if (options['bundle-id']) {
234 this.env.log.warn(`The ${color_1.input('--bundle-id')} option has been deprecated. Please use ${color_1.input('--package-id')}.`);
235 options['package-id'] = options['bundle-id'];
236 }
237 let projectId = options['project-id'] ? String(options['project-id']) : undefined;
238 if (projectId) {
239 await this.validateProjectId(projectId);
240 }
241 else {
242 projectId = options['project-id'] = project_1.isValidProjectId(inputs[0]) ? inputs[0] : string_1.slugify(inputs[0]);
243 }
244 const projectDir = path.resolve(projectId);
245 const packageId = options['package-id'] ? String(options['package-id']) : undefined;
246 if (projectId) {
247 await this.checkForExisting(projectDir);
248 }
249 if (cloned) {
250 this.schema = {
251 cloned: true,
252 url: inputs[1],
253 projectId,
254 projectDir,
255 };
256 }
257 else {
258 this.schema = {
259 cloned: false,
260 name: inputs[0],
261 type: projectType,
262 template: inputs[1],
263 projectId,
264 projectDir,
265 packageId,
266 appflowId,
267 };
268 }
269 }
270 async getProjectType() {
271 if (this.env.flags.interactive) {
272 this.env.log.nl();
273 this.env.log.msg(`${color_1.strong(`Pick a framework! ${emoji_1.emoji('😁', '')}`)}\n\n` +
274 `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`);
275 }
276 const frameworkChoice = await this.env.prompt({
277 type: 'list',
278 name: 'frameworks',
279 message: 'Framework:',
280 default: 'angular',
281 choices: () => {
282 const cols = format_1.columnar(start_1.SUPPORTED_FRAMEWORKS.map(({ name, description }) => [color_1.input(name), description]), {}).split('\n');
283 return start_1.SUPPORTED_FRAMEWORKS.map((starterTemplate, i) => {
284 return {
285 name: cols[i],
286 short: starterTemplate.name,
287 value: starterTemplate.type,
288 };
289 });
290 },
291 });
292 return frameworkChoice;
293 }
294 async run(inputs, options, runinfo) {
295 const { pkgManagerArgs } = await Promise.resolve().then(() => require('../lib/utils/npm'));
296 const { getTopLevel, isGitInstalled } = await Promise.resolve().then(() => require('../lib/git'));
297 if (!this.schema) {
298 throw new errors_1.FatalException(`Invalid start schema: cannot start app.`);
299 }
300 const { projectId, projectDir, packageId, appflowId } = this.schema;
301 const tag = options['tag'] ? String(options['tag']) : 'latest';
302 let linkConfirmed = typeof appflowId === 'string';
303 const gitDesired = options['git'] ? true : false;
304 const gitInstalled = await isGitInstalled(this.env);
305 const gitTopLevel = await getTopLevel(this.env);
306 let gitIntegration = gitDesired && gitInstalled && !gitTopLevel ? true : false;
307 if (!gitInstalled) {
308 const installationDocs = `See installation docs for git: ${color_1.strong('https://git-scm.com/book/en/v2/Getting-Started-Installing-Git')}`;
309 if (appflowId) {
310 throw new errors_1.FatalException(`Git CLI not found on your PATH.\n` +
311 `Git must be installed to connect this app to Ionic. ${installationDocs}`);
312 }
313 if (this.schema.cloned) {
314 throw new errors_1.FatalException(`Git CLI not found on your PATH.\n` +
315 `Git must be installed to clone apps with ${color_1.input('ionic start')}. ${installationDocs}`);
316 }
317 }
318 if (gitTopLevel && !this.schema.cloned) {
319 this.env.log.info(`Existing git project found (${color_1.strong(gitTopLevel)}). Git operations are disabled.`);
320 }
321 const tasks = this.createTaskChain();
322 tasks.next(`Preparing directory ${color_1.input(format_1.prettyPath(projectDir))}`);
323 if (this.canRemoveExisting) {
324 await utils_fs_1.remove(projectDir);
325 }
326 await utils_fs_1.mkdir(projectDir);
327 tasks.end();
328 if (this.schema.cloned) {
329 await this.env.shell.run('git', ['clone', this.schema.url, projectDir, '--progress'], { stdio: 'inherit' });
330 }
331 else {
332 const starterTemplate = await this.findStarterTemplate(this.schema.template, this.schema.type, tag);
333 await this.downloadStarterTemplate(projectDir, starterTemplate);
334 }
335 let project;
336 if (this.project && this.project.details.context === 'multiapp' && !this.schema.cloned) {
337 // We're in a multi-app setup, so the new config file isn't wanted.
338 await utils_fs_1.unlink(path.resolve(projectDir, 'ionic.config.json'));
339 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);
340 project.config.set('type', this.schema.type);
341 project.config.set('root', path.relative(this.project.rootDirectory, projectDir));
342 }
343 else {
344 project = await project_1.createProjectFromDirectory(projectDir, { _: [] }, this.env, { logErrors: false });
345 }
346 // start is weird, once the project directory is created, it becomes a
347 // "project" command and so we replace the `Project` instance that was
348 // autogenerated when the CLI booted up. This has worked thus far?
349 this.namespace.root.project = project;
350 if (!this.project) {
351 throw new errors_1.FatalException('Error while loading project.');
352 }
353 this.env.shell.alterPath = p => shell_1.prependNodeModulesBinToPath(projectDir, p);
354 if (!this.schema.cloned) {
355 if (typeof options['cordova'] === 'undefined' && !options['capacitor']) {
356 const confirm = await this.env.prompt({
357 type: 'confirm',
358 name: 'confirm',
359 message: 'Integrate your new app with Cordova to target native iOS and Android?',
360 default: false,
361 });
362 if (confirm) {
363 options['cordova'] = true;
364 }
365 }
366 if (options['cordova']) {
367 await executor_1.runCommand(runinfo, ['integrations', 'enable', 'cordova', '--quiet']);
368 }
369 if (options['capacitor']) {
370 await executor_1.runCommand(runinfo, ['integrations', 'enable', 'capacitor', '--quiet', '--', this.schema.name, packageId ? packageId : 'io.ionic.starter']);
371 }
372 await this.project.personalize({ name: this.schema.name, projectId, packageId });
373 this.env.log.nl();
374 }
375 const shellOptions = { cwd: projectDir, stdio: 'inherit' };
376 if (options['deps']) {
377 this.env.log.msg('Installing dependencies may take several minutes.');
378 this.env.log.rawmsg(start_1.getAdvertisement());
379 const [installer, ...installerArgs] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install' });
380 await this.env.shell.run(installer, installerArgs, shellOptions);
381 }
382 if (!this.schema.cloned) {
383 if (gitIntegration) {
384 try {
385 await this.env.shell.run('git', ['init'], shellOptions); // TODO: use initializeRepo()?
386 }
387 catch (e) {
388 this.env.log.warn('Error encountered during repo initialization. Disabling further git operations.');
389 gitIntegration = false;
390 }
391 }
392 if (options['link']) {
393 const cmdArgs = ['link'];
394 if (appflowId) {
395 cmdArgs.push(appflowId);
396 }
397 cmdArgs.push('--name', this.schema.name);
398 await executor_1.runCommand(runinfo, cmdArgs);
399 linkConfirmed = true;
400 }
401 const manifestPath = path.resolve(projectDir, 'ionic.starter.json');
402 const manifest = await this.loadManifest(manifestPath);
403 if (manifest) {
404 await utils_fs_1.unlink(manifestPath);
405 }
406 if (gitIntegration) {
407 try {
408 await this.env.shell.run('git', ['add', '-A'], shellOptions);
409 await this.env.shell.run('git', ['commit', '-m', 'Initial commit', '--no-gpg-sign'], shellOptions);
410 }
411 catch (e) {
412 this.env.log.warn('Error encountered during commit. Disabling further git operations.');
413 gitIntegration = false;
414 }
415 }
416 if (manifest) {
417 await this.performManifestOps(manifest);
418 }
419 }
420 this.env.log.nl();
421 await this.showNextSteps(projectDir, this.schema.cloned, linkConfirmed);
422 }
423 async checkForExisting(projectDir) {
424 const projectExists = await utils_fs_1.pathExists(projectDir);
425 if (projectExists) {
426 const confirm = await this.env.prompt({
427 type: 'confirm',
428 name: 'confirm',
429 message: `${color_1.input(format_1.prettyPath(projectDir))} exists. ${color_1.failure('Overwrite?')}`,
430 default: false,
431 });
432 if (!confirm) {
433 this.env.log.msg(`Not erasing existing project in ${color_1.input(format_1.prettyPath(projectDir))}.`);
434 throw new errors_1.FatalException();
435 }
436 this.canRemoveExisting = confirm;
437 }
438 }
439 async findStarterTemplate(template, type, tag) {
440 const starterTemplate = start_1.STARTER_TEMPLATES.find(t => t.projectType === type && t.name === template);
441 if (starterTemplate && starterTemplate.type === 'managed') {
442 return {
443 ...starterTemplate,
444 archive: `${start_1.STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starterTemplate.id}.tar.gz`,
445 };
446 }
447 const tasks = this.createTaskChain();
448 tasks.next('Looking up starter');
449 const starterList = await start_1.getStarterList(this.env.config, tag);
450 const starter = starterList.starters.find(t => t.type === type && t.name === template);
451 if (starter) {
452 tasks.end();
453 return {
454 name: starter.name,
455 projectType: starter.type,
456 archive: `${start_1.STARTER_BASE_URL}/${tag === 'latest' ? '' : `${tag}/`}${starter.id}.tar.gz`,
457 };
458 }
459 else {
460 throw new errors_1.FatalException(`Unable to find starter template for ${color_1.input(template)}\n` +
461 `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')}`);
462 }
463 }
464 async validateProjectType(type) {
465 const projectTypes = start_1.getStarterProjectTypes();
466 if (!projectTypes.includes(type)) {
467 throw new errors_1.FatalException(`${color_1.input(type)} is not a valid project type.\n` +
468 `Please choose a different ${color_1.input('--type')}. Use ${color_1.input('ionic start --list')} to list all available starter templates.`);
469 }
470 }
471 async validateProjectId(projectId) {
472 if (!project_1.isValidProjectId(projectId)) {
473 throw new errors_1.FatalException(`${color_1.input(projectId)} is not a valid package or directory name.\n` +
474 `Please choose a different ${color_1.input('--project-id')}. Alphanumeric characters are always safe.`);
475 }
476 }
477 async loadManifest(manifestPath) {
478 try {
479 return await start_1.readStarterManifest(manifestPath);
480 }
481 catch (e) {
482 debug(`Error with manifest file ${color_1.strong(format_1.prettyPath(manifestPath))}: ${e}`);
483 }
484 }
485 async performManifestOps(manifest) {
486 if (manifest.welcome) {
487 this.env.log.nl();
488 this.env.log.msg(`${color_1.strong('Starter Welcome')}:`);
489 this.env.log.msg(manifest.welcome);
490 }
491 }
492 async downloadStarterTemplate(projectDir, starterTemplate) {
493 const { createRequest, download } = await Promise.resolve().then(() => require('../lib/utils/http'));
494 const { tar } = await Promise.resolve().then(() => require('../lib/utils/archive'));
495 const tasks = this.createTaskChain();
496 const task = tasks.next(`Downloading and extracting ${color_1.input(starterTemplate.name.toString())} starter`);
497 debug('Tar extraction created for %s', projectDir);
498 const ws = tar.extract({ cwd: projectDir });
499 const { req } = await createRequest('GET', starterTemplate.archive, this.env.config.getHTTPConfig());
500 await download(req, ws, { progress: (loaded, total) => task.progress(loaded, total) });
501 tasks.end();
502 }
503 async showNextSteps(projectDir, cloned, linkConfirmed) {
504 const steps = [
505 `Go to your ${cloned ? 'cloned' : 'newly created'} project: ${color_1.input(`cd ${format_1.prettyPath(projectDir)}`)}`,
506 `Run ${color_1.input('ionic serve')} within the app directory to see your app`,
507 `Build features and components: ${color_1.strong('https://ion.link/scaffolding-docs')}`,
508 `Run your app on a hardware or virtual device: ${color_1.strong('https://ion.link/running-docs')}`,
509 ];
510 if (linkConfirmed) {
511 steps.push(`Push your code to Ionic Appflow to perform real-time updates, and more: ${color_1.input('git push ionic master')}`);
512 }
513 this.env.log.info(`${color_1.strong('Next Steps')}:\n${steps.map(s => `- ${s}`).join('\n')}`);
514 }
515}
516exports.StartCommand = StartCommand;