UNPKG

5.2 kBJavaScriptView Raw
1/**
2 * Copyright 2017-present, Callstack.
3 * All rights reserved.
4 *
5 * @flow
6 */
7import type { Command } from './types';
8
9const minimist = require('minimist');
10const camelcaseKeys = require('camelcase-keys');
11const clear = require('clear');
12const inquirer = require('inquirer');
13const path = require('path');
14const chalk = require('chalk');
15
16const pjson = require('../package.json');
17const logger = require('./logger');
18const messages = require('./messages');
19const { MessageError } = require('./errors');
20
21const DEFAULT_COMMAND = require('./commands/start');
22
23const COMMANDS: Array<Command> = [
24 require('./commands/init'),
25 require('./commands/start'),
26 require('./commands/bundle'),
27 require('./commands/reload'),
28];
29
30const NOT_SUPPORTED_COMMANDS = [
31 'run-ios',
32 'run-android',
33 'library',
34 'unbundle',
35 'link',
36 'unlink',
37 'install',
38 'uninstall',
39 'upgrade',
40 'log-android',
41 'log-ios',
42 'dependencies',
43];
44
45const getDisplayName = (command: string, opts: { [key: string]: mixed }) => {
46 const list = Object.keys(opts).map(key => `--${key} ${String(opts[key])}`);
47
48 const {
49 npm_execpath: execPath,
50 npm_lifecycle_event: scriptName,
51 npm_config_argv: npmArgv,
52 } = process.env;
53
54 // Haul has been called directly
55 if (!execPath || !scriptName || !npmArgv) {
56 return `haul ${command} ${list.join(' ')}`;
57 }
58
59 const client = path.basename(execPath) === 'yarn.js' ? 'yarn' : 'npm';
60
61 if (client === 'npm') {
62 const argv = JSON.parse(npmArgv).original;
63
64 return [
65 'npm',
66 ...(argv.includes('--') ? argv.slice(0, argv.indexOf('--')) : argv),
67 '--',
68 ...list,
69 ].join(' ');
70 }
71
72 // Yarn doesn't have `npmArgv` support
73 const lifecycleScript = process.env[`npm_package_scripts_${scriptName}`];
74
75 // If it's `npm script` that already defines command, e.g. "start": "haul start"
76 // then, `yarn run start --` is enough. Otherwise, command has to be set.
77 const exec =
78 lifecycleScript && lifecycleScript.includes(command)
79 ? `yarn run ${scriptName}`
80 : `yarn run ${scriptName} ${command}`;
81
82 return `${exec} -- ${list.join(' ')}`;
83};
84
85async function validateOptions(options, flags) {
86 const acc = {};
87 const promptedAcc = {};
88
89 for (const option of options) {
90 const defaultValue =
91 typeof option.default === 'function'
92 ? option.default(acc)
93 : option.default;
94
95 const parse =
96 typeof option.parse === 'function' ? option.parse : val => val;
97
98 let value = flags[option.name] ? parse(flags[option.name]) : defaultValue;
99
100 const missingValue = option.required && typeof value === 'undefined';
101 const invalidOption =
102 option.choices &&
103 typeof value !== 'undefined' &&
104 typeof option.choices.find(c => c.value === value) === 'undefined';
105
106 if (missingValue || invalidOption) {
107 const message = option.choices ? 'Select' : 'Enter';
108
109 // eslint-disable-next-line no-await-in-loop
110 const question = await inquirer.prompt([
111 {
112 type: option.choices ? 'list' : 'input',
113 name: 'answer',
114 message: `${message} ${option.description.toLowerCase()}`,
115 choices: (option.choices || []).map(choice => ({
116 name: `${String(choice.value)} - ${choice.description}`,
117 value: choice.value,
118 short: choice.value,
119 })),
120 },
121 ]);
122
123 value = option.choices ? question.answer : parse(question.answer);
124
125 promptedAcc[option.name] = value;
126 }
127
128 acc[option.name] = value;
129 }
130
131 return { options: acc, promptedOptions: promptedAcc };
132}
133
134async function run(args: Array<string>) {
135 if (
136 args[0] === 'version' ||
137 args.includes('-v') ||
138 args.includes('--version')
139 ) {
140 console.log(`v${pjson.version}`);
141 return;
142 }
143
144 if (['--help', '-h', 'help'].includes(args[0])) {
145 console.log(messages.haulHelp(COMMANDS));
146 return;
147 }
148
149 if (NOT_SUPPORTED_COMMANDS.includes(args[0])) {
150 logger.info(messages.commandNotImplemented(args[0]));
151 return;
152 }
153
154 const command = COMMANDS.find(cmd => cmd.name === args[0]) || DEFAULT_COMMAND;
155
156 if (args.includes('--help') || args.includes('-h')) {
157 console.log(messages.haulCommandHelp(command));
158 return;
159 }
160
161 const opts = command.options || [];
162
163 const { _, ...flags } = camelcaseKeys(
164 minimist(args, {
165 string: opts.map(opt => opt.name),
166 })
167 );
168
169 if (command.adjustOptions) command.adjustOptions(flags);
170
171 const { options, promptedOptions } = await validateOptions(opts, flags);
172 const userDefinedOptions = { ...flags, ...promptedOptions };
173 const displayName = getDisplayName(command.name, userDefinedOptions);
174
175 if (Object.keys(promptedOptions).length) {
176 logger.info(`Running ${chalk.cyan(displayName)}`);
177 }
178
179 try {
180 await command.action(options);
181 } catch (error) {
182 clear();
183 if (error instanceof MessageError) {
184 logger.reset().error(error.message);
185 } else {
186 logger.reset().error(
187 messages.commandFailed({
188 command: displayName,
189 error,
190 stack: error && error.stack,
191 })
192 );
193 }
194 process.exit(1);
195 }
196}
197
198module.exports = run;
199module.exports.validateOptions = validateOptions;