Skip to content

Commit 648f69c

Browse files
add types
1 parent 02c9761 commit 648f69c

25 files changed

+386
-170
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
build
33
yarn.lock
44
test-d/build.ts
5+
.tsimp

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@
6363
"@rollup/plugin-commonjs": "^25.0.7",
6464
"@rollup/plugin-json": "^6.1.0",
6565
"@rollup/plugin-node-resolve": "^15.2.3",
66+
"@sindresorhus/is": "^6.2.0",
6667
"@sindresorhus/tsconfig": "^5.0.0",
6768
"@types/common-tags": "^1.8.4",
6869
"@types/minimist": "^1.2.5",
6970
"@types/node": "18",
71+
"@types/stack-utils": "^2.0.3",
7072
"@types/yargs-parser": "^21.0.3",
7173
"ava": "^6.1.2",
7274
"camelcase-keys": "^9.1.3",

rollup.config.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ const dtsConfig = defineConfig({
7979
name: 'copy-tsd',
8080
async generateBundle() {
8181
let tsdFile = await fs.readFile('./test-d/index.ts', 'utf8');
82+
8283
tsdFile = tsdFile.replace(
83-
`import meow from '../${sourceDirectory}/index.js'`,
84-
`import meow from '../${outputDirectory}/index.js'`,
84+
`../${sourceDirectory}/index.js`,
85+
`../${outputDirectory}/index.js`,
8586
);
8687

8788
await fs.writeFile(`./test-d/${outputDirectory}.ts`, tsdFile);

source/index.ts

+77-21
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import process from 'node:process';
2-
import parseArguments from 'yargs-parser';
2+
import parseArguments, {type Options as ParserOptions} from 'yargs-parser';
33
import camelCaseKeys from 'camelcase-keys';
44
import {trimNewlines} from 'trim-newlines';
55
import redent from 'redent';
66
import {buildOptions} from './options.js';
77
import {buildParserOptions} from './parser.js';
88
import {validate, checkUnknownFlags, checkMissingRequiredFlags} from './validate.js';
9-
10-
const buildResult = ({pkg: packageJson, ...options}, parserOptions) => {
11-
const argv = parseArguments(options.argv, parserOptions);
9+
import type {
10+
Options,
11+
ParsedOptions,
12+
Result,
13+
AnyFlags,
14+
} from './types.js';
15+
16+
const buildResult = <Flags extends AnyFlags = AnyFlags>({pkg: packageJson, ...options}: ParsedOptions, parserOptions: ParserOptions): Result<Flags> => {
17+
const {_: input, ...argv} = parseArguments(options.argv as string[], parserOptions);
1218
let help = '';
1319

1420
if (options.help) {
@@ -32,7 +38,7 @@ const buildResult = ({pkg: packageJson, ...options}, parserOptions) => {
3238

3339
help += '\n';
3440

35-
const showHelp = code => {
41+
const showHelp = (code?: number) => {
3642
console.log(help);
3743
process.exit(typeof code === 'number' ? code : 2); // Default to code 2 for incorrect usage (#47)
3844
};
@@ -42,17 +48,14 @@ const buildResult = ({pkg: packageJson, ...options}, parserOptions) => {
4248
process.exit(0);
4349
};
4450

45-
if (argv._.length === 0 && options.argv.length === 1) {
46-
if (argv.version === true && options.autoVersion) {
51+
if (input.length === 0 && options.argv.length === 1) {
52+
if (argv['version'] === true && options.autoVersion) {
4753
showVersion();
48-
} else if (argv.help === true && options.autoHelp) {
54+
} else if (argv['help'] === true && options.autoHelp) {
4955
showHelp(0);
5056
}
5157
}
5258

53-
const input = argv._;
54-
delete argv._;
55-
5659
if (!options.allowUnknownFlags) {
5760
checkUnknownFlags(input);
5861
}
@@ -69,12 +72,14 @@ const buildResult = ({pkg: packageJson, ...options}, parserOptions) => {
6972
}
7073
}
7174

72-
delete flags[flagValue.shortFlag];
75+
if (flagValue.shortFlag) {
76+
delete flags[flagValue.shortFlag];
77+
}
7378
}
7479

75-
checkMissingRequiredFlags(options.flags, flags, input);
80+
checkMissingRequiredFlags(options.flags, flags, input as string[]);
7681

77-
return {
82+
const result = {
7883
input,
7984
flags,
8085
unnormalizedFlags,
@@ -83,16 +88,67 @@ const buildResult = ({pkg: packageJson, ...options}, parserOptions) => {
8388
showHelp,
8489
showVersion,
8590
};
91+
92+
return result as unknown as Result<Flags>;
8693
};
8794

88-
const meow = (helpText, options = {}) => {
89-
const parsedOptions = buildOptions(helpText, options);
95+
/**
96+
@param helpMessage - Shortcut for the `help` option.
97+
98+
@example
99+
```
100+
#!/usr/bin/env node
101+
import meow from 'meow';
102+
import foo from './index.js';
103+
104+
const cli = meow(`
105+
Usage
106+
$ foo <input>
107+
108+
Options
109+
--rainbow, -r Include a rainbow
110+
111+
Examples
112+
$ foo unicorns --rainbow
113+
🌈 unicorns 🌈
114+
`, {
115+
importMeta: import.meta, // This is required
116+
flags: {
117+
rainbow: {
118+
type: 'boolean',
119+
shortFlag: 'r'
120+
}
121+
}
122+
});
123+
124+
//{
125+
// input: ['unicorns'],
126+
// flags: {rainbow: true},
127+
// ...
128+
//}
129+
130+
foo(cli.input.at(0), cli.flags);
131+
```
132+
*/
133+
export default function meow<Flags extends AnyFlags>(helpMessage: string, options: Options<Flags>): Result<Flags>;
134+
export default function meow<Flags extends AnyFlags>(options: Options<Flags>): Result<Flags>;
135+
136+
export default function meow<Flags extends AnyFlags = AnyFlags>(helpMessage: string | Options<Flags>, options?: Options<Flags>): Result<Flags> {
137+
if (typeof helpMessage !== 'string') {
138+
options = helpMessage;
139+
helpMessage = '';
140+
}
141+
142+
const parsedOptions = buildOptions(helpMessage, options!);
90143
const parserOptions = buildParserOptions(parsedOptions);
91-
const result = buildResult(parsedOptions, parserOptions);
144+
const result = buildResult<Flags>(parsedOptions, parserOptions);
92145

93-
process.title = result.pkg.bin ? Object.keys(result.pkg.bin).at(0) : result.pkg.name;
146+
const packageTitle = result.pkg.bin ? Object.keys(result.pkg.bin).at(0) : result.pkg.name;
94147

95-
return result;
96-
};
148+
// TODO: move to separate PR?
149+
if (packageTitle) {
150+
process.title = packageTitle;
151+
}
97152

98-
export default meow;
153+
return result;
154+
}

source/minimist-options.d.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
declare module 'minimist-options' {
3+
import type {Opts as MinimistOptions} from 'minimist';
4+
5+
export type {Opts as MinimistOptions} from 'minimist';
6+
7+
export type OptionType = 'string' | 'boolean' | 'number' | 'array' | 'string-array' | 'boolean-array' | 'number-array';
8+
9+
export type BaseOption<
10+
TypeOptionType extends OptionType,
11+
DefaultOptionType,
12+
> = {
13+
/**
14+
* The data type the option should be parsed to.
15+
*/
16+
readonly type?: TypeOptionType;
17+
18+
/**
19+
* An alias/list of aliases for the option.
20+
*/
21+
readonly alias?: string | readonly string[];
22+
23+
/**
24+
* The default value for the option.
25+
*/
26+
readonly default?: DefaultOptionType;
27+
};
28+
29+
export type StringOption = BaseOption<'string', string>;
30+
export type BooleanOption = BaseOption<'boolean', boolean>;
31+
export type NumberOption = BaseOption<'number', number>;
32+
export type DefaultArrayOption = BaseOption<'array', readonly string[]>;
33+
export type StringArrayOption = BaseOption<'string-array', readonly string[]>;
34+
export type BooleanArrayOption = BaseOption<'boolean-array', readonly boolean[]>;
35+
export type NumberArrayOption = BaseOption<'number-array', readonly number[]>;
36+
37+
export type AnyOption = (
38+
| StringOption
39+
| BooleanOption
40+
| NumberOption
41+
| DefaultArrayOption
42+
| StringArrayOption
43+
| BooleanArrayOption
44+
| NumberArrayOption
45+
);
46+
47+
export type MinimistOption = Pick<MinimistOptions, 'stopEarly' | 'unknown' | '--'>;
48+
49+
export type Options = MinimistOption & {
50+
[key: string]: (
51+
| OptionType
52+
| StringOption
53+
| BooleanOption
54+
| NumberOption
55+
| DefaultArrayOption
56+
| StringArrayOption
57+
| BooleanArrayOption
58+
| NumberArrayOption
59+
);
60+
arguments?: string;
61+
};
62+
63+
/**
64+
* Write options for [minimist](https://npmjs.org/package/minimist) in a comfortable way. Support string, boolean, number and array options.
65+
*/
66+
export default function buildOptions(options?: Options): MinimistOptions;
67+
}

source/options.ts

+30-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import process from 'node:process';
22
import {dirname} from 'node:path';
33
import {fileURLToPath} from 'node:url';
4-
import {readPackageUpSync} from 'read-package-up';
4+
import {readPackageUpSync, type PackageJson} from 'read-package-up';
55
import normalizePackageData from 'normalize-package-data';
66
import {decamelizeFlagKey, joinFlagKeys} from './utils.js';
7+
import type {
8+
Options,
9+
ParsedOptions,
10+
AnyFlag,
11+
AnyFlags,
12+
} from './types.js';
713

8-
const validateOptions = options => {
9-
const invalidOptionFilters = {
14+
type InvalidOptionFilter = {
15+
filter: (flag: [flagKey: string, flag: AnyFlag]) => boolean;
16+
message: (flagKeys: string[]) => string;
17+
};
18+
19+
type InvalidOptionFilters = {
20+
flags: Record<string, InvalidOptionFilter>;
21+
};
22+
23+
const validateOptions = (options: ParsedOptions): void => {
24+
const invalidOptionFilters: InvalidOptionFilters = {
1025
flags: {
1126
keyContainsDashes: {
1227
filter: ([flagKey]) => flagKey.includes('-') && flagKey !== '--',
@@ -21,22 +36,23 @@ const validateOptions = options => {
2136
message: flagKeys => `The option \`choices\` must be an array. Invalid flags: ${joinFlagKeys(flagKeys)}`,
2237
},
2338
choicesNotMatchFlagType: {
24-
filter: ([, flag]) => flag.type && Array.isArray(flag.choices) && flag.choices.some(choice => typeof choice !== flag.type),
39+
filter: ([, flag]) => flag.type !== undefined && Array.isArray(flag.choices) && flag.choices.some(choice => typeof choice !== flag.type),
2540
message(flagKeys) {
26-
const flagKeysAndTypes = flagKeys.map(flagKey => `(\`${decamelizeFlagKey(flagKey)}\`, type: '${options.flags[flagKey].type}')`);
41+
const flagKeysAndTypes = flagKeys.map(flagKey => `(\`${decamelizeFlagKey(flagKey)}\`, type: '${options.flags[flagKey]!.type}')`);
2742
return `Each value of the option \`choices\` must be of the same type as its flag. Invalid flags: ${flagKeysAndTypes.join(', ')}`;
2843
},
2944
},
3045
defaultNotInChoices: {
31-
filter: ([, flag]) => flag.default && Array.isArray(flag.choices) && ![flag.default].flat().every(value => flag.choices.includes(value)),
46+
filter: ([, flag]) => flag.default !== undefined && Array.isArray(flag.choices) && ![flag.default].flat().every(value => flag.choices!.includes(value as never)), // TODO: never?
3247
message: flagKeys => `Each value of the option \`default\` must exist within the option \`choices\`. Invalid flags: ${joinFlagKeys(flagKeys)}`,
3348
},
3449
},
3550
};
3651

3752
const errorMessages = [];
53+
type Entry = ['flags', Record<string, InvalidOptionFilter>];
3854

39-
for (const [optionKey, filters] of Object.entries(invalidOptionFilters)) {
55+
for (const [optionKey, filters] of Object.entries(invalidOptionFilters) as Entry[]) {
4056
const optionEntries = Object.entries(options[optionKey]);
4157

4258
for (const {filter, message} of Object.values(filters)) {
@@ -54,17 +70,12 @@ const validateOptions = options => {
5470
}
5571
};
5672

57-
export const buildOptions = (helpText, options) => {
58-
if (typeof helpText !== 'string') {
59-
options = helpText;
60-
helpText = '';
61-
}
62-
63-
if (!options.importMeta?.url) {
73+
export const buildOptions = (helpMessage: string, options: Options<AnyFlags>): ParsedOptions => {
74+
if (!options?.importMeta?.url) {
6475
throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.');
6576
}
6677

67-
const foundPackage = options.pkg ?? readPackageUpSync({
78+
const foundPackage = options.pkg as PackageJson ?? readPackageUpSync({
6879
cwd: dirname(fileURLToPath(options.importMeta.url)),
6980
normalize: false,
7081
})?.packageJson;
@@ -73,14 +84,14 @@ export const buildOptions = (helpText, options) => {
7384
const pkg = foundPackage ?? {};
7485
normalizePackageData(pkg);
7586

76-
const parsedOptions = {
87+
const parsedOptions: ParsedOptions = {
7788
argv: process.argv.slice(2),
7889
flags: {},
7990
inferType: false,
80-
input: 'string',
91+
input: 'string', // TODO: undocumented option?
8192
description: pkg.description ?? false,
82-
help: helpText,
83-
version: pkg.version || 'No version found',
93+
help: helpMessage,
94+
version: pkg.version || 'No version found', // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
8495
autoHelp: true,
8596
autoVersion: true,
8697
booleanDefault: false,

0 commit comments

Comments
 (0)