UNPKG

6.66 kBJavaScriptView Raw
1import {dirname} from 'node:path';
2import process from 'node:process';
3import {fileURLToPath} from 'node:url';
4import buildParserOptions from 'minimist-options';
5import parseArguments from 'yargs-parser';
6import camelCaseKeys from 'camelcase-keys';
7import decamelize from 'decamelize';
8import decamelizeKeys from 'decamelize-keys';
9import trimNewlines from 'trim-newlines';
10import redent from 'redent';
11import {readPackageUpSync} from 'read-pkg-up';
12import hardRejection from 'hard-rejection';
13import normalizePackageData from 'normalize-package-data';
14
15const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => {
16 const flag = definedFlags[flagName];
17 let isFlagRequired = true;
18
19 if (typeof flag.isRequired === 'function') {
20 isFlagRequired = flag.isRequired(receivedFlags, input);
21 if (typeof isFlagRequired !== 'boolean') {
22 throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`);
23 }
24 }
25
26 if (typeof receivedFlags[flagName] === 'undefined') {
27 return isFlagRequired;
28 }
29
30 return flag.isMultiple && receivedFlags[flagName].length === 0 && isFlagRequired;
31};
32
33const getMissingRequiredFlags = (flags, receivedFlags, input) => {
34 const missingRequiredFlags = [];
35 if (typeof flags === 'undefined') {
36 return [];
37 }
38
39 for (const flagName of Object.keys(flags)) {
40 if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) {
41 missingRequiredFlags.push({key: flagName, ...flags[flagName]});
42 }
43 }
44
45 return missingRequiredFlags;
46};
47
48const reportMissingRequiredFlags = missingRequiredFlags => {
49 console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`);
50 for (const flag of missingRequiredFlags) {
51 console.error(`\t--${decamelize(flag.key, {separator: '-'})}${flag.alias ? `, -${flag.alias}` : ''}`);
52 }
53};
54
55const validateOptions = ({flags}) => {
56 const invalidFlags = Object.keys(flags).filter(flagKey => flagKey.includes('-') && flagKey !== '--');
57 if (invalidFlags.length > 0) {
58 throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`);
59 }
60};
61
62const reportUnknownFlags = unknownFlags => {
63 console.error([
64 `Unknown flag${unknownFlags.length > 1 ? 's' : ''}`,
65 ...unknownFlags,
66 ].join('\n'));
67};
68
69const buildParserFlags = ({flags, booleanDefault}) => {
70 const parserFlags = {};
71
72 for (const [flagKey, flagValue] of Object.entries(flags)) {
73 const flag = {...flagValue};
74
75 if (
76 typeof booleanDefault !== 'undefined'
77 && flag.type === 'boolean'
78 && !Object.prototype.hasOwnProperty.call(flag, 'default')
79 ) {
80 flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault;
81 }
82
83 if (flag.isMultiple) {
84 flag.type = flag.type ? `${flag.type}-array` : 'array';
85 flag.default = flag.default || [];
86 delete flag.isMultiple;
87 }
88
89 parserFlags[flagKey] = flag;
90 }
91
92 return parserFlags;
93};
94
95const validateFlags = (flags, options) => {
96 for (const [flagKey, flagValue] of Object.entries(options.flags)) {
97 if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) {
98 throw new Error(`The flag --${flagKey} can only be set once.`);
99 }
100 }
101};
102
103/* eslint complexity: off */
104const meow = (helpText, options = {}) => {
105 if (typeof helpText !== 'string') {
106 options = helpText;
107 helpText = '';
108 }
109
110 if (!(options.importMeta && options.importMeta.url)) {
111 throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.');
112 }
113
114 const foundPackage = readPackageUpSync({
115 cwd: dirname(fileURLToPath(options.importMeta.url)),
116 normalize: false,
117 });
118
119 options = {
120 pkg: foundPackage ? foundPackage.packageJson : {},
121 argv: process.argv.slice(2),
122 flags: {},
123 inferType: false,
124 input: 'string',
125 help: helpText,
126 autoHelp: true,
127 autoVersion: true,
128 booleanDefault: false,
129 hardRejection: true,
130 allowUnknownFlags: true,
131 ...options,
132 };
133
134 if (options.hardRejection) {
135 hardRejection();
136 }
137
138 validateOptions(options);
139 let parserOptions = {
140 arguments: options.input,
141 ...buildParserFlags(options),
142 };
143
144 parserOptions = decamelizeKeys(parserOptions, '-', {exclude: ['stopEarly', '--']});
145
146 if (options.inferType) {
147 delete parserOptions.arguments;
148 }
149
150 // Add --help and --version to known flags if autoHelp or autoVersion are set
151 if (!options.allowUnknownFlags) {
152 if (options.autoHelp) {
153 parserOptions.help = {type: 'boolean'};
154 }
155
156 if (options.autoVersion) {
157 parserOptions.version = {type: 'boolean'};
158 }
159 }
160
161 parserOptions = buildParserOptions(parserOptions);
162
163 parserOptions.configuration = {
164 ...parserOptions.configuration,
165 'greedy-arrays': false,
166 };
167
168 if (parserOptions['--']) {
169 parserOptions.configuration['populate--'] = true;
170 }
171
172 if (!options.allowUnknownFlags) {
173 // Collect unknown options in `argv._` to be checked later.
174 parserOptions.configuration['unknown-options-as-args'] = true;
175 }
176
177 const {pkg: package_} = options;
178 const argv = parseArguments(options.argv, parserOptions);
179 let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2);
180
181 normalizePackageData(package_);
182
183 process.title = package_.bin ? Object.keys(package_.bin)[0] : package_.name;
184
185 let {description} = options;
186 if (!description && description !== false) {
187 ({description} = package_);
188 }
189
190 help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n');
191
192 const showHelp = code => {
193 console.log(help);
194 process.exit(typeof code === 'number' ? code : 2);
195 };
196
197 const showVersion = () => {
198 console.log(typeof options.version === 'string' ? options.version : package_.version);
199 process.exit(0);
200 };
201
202 if (argv._.length === 0 && options.argv.length === 1) {
203 if (argv.version === true && options.autoVersion) {
204 showVersion();
205 } else if (argv.help === true && options.autoHelp) {
206 showHelp(0);
207 }
208 }
209
210 const input = argv._;
211 delete argv._;
212
213 if (!options.allowUnknownFlags) {
214 const unknownFlags = input.filter(item => typeof item === 'string' && item.startsWith('-'));
215 if (unknownFlags.length > 0) {
216 reportUnknownFlags(unknownFlags);
217 process.exit(2);
218 }
219 }
220
221 const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]});
222 const unnormalizedFlags = {...flags};
223
224 validateFlags(flags, options);
225
226 for (const flagValue of Object.values(options.flags)) {
227 delete flags[flagValue.alias];
228 }
229
230 const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input);
231 if (missingRequiredFlags.length > 0) {
232 reportMissingRequiredFlags(missingRequiredFlags);
233 process.exit(2);
234 }
235
236 return {
237 input,
238 flags,
239 unnormalizedFlags,
240 pkg: package_,
241 help,
242 showHelp,
243 showVersion,
244 };
245};
246
247export default meow;