Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite in TypeScript #252

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -2,3 +2,4 @@ node_modules
build
yarn.lock
test-d/build.ts
.tsimp
25 changes: 23 additions & 2 deletions package.json
Expand Up @@ -63,8 +63,14 @@
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@sindresorhus/is": "^6.2.0",
"@sindresorhus/tsconfig": "^5.0.0",
"@types/common-tags": "^1.8.4",
"@types/minimist": "^1.2.5",
"ava": "^6.1.1",
"@types/node": "18",
"@types/stack-utils": "^2.0.3",
"@types/yargs-parser": "^21.0.3",
"ava": "^6.1.2",
"camelcase-keys": "^9.1.3",
"common-tags": "^2.0.0-alpha.1",
"decamelize": "^6.0.0",
Expand All @@ -81,9 +87,11 @@
"rollup": "^4.12.0",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-license": "^3.2.0",
"rollup-plugin-ts": "^3.4.5",
"stack-utils": "^2.0.6",
"trim-newlines": "^5.0.0",
"tsd": "^0.30.7",
"tsimp": "^2.0.11",
"type-fest": "^4.10.3",
"typescript": "~5.3.3",
"xo": "^0.57.0",
Expand All @@ -92,10 +100,23 @@
"xo": {
"rules": {
"unicorn/no-process-exit": "off",
"unicorn/error-message": "off"
"unicorn/error-message": "off",
"@typescript-eslint/no-dynamic-delete": "off"
},
"ignores": [
"build"
]
},
"ava": {
"extensions": {
"ts": "module",
"js": true
},
"nodeArguments": [
"--import=tsimp"
],
"environmentVariables": {
"TSIMP_DIAG": "ignore"
}
}
}
17 changes: 12 additions & 5 deletions rollup.config.js
@@ -1,13 +1,14 @@
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-ts';
import json from '@rollup/plugin-json';
import license from 'rollup-plugin-license';
import {dts} from 'rollup-plugin-dts';
import {globby} from 'globby';
import {createTag, replaceResultTransformer} from 'common-tags';
import {delete_comments as deleteComments} from 'delete_comments';
import {defineConfig} from 'rollup';

/** Matches empty lines: https://stackoverflow.com/q/16369642/10292952 */
const emptyLineRegex = /^\s*[\r\n]/gm;
Expand All @@ -21,7 +22,9 @@ const sourceDirectory = 'source';
const outputDirectory = 'build';

const config = defineConfig({
input: await globby(`${sourceDirectory}/**/*.js`),
input: await globby(`./${sourceDirectory}/**/*.ts`, {
ignore: [`./${sourceDirectory}/*.d.ts`, `./${sourceDirectory}/types.ts`],
}),
output: {
dir: outputDirectory,
interop: 'esModule',
Expand Down Expand Up @@ -51,6 +54,9 @@ const config = defineConfig({
include: 'node_modules/**',
}),
json(),
typescript({
tsconfig: resolvedConfig => ({...resolvedConfig, declaration: false}),
}),
license({
thirdParty: {
output: `${outputDirectory}/licenses.md`,
Expand All @@ -60,7 +66,7 @@ const config = defineConfig({
});

const dtsConfig = defineConfig({
input: `./${sourceDirectory}/index.d.ts`,
input: `./${sourceDirectory}/index.ts`,
output: {
file: `./${outputDirectory}/index.d.ts`,
format: 'es',
Expand All @@ -73,9 +79,10 @@ const dtsConfig = defineConfig({
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'`,
`../${sourceDirectory}/index.js`,
`../${outputDirectory}/index.js`,
);

await fs.writeFile(`./test-d/${outputDirectory}.ts`, tsdFile);
Expand Down
98 changes: 0 additions & 98 deletions source/index.js

This file was deleted.

154 changes: 154 additions & 0 deletions source/index.ts
@@ -0,0 +1,154 @@
import process from 'node:process';
import parseArguments, {type Options as ParserOptions} from 'yargs-parser';
import camelCaseKeys from 'camelcase-keys';
import {trimNewlines} from 'trim-newlines';
import redent from 'redent';
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 = <Flags extends AnyFlags = AnyFlags>({pkg: packageJson, ...options}: ParsedOptions, parserOptions: ParserOptions): Result<Flags> => {
const {_: input, ...argv} = parseArguments(options.argv as string[], parserOptions);
let help = '';

if (options.help) {
help = trimNewlines((options.help || '').replace(/\t+\n*$/, ''));

if (help.includes('\n')) {
help = redent(help, options.helpIndent);
}

help = `\n${help}`;
}

if (options.description !== false) {
let {description} = options;

if (description) {
description = help ? redent(`\n${description}\n`, options.helpIndent) : `\n${description}`;
help = `${description}${help}`;
}
}

help += '\n';

const showHelp = (code?: number) => {
console.log(help);
process.exit(typeof code === 'number' ? code : 2); // Default to code 2 for incorrect usage (#47)
};

const showVersion = () => {
console.log(options.version);
process.exit(0);
};

if (input.length === 0 && options.argv.length === 1) {
if (argv['version'] === true && options.autoVersion) {
showVersion();
} else if (argv['help'] === true && options.autoHelp) {
showHelp(0);
}
}

if (!options.allowUnknownFlags) {
checkUnknownFlags(input);
}

const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]});
const unnormalizedFlags = {...flags};

validate(flags, options);

for (const flagValue of Object.values(options.flags)) {
if (Array.isArray(flagValue.aliases)) {
for (const alias of flagValue.aliases) {
delete flags[alias];
}
}

if (flagValue.shortFlag) {
delete flags[flagValue.shortFlag];
}
}

checkMissingRequiredFlags(options.flags, flags, input as string[]);

const result = {
input,
flags,
unnormalizedFlags,
pkg: packageJson,
help,
showHelp,
showVersion,
};

return result as unknown as Result<Flags>;
};

/**
@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, // This is required
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>;

export default function meow<Flags extends AnyFlags = AnyFlags>(helpMessage: string | Options<Flags>, options?: Options<Flags>): Result<Flags> {
if (typeof helpMessage !== 'string') {
options = helpMessage;
helpMessage = '';
}

const parsedOptions = buildOptions(helpMessage, options!);
const parserOptions = buildParserOptions(parsedOptions);
const result = buildResult<Flags>(parsedOptions, parserOptions);

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

// TODO: move to separate PR?
if (packageTitle) {
process.title = packageTitle;
}

return result;
}