Skip to content

Commit

Permalink
fix types
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy-mitchell committed Jan 28, 2024
1 parent 8ebbb16 commit 940938b
Show file tree
Hide file tree
Showing 19 changed files with 194 additions and 278 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
build
yarn.lock
.tsimp
test-d/build.ts
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -108,14 +108,15 @@
]
},
"ava": {
"files": [
"test/*.ts"
],
"extensions": {
"ts": "module"
"ts": "module",
"js": true
},
"nodeArguments": [
"--import=tsimp"
]
],
"environmentVariables": {
"TSIMP_DIAG": "ignore"
}
}
}
35 changes: 25 additions & 10 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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:
Expand All @@ -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',
Expand Down Expand Up @@ -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`,
Expand All @@ -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];
84 changes: 62 additions & 22 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Flags extends AnyFlags = AnyFlags>({pkg: packageJson, ...options}: ParsedOptions, parserOptions: ParserOptions): Result<Flags> => {
const argv = parseArguments(options.argv as string[], parserOptions);
let help = '';

Expand All @@ -23,38 +22,39 @@ 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);
};

if (argv._.length === 0 && options.argv.length === 1) {
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);
Expand Down Expand Up @@ -87,18 +87,60 @@ const buildResult = ({pkg: packageJson, ...options}: ParsedOptions, parserOption
help,
showHelp,
showVersion,
};
} as unknown as Result<Flags>; // 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 <input>
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<Flags extends AnyFlags>(helpMessage: string, options?: Options<Flags>): Result<Flags>;
export default function meow<Flags extends AnyFlags>(options?: Options<Flags>): Result<Flags>;
// TODO: should these be optional? importMeta is always needed

export default function meow<Flags extends AnyFlags>(helpMessage?: string | Options<Flags>, options?: Options<Flags>): Result<Flags> {
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<Flags>(parsedOptions, parserOptions);

const pkgTitle = result.pkg.bin ? Object.keys(result.pkg.bin).at(0) : result.pkg.name;

Expand All @@ -107,6 +149,4 @@ const meow: MeowFn = (helpMessage, options = {}) => {
}

return result;
};

export default meow;
}
4 changes: 2 additions & 2 deletions source/minimist-options.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -42,7 +42,7 @@ declare module 'minimist-options' {
| NumberArrayOption
);

type MinimistOption = Pick<MinimistOptions, 'stopEarly' | 'unknown' | '--'>;
export type MinimistOption = Pick<MinimistOptions, 'stopEarly' | 'unknown' | '--'>;

export type Options = MinimistOption & {
[key: string]: (
Expand Down
22 changes: 13 additions & 9 deletions source/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -15,7 +16,6 @@ type InvalidOptionFilters = {
};

const validateOptions = (options: ParsedOptions): void => {
// TODO: refactor to not use filter -> iterate once
const invalidOptionFilters: InvalidOptionFilters = {
flags: {
keyContainsDashes: {
Expand All @@ -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<string, InvalidOptionFilter>];

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)) {
Expand All @@ -64,7 +65,7 @@ const validateOptions = (options: ParsedOptions): void => {
}
};

export const buildOptions = (helpMessage: string, options: Options): ParsedOptions => {
export const buildOptions = (helpMessage: string, options: Options<AnyFlags>): ParsedOptions => {
if (!options.importMeta?.url) {
throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.');
}
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 940938b

Please sign in to comment.