From 940938be4d7b2b38c8b810408bd3e16d8ace8d47 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sun, 28 Jan 2024 01:20:18 -0600 Subject: [PATCH] fix types --- .gitignore | 1 + package.json | 17 +-- rollup.config.js | 35 +++-- source/index.ts | 84 ++++++++--- source/minimist-options.d.ts | 4 +- source/options.ts | 22 +-- source/parser.ts | 13 +- source/types.ts | 60 +------- source/validate.ts | 6 +- test-d/build.test-d.ts | 132 ------------------ test-d/index.ts | 8 +- test/_utils.ts | 20 ++- test/build.js | 2 +- test/errors.ts | 2 +- .../required/fixture-required-function.ts | 8 +- test/help.ts | 39 +++++- test/test.ts | 10 +- test/version.ts | 2 +- tsconfig.json | 7 +- 19 files changed, 194 insertions(+), 278 deletions(-) delete mode 100644 test-d/build.test-d.ts diff --git a/.gitignore b/.gitignore index 04186b0..8404740 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules build yarn.lock .tsimp +test-d/build.ts diff --git a/package.json b/package.json index ab60a0f..74ea9a8 100644 --- a/package.json +++ b/package.json @@ -64,17 +64,17 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", + "@types/common-tags": "^1.8.4", "@sindresorhus/tsconfig": "^5.0.0", "@types/minimist": "^1.2.5", "@types/node": "~18.18", "@types/yargs-parser": "^21.0.3", "ava": "^6.1.0", "camelcase-keys": "^9.1.3", + "common-tags": "^2.0.0-alpha.1", "cross-env": "^7.0.3", "decamelize": "^6.0.0", "decamelize-keys": "^2.0.1", - "dedent": "^1.5.1", "delete_comments": "^0.0.2", "execa": "^8.0.1", "globby": "^14.0.0", @@ -88,10 +88,10 @@ "rollup": "^4.9.6", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-license": "^3.2.0", + "rollup-plugin-ts": "^3.4.5", "trim-newlines": "^5.0.0", "tsd": "^0.30.4", "tsimp": "^2.0.10", - "tslib": "^2.6.2", "type-fest": "^4.10.1", "typescript": "~5.3.3", "xo": "^0.56.0", @@ -108,14 +108,15 @@ ] }, "ava": { - "files": [ - "test/*.ts" - ], "extensions": { - "ts": "module" + "ts": "module", + "js": true }, "nodeArguments": [ "--import=tsimp" - ] + ], + "environmentVariables": { + "TSIMP_DIAG": "ignore" + } } } diff --git a/rollup.config.js b/rollup.config.js index a15915c..632e591 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,7 +1,8 @@ +import fs from 'node:fs/promises'; import {defineConfig} from 'rollup'; import {nodeResolve} from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; +import typescript from 'rollup-plugin-ts'; import json from '@rollup/plugin-json'; import license from 'rollup-plugin-license'; import {dts} from 'rollup-plugin-dts'; @@ -17,9 +18,10 @@ const stripComments = createTag( replaceResultTransformer(emptyLineRegex, ''), ); +const sourceDirectory = 'source'; const outputDirectory = 'build'; -// TODO: warn if dependency not in _actualDependencies, error on publish build +// TODO: warn if dependency not in _actualDependencies, error on publish build? /* TODO: check these: @@ -33,7 +35,9 @@ systemNullSetters: false */ const config = defineConfig({ - input: await globby('source/**/*.ts', {ignore: ['source/**/*.d.ts']}), + input: await globby(`${sourceDirectory}/**/*.ts`, { + ignore: [`${sourceDirectory}/*.d.ts`, `${sourceDirectory}/types.ts`], + }), output: { dir: outputDirectory, interop: 'esModule', @@ -62,11 +66,10 @@ const config = defineConfig({ commonjs({ include: 'node_modules/**', }), + json(), typescript({ - module: 'Node16', // Plugin overrides extended settings: rollup/plugins#1583 - moduleResolution: 'Node16', + tsconfig: resolvedConfig => ({...resolvedConfig, declaration: false}), }), - json(), license({ thirdParty: { output: `${outputDirectory}/licenses.md`, @@ -75,18 +78,30 @@ const config = defineConfig({ ], }); -// TODO: bundle types const dtsConfig = defineConfig({ - input: './source/index.d.ts', + input: `${sourceDirectory}/index.ts`, output: { - file: `./${outputDirectory}/index.d.ts`, + file: `${outputDirectory}/index.d.ts`, format: 'es', }, plugins: [ dts({ respectExternal: true, }), + { + name: 'copy-tsd', + async generateBundle() { + let tsdFile = await fs.readFile('./test-d/index.ts', 'utf8'); + tsdFile = tsdFile.replace( + `import meow from '../${sourceDirectory}/index.js'`, + `import meow from '../${outputDirectory}/index.js'`, + ); + + await fs.writeFile(`./test-d/${outputDirectory}.ts`, tsdFile); + }, + }, ], }); -export default config; +// eslint-disable-next-line import/no-anonymous-default-export +export default [config, dtsConfig]; diff --git a/source/index.ts b/source/index.ts index 446ed79..dc70b1c 100644 --- a/source/index.ts +++ b/source/index.ts @@ -3,13 +3,12 @@ import parseArguments, {type Options as ParserOptions} from 'yargs-parser'; import camelCaseKeys from 'camelcase-keys'; import {trimNewlines} from 'trim-newlines'; import redent from 'redent'; -import normalizePackageData from 'normalize-package-data'; -import type {Options, ParsedOptions, Result, MeowFn} from './types.js'; import {buildOptions} from './options.js'; import {buildParserOptions} from './parser.js'; import {validate, checkUnknownFlags, checkMissingRequiredFlags} from './validate.js'; +import type {Options, ParsedOptions, Result, AnyFlags} from './types.js'; -const buildResult = ({pkg: packageJson, ...options}: ParsedOptions, parserOptions: ParserOptions): Result => { +const buildResult = ({pkg: packageJson, ...options}: ParsedOptions, parserOptions: ParserOptions): Result => { const argv = parseArguments(options.argv as string[], parserOptions); let help = ''; @@ -23,25 +22,24 @@ const buildResult = ({pkg: packageJson, ...options}: ParsedOptions, parserOption help = `\n${help}`; } - normalizePackageData(packageJson); + if (options.description !== false) { + let {description} = options; - if (options.description === false) { - help += '\n'; - } else { - let description = (options.description || packageJson.description) ?? ''; - - // TODO: make this more readable - description &&= help ? `\n ${description}\n` : `\n${description}`; - help = `${description}${help}\n`; + if (description) { + description = help ? `\n ${description}\n` : `\n${description}`; + help = `${description}${help}`; + } } + help += '\n'; + const showHelp = (code?: number) => { console.log(help); process.exit(typeof code === 'number' ? code : 2); }; const showVersion = () => { - console.log(typeof options.version === 'string' ? options.version : packageJson.version); + console.log(options.version); process.exit(0); }; @@ -49,12 +47,14 @@ const buildResult = ({pkg: packageJson, ...options}: ParsedOptions, parserOption if (argv['version'] === true && options.autoVersion) { showVersion(); } else if (argv['help'] === true && options.autoHelp) { - showHelp(0); + showHelp(0); // TODO: why is this 0 and showHelp is 2? } } const input = argv._ as string[]; - delete argv._; // TODO: TS errors bc argv._ does exist, but it thinks we might want to use the type later + // TODO: + // @ts-expect-error: TS errors bc argv._ does exist, but it thinks we might want to use the type later + delete argv._; if (!options.allowUnknownFlags) { checkUnknownFlags(input); @@ -87,18 +87,60 @@ const buildResult = ({pkg: packageJson, ...options}: ParsedOptions, parserOption help, showHelp, showVersion, - }; + } as unknown as Result; // TODO: flags complain bc Result camelcases }; -const meow: MeowFn = (helpMessage, options = {}) => { +/** +@param helpMessage - Shortcut for the `help` option. + +@example +``` +#!/usr/bin/env node +import meow from 'meow'; +import foo from './index.js'; + +const cli = meow(` + Usage + $ foo + + Options + --rainbow, -r Include a rainbow + + Examples + $ foo unicorns --rainbow + 🌈 unicorns 🌈 +`, { + importMeta: import.meta, + flags: { + rainbow: { + type: 'boolean', + shortFlag: 'r' + } + } +}); + +//{ +// input: ['unicorns'], +// flags: {rainbow: true}, +// ... +//} + +foo(cli.input.at(0), cli.flags); +``` +*/ +export default function meow(helpMessage: string, options?: Options): Result; +export default function meow(options?: Options): Result; +// TODO: should these be optional? importMeta is always needed + +export default function meow(helpMessage?: string | Options, options?: Options): Result { if (typeof helpMessage !== 'string') { options = helpMessage; helpMessage = ''; } - const parsedOptions = buildOptions(helpMessage, options as Options); + const parsedOptions = buildOptions(helpMessage, options!); const parserOptions = buildParserOptions(parsedOptions); - const result = buildResult(parsedOptions, parserOptions); + const result = buildResult(parsedOptions, parserOptions); const pkgTitle = result.pkg.bin ? Object.keys(result.pkg.bin).at(0) : result.pkg.name; @@ -107,6 +149,4 @@ const meow: MeowFn = (helpMessage, options = {}) => { } return result; -}; - -export default meow; +} diff --git a/source/minimist-options.d.ts b/source/minimist-options.d.ts index df46ef4..45bfdfd 100644 --- a/source/minimist-options.d.ts +++ b/source/minimist-options.d.ts @@ -1,5 +1,5 @@ +/* eslint-disable import/no-extraneous-dependencies */ declare module 'minimist-options' { - // eslint-disable-next-line import/no-extraneous-dependencies import type {Opts as MinimistOptions} from 'minimist'; export type OptionType = 'string' | 'boolean' | 'number' | 'array' | 'string-array' | 'boolean-array' | 'number-array'; @@ -42,7 +42,7 @@ declare module 'minimist-options' { | NumberArrayOption ); - type MinimistOption = Pick; + export type MinimistOption = Pick; export type Options = MinimistOption & { [key: string]: ( diff --git a/source/options.ts b/source/options.ts index e79e10c..6789729 100644 --- a/source/options.ts +++ b/source/options.ts @@ -2,7 +2,8 @@ import process from 'node:process'; import {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import {readPackageUpSync} from 'read-package-up'; -import type {Options, ParsedOptions, AnyFlag} from './types.js'; +import normalizePackageData from 'normalize-package-data'; +import type {Options, ParsedOptions, AnyFlag, AnyFlags} from './types.js'; import {decamelizeFlagKey, joinFlagKeys} from './utils.js'; type InvalidOptionFilter = { @@ -15,7 +16,6 @@ type InvalidOptionFilters = { }; const validateOptions = (options: ParsedOptions): void => { - // TODO: refactor to not use filter -> iterate once const invalidOptionFilters: InvalidOptionFilters = { flags: { keyContainsDashes: { @@ -33,20 +33,21 @@ const validateOptions = (options: ParsedOptions): void => { choicesNotMatchFlagType: { filter: ([, flag]) => flag.type !== undefined && Array.isArray(flag.choices) && flag.choices.some(choice => typeof choice !== flag.type), message(flagKeys) { - const flagKeysAndTypes = flagKeys.map(flagKey => `(\`${decamelizeFlagKey(flagKey)}\`, type: '${options.flags[flagKey].type}')`); + const flagKeysAndTypes = flagKeys.map(flagKey => `(\`${decamelizeFlagKey(flagKey)}\`, type: '${options.flags[flagKey]!.type}')`); return `Each value of the option \`choices\` must be of the same type as its flag. Invalid flags: ${flagKeysAndTypes.join(', ')}`; }, }, defaultNotInChoices: { - filter: ([, flag]) => flag.default !== undefined && Array.isArray(flag.choices) && ![flag.default].flat().every(value => flag.choices!.includes(value)), + filter: ([, flag]) => flag.default !== undefined && Array.isArray(flag.choices) && ![flag.default].flat().every(value => flag.choices!.includes(value as never)), // TODO: not sure why this is never message: flagKeys => `Each value of the option \`default\` must exist within the option \`choices\`. Invalid flags: ${joinFlagKeys(flagKeys)}`, }, }, }; const errorMessages = []; + type Entry = ['flags', Record]; - for (const [optionKey, filters] of Object.entries(invalidOptionFilters)) { + for (const [optionKey, filters] of Object.entries(invalidOptionFilters) as Entry[]) { const optionEntries = Object.entries(options[optionKey]); for (const {filter, message} of Object.values(filters)) { @@ -64,7 +65,7 @@ const validateOptions = (options: ParsedOptions): void => { } }; -export const buildOptions = (helpMessage: string, options: Options): ParsedOptions => { +export const buildOptions = (helpMessage: string, options: Options): ParsedOptions => { if (!options.importMeta?.url) { throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.'); } @@ -74,15 +75,18 @@ export const buildOptions = (helpMessage: string, options: Options): ParsedOptio normalize: false, }); + const pkg = foundPackage?.packageJson ?? {}; + normalizePackageData(pkg); + const parsedOptions: ParsedOptions = { - pkg: foundPackage ? foundPackage.packageJson : {}, + pkg, argv: process.argv.slice(2), flags: {}, inferType: false, input: 'string', - description: '', // TODO: maybe set from package.json description here? + description: pkg.description ?? false, help: helpMessage, - version: false, + version: pkg.version ?? '', // TODO: maybe say "No version found"? autoHelp: true, autoVersion: true, booleanDefault: false, diff --git a/source/parser.ts b/source/parser.ts index 21c6d82..858235b 100644 --- a/source/parser.ts +++ b/source/parser.ts @@ -3,12 +3,14 @@ import constructParserOptions, { type MinimistOptions, type AnyOption, } from 'minimist-options'; -import type {Options as YargsOptions} from 'yargs-parser'; +import type YargsParser from 'yargs-parser'; import decamelizeKeys from 'decamelize-keys'; import mapObject from 'map-obj'; import type {Writable} from 'type-fest'; import type {ParsedOptions} from './types.js'; +type YargsOptions = YargsParser.Options; + type ParserFlag = AnyOption; type ParserOptions = YargsOptions & MinimistOptions; @@ -23,17 +25,18 @@ const buildParserFlags = ({flags, booleanDefault}: ParsedOptions): ParserFlags = && flag.type === 'boolean' && !Object.hasOwn(flag, 'default'); - // TODO: remove strings from ParserFlag type const parserFlag: Writable = { - type: flag.isMultiple - ? (flag.type ? `${flag.type}-array` as const : 'array' as const) - : flag.type, default: shouldUseBooleanDefault ? (flag.isMultiple ? [booleanDefault] : booleanDefault) : (flag.default ?? (flag.isMultiple ? [] : undefined)), alias: aliases, }; + // TODO: for some reason this errors if set in the object literal + parserFlag.type = flag.isMultiple + ? (flag.type ? `${flag.type}-array` as const : 'array' as const) + : flag.type; + if (parserFlag.default === undefined) { delete parserFlag.default; } diff --git a/source/types.ts b/source/types.ts index 199da9b..245dc74 100644 --- a/source/types.ts +++ b/source/types.ts @@ -1,7 +1,4 @@ -import type { - CamelCasedProperties, - PackageJson, -} from 'type-fest'; +import type {CamelCasedProperties, PackageJson} from 'type-fest'; export type FlagType = 'string' | 'boolean' | 'number'; @@ -112,9 +109,9 @@ type StringFlag = Flag<'string', string> | Flag<'string', string[], true>; type BooleanFlag = Flag<'boolean', boolean> | Flag<'boolean', boolean[], true>; type NumberFlag = Flag<'number', number> | Flag<'number', number[], true>; export type AnyFlag = StringFlag | BooleanFlag | NumberFlag; -type AnyFlags = Record; +export type AnyFlags = Record; -export type Options = { +export type Options = { /** Pass in [`import.meta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta). This is used to find the correct package.json file. */ @@ -161,7 +158,7 @@ export type Options = { } ``` */ - readonly flags?: Flags; + readonly flags?: Flags; // & Pick /** Description to show above the help text. Default: The package.json `"description"` property. @@ -313,8 +310,8 @@ export type Options = { readonly allowUnknownFlags?: boolean; }; -type OptionsWithoutBooleanDefault = Omit; -type BooleanDefault = Pick; +type OptionsWithoutBooleanDefault = Omit, 'booleanDefault'>; +type BooleanDefault = Pick, 'booleanDefault'>; export type ParsedOptions = Required & BooleanDefault & { input: string; @@ -343,7 +340,7 @@ export type TypedFlags = { : PossiblyOptionalFlag> }; -export type Result = { +export type Result = { /** Non-flag arguments. */ @@ -381,46 +378,3 @@ export type Result = { */ showVersion: () => void; }; - -/** -@param helpMessage - Shortcut for the `help` option. - -@example -``` -#!/usr/bin/env node -import meow from 'meow'; -import foo from './index.js'; - -const cli = meow(` - Usage - $ foo - - Options - --rainbow, -r Include a rainbow - - Examples - $ foo unicorns --rainbow - 🌈 unicorns 🌈 -`, { - importMeta: import.meta, - flags: { - rainbow: { - type: 'boolean', - shortFlag: 'r' - } - } -}); - -//{ -// input: ['unicorns'], -// flags: {rainbow: true}, -// ... -//} - -foo(cli.input.at(0), cli.flags); -``` -*/ -declare function meow(helpMessage: string, options?: Options): Result; -declare function meow(options?: Options): Result; - -export type MeowFn = typeof meow; diff --git a/source/validate.ts b/source/validate.ts index a70d6e6..1812e95 100644 --- a/source/validate.ts +++ b/source/validate.ts @@ -35,16 +35,18 @@ const validateChoicesByFlag = (flagKey: string, flagValue: AnyFlag, receivedInpu } if (Array.isArray(receivedInput)) { - const unknownValues = receivedInput.filter(index => !choices.includes(index)); + const unknownValues = receivedInput.filter(index => !choices.includes(index as never)); // TODO: why never? if (unknownValues.length > 0) { const valuesText = unknownValues.length > 1 ? 'values' : 'value'; return `Unknown ${valuesText} for flag \`${decamelizeFlagKey(flagKey)}\`: \`${unknownValues.join('`, `')}\`. ${valueMustBeOneOf}`; } - } else if (!choices.includes(receivedInput)) { + } else if (!choices.includes(receivedInput as never)) { // TODO: why never? return `Unknown value for flag \`${decamelizeFlagKey(flagKey)}\`: \`${receivedInput}\`. ${valueMustBeOneOf}`; } + + return undefined; // TODO: don't know why I need this to satisfy the compiler }; const validateChoices = (flags: DefinedFlags, receivedFlags: Flags): void => { diff --git a/test-d/build.test-d.ts b/test-d/build.test-d.ts deleted file mode 100644 index 29ec7de..0000000 --- a/test-d/build.test-d.ts +++ /dev/null @@ -1,132 +0,0 @@ -import {expectAssignable, expectError, expectType} from 'tsd'; -import type {PackageJson} from 'type-fest'; -import meow, {type Result} from '../build/index.js'; - -type AnyFlag = NonNullable[0]>['flags']>[string]; - -const importMeta = import.meta; - -expectType>(meow('Help text')); -expectType>(meow('Help text', {importMeta})); -expectAssignable<{flags: {foo: number}}>( - meow({importMeta: import.meta, flags: {foo: {type: 'number', isRequired: true}}}), -); -expectAssignable<{flags: {foo: string}}>( - meow({importMeta, flags: {foo: {type: 'string', isRequired: true}}}), -); -expectAssignable<{flags: {foo: boolean}}>( - meow({importMeta, flags: {foo: {type: 'boolean', isRequired: true}}}), -); -expectAssignable<{flags: {foo: number | undefined}}>( - meow({importMeta, flags: {foo: {type: 'number'}}}), -); -expectAssignable<{flags: {foo: string | undefined}}>( - meow({importMeta, flags: {foo: {type: 'string'}}}), -); -expectAssignable<{flags: {foo: boolean | undefined}}>( - meow({importMeta, flags: {foo: {type: 'boolean'}}}), -); -expectAssignable<{flags: {foo: number[] | undefined}}>( - meow({importMeta, flags: {foo: {type: 'number', isMultiple: true}}}), -); -expectAssignable<{flags: {foo: string[] | undefined}}>( - meow({importMeta, flags: {foo: {type: 'string', isMultiple: true}}}), -); -expectAssignable<{flags: {foo: boolean[] | undefined}}>( - meow({importMeta, flags: {foo: {type: 'boolean', isMultiple: true}}}), -); -expectType>(meow({importMeta, description: 'foo'})); -expectType>(meow({importMeta, description: false})); -expectType>(meow({importMeta, help: 'foo'})); -expectType>(meow({importMeta, help: false})); -expectType>(meow({importMeta, version: 'foo'})); -expectType>(meow({importMeta, version: false})); -expectType>(meow({importMeta, autoHelp: false})); -expectType>(meow({importMeta, autoVersion: false})); -expectType>(meow({importMeta, pkg: {foo: 'bar'}})); -expectType>(meow({importMeta, argv: ['foo', 'bar']})); -expectType>(meow({importMeta, inferType: true})); -expectType>(meow({importMeta, booleanDefault: true})); -expectType>(meow({importMeta, booleanDefault: null})); -expectType>(meow({importMeta, booleanDefault: undefined})); -expectType>(meow({importMeta})); - -const result = meow('Help text', { - importMeta, - flags: { - foo: {type: 'boolean', shortFlag: 'f'}, - 'foo-bar': {type: 'number', aliases: ['foobar', 'fooBar']}, - bar: {type: 'string', default: ''}, - abc: {type: 'string', isMultiple: true}, - baz: {type: 'string', choices: ['rainbow', 'cat', 'unicorn']}, - }, -}); - -expectType(result.input); -expectType(result.pkg); -expectType(result.help); - -expectType(result.flags.foo); -expectType(result.flags.fooBar); -expectType(result.flags.bar); -expectType(result.flags.abc); -expectType(result.flags.baz); -expectType(result.unnormalizedFlags.foo); -expectType(result.unnormalizedFlags.f); -expectType(result.unnormalizedFlags['foo-bar']); -expectType(result.unnormalizedFlags.foobar); -expectType(result.unnormalizedFlags.fooBar); -expectType(result.unnormalizedFlags.bar); -expectType(result.unnormalizedFlags.abc); -expectType(result.unnormalizedFlags.baz); - -result.showHelp(); -result.showHelp(1); -result.showVersion(); - -const options = { - importMeta, - flags: { - rainbow: { - type: 'boolean', - shortFlag: 'r', - }, - }, -} as const; - -meow('', options); - -expectAssignable({type: 'string', default: 'cat'}); -expectAssignable({type: 'number', default: 42}); -expectAssignable({type: 'boolean', default: true}); - -expectAssignable({type: 'string', default: undefined}); -expectAssignable({type: 'number', default: undefined}); -expectAssignable({type: 'boolean', default: undefined}); - -expectAssignable({type: 'string', isMultiple: true, default: ['cat']}); -expectAssignable({type: 'number', isMultiple: true, default: [42]}); -expectAssignable({type: 'boolean', isMultiple: true, default: [false]}); - -expectError({type: 'string', isMultiple: true, default: 'cat'}); -expectError({type: 'number', isMultiple: true, default: 42}); -expectError({type: 'boolean', isMultiple: true, default: false}); - -expectAssignable({type: 'string', choices: ['cat', 'unicorn']}); -expectAssignable({type: 'number', choices: [1, 2]}); -expectAssignable({type: 'boolean', choices: [true, false]}); -expectAssignable({type: 'string', isMultiple: true, choices: ['cat']}); -expectAssignable({type: 'string', isMultiple: false, choices: ['cat']}); - -expectError({type: 'string', choices: 'cat'}); -expectError({type: 'number', choices: 1}); -expectError({type: 'boolean', choices: true}); - -expectError({type: 'string', choices: [1]}); -expectError({type: 'number', choices: ['cat']}); -expectError({type: 'boolean', choices: ['cat']}); - -expectAssignable({choices: ['cat']}); -expectAssignable({choices: [1]}); -expectAssignable({choices: [true]}); -expectError({choices: ['cat', 1, true]}); diff --git a/test-d/index.ts b/test-d/index.ts index 62aa18f..271d447 100644 --- a/test-d/index.ts +++ b/test-d/index.ts @@ -39,14 +39,12 @@ expectType>(meow({importMeta, description: false})); expectType>(meow({importMeta, help: 'foo'})); expectType>(meow({importMeta, help: false})); expectType>(meow({importMeta, version: 'foo'})); -expectType>(meow({importMeta, version: false})); expectType>(meow({importMeta, autoHelp: false})); expectType>(meow({importMeta, autoVersion: false})); expectType>(meow({importMeta, pkg: {foo: 'bar'}})); expectType>(meow({importMeta, argv: ['foo', 'bar']})); expectType>(meow({importMeta, inferType: true})); expectType>(meow({importMeta, booleanDefault: true})); -expectType>(meow({importMeta, booleanDefault: null})); expectType>(meow({importMeta, booleanDefault: undefined})); expectType>(meow({importMeta, hardRejection: false})); @@ -71,10 +69,10 @@ expectType(result.flags.bar); expectType(result.flags.abc); expectType(result.flags.baz); expectType(result.unnormalizedFlags.foo); -expectType(result.unnormalizedFlags.f); +expectType(result.unnormalizedFlags['f']); expectType(result.unnormalizedFlags['foo-bar']); -expectType(result.unnormalizedFlags.foobar); -expectType(result.unnormalizedFlags.fooBar); +expectType(result.unnormalizedFlags['foobar']); +expectType(result.unnormalizedFlags['fooBar']); expectType(result.unnormalizedFlags.bar); expectType(result.unnormalizedFlags.abc); expectType(result.unnormalizedFlags.baz); diff --git a/test/_utils.ts b/test/_utils.ts index 44a2cd8..a07e294 100644 --- a/test/_utils.ts +++ b/test/_utils.ts @@ -3,11 +3,15 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import test from 'ava'; import {execa, type ExecaChildProcess, type Options as ExecaOptions} from 'execa'; -import dedent from 'dedent'; +import {createTag, stripIndentTransformer, trimResultTransformer} from 'common-tags'; import meow from '../source/index.js'; import type {Options} from '../source/types.js'; -export const stripIndent = dedent; +// Use old behavior prior to zspecza/common-tags#165 +export const stripIndent = createTag( + stripIndentTransformer(), + trimResultTransformer(), +); export const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -36,11 +40,7 @@ type MeowThrowsMacroArgs = [ message: {message: string}, ]; -/** -A convenience-wrapper around `t.throws` with `meow`. -@param options `meow` options with `importMeta` set. -@param message The thrown error message. Strips indentation, so template literals can be used. -*/ +/** A convenience-wrapper around `t.throws` with `meow`. */ export const _meowThrows = (importMeta: ImportMeta) => ( test.macro((t, testOptions, {message}) => { const options = { @@ -51,19 +51,17 @@ export const _meowThrows = (importMeta: ImportMeta) => ( message = stripIndent(message); t.throws(() => meow(options), {message}); - // TODO: if error message is unprovided, snapshot thrown message }) ); type MeowSucceedsMacroArgs = [ /** `meow` options with `importMeta` set. */ options: MeowOptions, - /** The expected output. */ - expected: {expected: ReturnType}, // TODO: implement ]; +/** A convenience-wrapper around `t.notThrows` with `meow`. */ export const _meowSucceeds = (importMeta: ImportMeta) => ( - test.macro((t, testOptions: MeowOptions) => { + test.macro((t, testOptions) => { const options = { importMeta, ...testOptions, diff --git a/test/build.js b/test/build.js index 5607ef7..d8cad87 100644 --- a/test/build.js +++ b/test/build.js @@ -41,6 +41,6 @@ test('main', t => { test('spawn cli and show version', async t => { const pkg = await readPackage(); - const {stdout} = await spawnFixture(['--version']); + const {stdout} = await spawnFixture('build.js', ['--version']); t.is(stdout, pkg.version); }); diff --git a/test/errors.ts b/test/errors.ts index cd21013..7181c2a 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -4,7 +4,7 @@ import {_meowThrows} from './_utils.js'; const meowThrows = _meowThrows(import.meta); test('invalid package url', meowThrows, { - importMeta: '/path/to/package' as ImportMeta, + importMeta: '/path/to/package', }, {message: 'The `importMeta` option is required. Its value must be `import.meta`.'}); test('supports `number` flag type - throws on incorrect default value', meowThrows, { diff --git a/test/fixtures/required/fixture-required-function.ts b/test/fixtures/required/fixture-required-function.ts index c86d7ab..0ef37c9 100755 --- a/test/fixtures/required/fixture-required-function.ts +++ b/test/fixtures/required/fixture-required-function.ts @@ -1,7 +1,7 @@ #!/usr/bin/env tsimp import meow from '../../../source/index.js'; -// TODO: fix types and check back here +// TODO: see if `isRequired` can have access to keys const cli = meow({ importMeta: import.meta, description: 'Custom description', @@ -16,7 +16,7 @@ const cli = meow({ }, withTrigger: { type: 'string', - isRequired: (flags, _) => flags.trigger, + isRequired: flags => Boolean(flags['trigger']), }, allowError: { type: 'boolean', @@ -24,9 +24,7 @@ const cli = meow({ }, shouldError: { type: 'boolean', - isRequired: (flags, _) => - flags.allowError ? 'should error' : false - , + isRequired: flags => flags['allowError'] ? 'should error' as unknown as boolean : false, }, }, }); diff --git a/test/help.ts b/test/help.ts index 891cd6a..d684a96 100644 --- a/test/help.ts +++ b/test/help.ts @@ -1,7 +1,7 @@ import test from 'ava'; import indentString from 'indent-string'; import meow from '../source/index.js'; -import {spawnFixture} from './_utils.js'; +import {spawnFixture, stripIndent} from './_utils.js'; const importMeta = import.meta; @@ -21,12 +21,23 @@ test('spawn cli and show help screen', async t => { test('spawn cli and disabled autoHelp', async t => { const {stdout} = await spawnFixture(['--help', '--no-auto-help']); - t.is(stdout, 'help\nautoHelp\nmeow\ncamelCaseOption'); + + t.is(stdout, stripIndent` + help + autoHelp + meow + camelCaseOption + `); }); test('spawn cli and not show help', async t => { const {stdout} = await spawnFixture(['--help=all']); - t.is(stdout, 'help\nmeow\ncamelCaseOption'); + + t.is(stdout, stripIndent` + help + meow + camelCaseOption + `); }); test('single line help messages are not indented', t => { @@ -48,3 +59,25 @@ test('descriptions with no help are not indented', t => { t.is(help, '\nsingle line\n'); }); + +test('exits with code 0 by default', async t => { + const {exitCode} = await spawnFixture(['--help']); + t.is(exitCode, 0); +}); + +test.todo('showHelp exits with code 2 by default'); + +test.todo('showHelp exits with given code'); + +// TODO: description settings + +// reorganize tests: +// \_ options +// \_ description +// \_ help +// \_ version +// \_ ... +// \_ flags +// \_ aliases +// \_ choices +// \_ ... diff --git a/test/test.ts b/test/test.ts index 095ab19..37d8e13 100644 --- a/test/test.ts +++ b/test/test.ts @@ -20,7 +20,9 @@ test('return object', t => { flags: { unicorn: {shortFlag: 'u'}, meow: {default: 'dog'}, - '--': true, // TODO: add '--' to type? + '--': true as unknown as {type: 'string'; isMultiple: true}, + // TODO: can not model due to microsoft/TypeScript#17867 + // A potential workaround is to use a symbol for '--' -> {flags: [argvDashDash]: true} }, }); @@ -84,11 +86,11 @@ test('single character flag casing should be preserved', t => { }); t.like(cli.flags, { - F: true, + F: true, // eslint-disable-line @typescript-eslint/naming-convention }); }); -// TODO +// TODO: add inference to types themselves? test('type inference', t => { t.is(meow({importMeta, argv: ['5']}).input.at(0), '5'); t.is(meow({importMeta, argv: ['5']}, {input: 'string'}).input.at(0), '5'); @@ -145,7 +147,7 @@ test('booleanDefault: undefined, filter out unset boolean args', t => { test('booleanDefault: null throws', meowThrows, { argv: ['--foo'], - booleanDefault: null, + booleanDefault: null as unknown as undefined, flags: { foo: { type: 'boolean', diff --git a/test/version.ts b/test/version.ts index 26e1767..cc081ce 100644 --- a/test/version.ts +++ b/test/version.ts @@ -38,7 +38,7 @@ test('custom version', async t => { test('version = false has no effect', async t => { // eslint-disable-next-line @typescript-eslint/naming-convention const {stdout} = await spawnFixture(fixture, ['--version'], {env: {VERSION: 'false'}}); - t.is(stdout, ''); + t.is(stdout, 'false'); }); test('manual showVersion', async t => { diff --git a/tsconfig.json b/tsconfig.json index c9a46ac..4f830c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,9 @@ { "extends": "@sindresorhus/tsconfig", "exclude": ["build", "node_modules"], - "include": ["source", "test"], + "include": ["source", "test", "test-d"], "compilerOptions": { - "outDir": "build", - "noEmitOnError": false, - "declaration": false, + "moduleResolution": "Bundler", + "module": "ES2022", }, }