UNPKG

6.38 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 parserOptions = buildParserOptions(parserOptions);
151
152 parserOptions.configuration = {
153 ...parserOptions.configuration,
154 'greedy-arrays': false,
155 };
156
157 if (parserOptions['--']) {
158 parserOptions.configuration['populate--'] = true;
159 }
160
161 if (!options.allowUnknownFlags) {
162 // Collect unknown options in `argv._` to be checked later.
163 parserOptions.configuration['unknown-options-as-args'] = true;
164 }
165
166 const {pkg: package_} = options;
167 const argv = parseArguments(options.argv, parserOptions);
168 let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2);
169
170 normalizePackageData(package_);
171
172 process.title = package_.bin ? Object.keys(package_.bin)[0] : package_.name;
173
174 let {description} = options;
175 if (!description && description !== false) {
176 ({description} = package_);
177 }
178
179 help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n');
180
181 const showHelp = code => {
182 console.log(help);
183 process.exit(typeof code === 'number' ? code : 2);
184 };
185
186 const showVersion = () => {
187 console.log(typeof options.version === 'string' ? options.version : package_.version);
188 process.exit(0);
189 };
190
191 if (argv._.length === 0 && options.argv.length === 1) {
192 if (argv.version === true && options.autoVersion) {
193 showVersion();
194 } else if (argv.help === true && options.autoHelp) {
195 showHelp(0);
196 }
197 }
198
199 const input = argv._;
200 delete argv._;
201
202 if (!options.allowUnknownFlags) {
203 const unknownFlags = input.filter(item => typeof item === 'string' && item.startsWith('-'));
204 if (unknownFlags.length > 0) {
205 reportUnknownFlags(unknownFlags);
206 process.exit(2);
207 }
208 }
209
210 const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]});
211 const unnormalizedFlags = {...flags};
212
213 validateFlags(flags, options);
214
215 for (const flagValue of Object.values(options.flags)) {
216 delete flags[flagValue.alias];
217 }
218
219 const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input);
220 if (missingRequiredFlags.length > 0) {
221 reportMissingRequiredFlags(missingRequiredFlags);
222 process.exit(2);
223 }
224
225 return {
226 input,
227 flags,
228 unnormalizedFlags,
229 pkg: package_,
230 help,
231 showHelp,
232 showVersion,
233 };
234};
235
236export default meow;