1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import type { Command } from './types';
|
8 |
|
9 | const minimist = require('minimist');
|
10 | const camelcaseKeys = require('camelcase-keys');
|
11 | const clear = require('clear');
|
12 | const inquirer = require('inquirer');
|
13 | const path = require('path');
|
14 | const chalk = require('chalk');
|
15 |
|
16 | const pjson = require('../package.json');
|
17 | const logger = require('./logger');
|
18 | const messages = require('./messages');
|
19 | const { MessageError } = require('./errors');
|
20 |
|
21 | const DEFAULT_COMMAND = require('./commands/start');
|
22 |
|
23 | const COMMANDS: Array<Command> = [
|
24 | require('./commands/init'),
|
25 | require('./commands/start'),
|
26 | require('./commands/bundle'),
|
27 | require('./commands/reload'),
|
28 | ];
|
29 |
|
30 | const 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 |
|
45 | const 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 |
|
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 |
|
73 | const lifecycleScript = process.env[`npm_package_scripts_${scriptName}`];
|
74 |
|
75 |
|
76 |
|
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 |
|
85 | async 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 |
|
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 |
|
134 | async 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 |
|
198 | module.exports = run;
|
199 | module.exports.validateOptions = validateOptions;
|