From 8860b441e7833b63fb91035be99df95e604284b8 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sun, 22 Dec 2024 00:13:01 +0300 Subject: [PATCH] feat(cli): provide `--prefer-local` option (#1015) * feat(cli): provide `--prefer-local` option * test: extend argv test * refactor(util): simplify `snakeToCamel` --- .size-limit.json | 2 +- man/zx.1 | 4 +++- src/cli.ts | 21 +++++++++---------- src/core.ts | 3 ++- src/goods.ts | 33 ++++++++++++++++++++++++++---- src/util.ts | 17 +++++++++------- test/goods.test.js | 50 ++++++++++++++++++++++++++++++++++++++-------- test/index.test.js | 6 ++++-- test/util.test.js | 1 + 9 files changed, 101 insertions(+), 36 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 1be91f16d0..14c81174b3 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -9,7 +9,7 @@ { "name": "zx/index", "path": "build/*.{js,cjs}", - "limit": "803 kB", + "limit": "804 kB", "brotli": false, "gzip": false }, diff --git a/man/zx.1 b/man/zx.1 index 5012159fa2..b6f23a9c47 100644 --- a/man/zx.1 +++ b/man/zx.1 @@ -19,13 +19,15 @@ set the shell to use prefix all commands .SS --postfix= postfix all commands +.SS --prefer-local, -l +prefer locally installed packages bins .SS --eval=, -e evaluate script .SS --ext=<.mjs> default extension .SS --install, -i install dependencies -.SS --registry +.SS --registry= npm registry, defaults to https://registry.npmjs.org/ .SS --repl start repl diff --git a/src/cli.ts b/src/cli.ts index 6e90cfcdf2..8800a2d156 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,10 +21,10 @@ import { updateArgv, fetch, chalk, - minimist, fs, path, VERSION, + parseArgv, } from './index.js' import { installDeps, parseDeps } from './deps.js' import { randomId } from './util.js' @@ -57,6 +57,7 @@ export function printUsage() { --shell= custom shell binary --prefix= prefix all commands --postfix= postfix all commands + --prefer-local, -l prefer locally installed packages bins --cwd= set current directory --eval=, -e evaluate script --ext=<.mjs> default extension @@ -71,19 +72,14 @@ export function printUsage() { `) } -export const argv: minimist.ParsedArgs = minimist(process.argv.slice(2), { +// prettier-ignore +export const argv = parseArgv(process.argv.slice(2), { string: ['shell', 'prefix', 'postfix', 'eval', 'cwd', 'ext', 'registry'], - boolean: [ - 'version', - 'help', - 'quiet', - 'verbose', - 'install', - 'repl', - 'experimental', - ], - alias: { e: 'eval', i: 'install', v: 'version', h: 'help' }, + boolean: ['version', 'help', 'quiet', 'verbose', 'install', 'repl', 'experimental', 'prefer-local'], + alias: { e: 'eval', i: 'install', v: 'version', h: 'help', l: 'prefer-local' }, stopEarly: true, + parseBoolean: true, + camelCase: true, }) export async function main() { @@ -95,6 +91,7 @@ export async function main() { if (argv.shell) $.shell = argv.shell if (argv.prefix) $.prefix = argv.prefix if (argv.postfix) $.postfix = argv.postfix + if (argv.preferLocal) $.preferLocal = argv.preferLocal if (argv.version) { console.log(VERSION) return diff --git a/src/core.ts b/src/core.ts index c2cd84c803..e0c3d13299 100644 --- a/src/core.ts +++ b/src/core.ts @@ -50,6 +50,7 @@ import { isStringLiteral, noop, once, + parseBool, parseDuration, preferLocalBin, proxyOverride, @@ -929,7 +930,7 @@ export function resolveDefaults( return Object.entries(env).reduce((m, [k, v]) => { if (v && k.startsWith(prefix)) { const _k = snakeToCamel(k.slice(prefix.length)) - const _v = { true: true, false: false }[v.toLowerCase()] ?? v + const _v = parseBool(v) if (allowed.has(_k)) (m as any)[_k] = _v } return m diff --git a/src/goods.ts b/src/goods.ts index 090cef8d63..1a5888fe70 100644 --- a/src/goods.ts +++ b/src/goods.ts @@ -15,7 +15,14 @@ import assert from 'node:assert' import { createInterface } from 'node:readline' import { $, within, ProcessOutput } from './core.js' -import { type Duration, isStringLiteral, parseDuration } from './util.js' +import { + type Duration, + identity, + isStringLiteral, + parseBool, + parseDuration, + snakeToCamel, +} from './util.js' import { chalk, minimist, @@ -27,12 +34,30 @@ import { export { default as path } from 'node:path' export * as os from 'node:os' -export const argv: minimist.ParsedArgs = minimist(process.argv.slice(2)) -export function updateArgv(args: string[]) { +type ArgvOpts = minimist.Opts & { camelCase?: boolean; parseBoolean?: boolean } + +export const parseArgv = ( + args: string[] = process.argv.slice(2), + opts: ArgvOpts = {} +): minimist.ParsedArgs => + Object.entries(minimist(args, opts)).reduce( + (m, [k, v]) => { + const kTrans = opts.camelCase ? snakeToCamel : identity + const vTrans = opts.parseBoolean ? parseBool : identity + const [_k, _v] = k === '--' || k === '_' ? [k, v] : [kTrans(k), vTrans(v)] + m[_k] = _v + return m + }, + {} as minimist.ParsedArgs + ) + +export function updateArgv(args?: string[], opts?: ArgvOpts) { for (const k in argv) delete argv[k] - Object.assign(argv, minimist(args)) + Object.assign(argv, parseArgv(args, opts)) } +export const argv: minimist.ParsedArgs = parseArgv() + export function sleep(duration: Duration): Promise { return new Promise((resolve) => { setTimeout(resolve, parseDuration(duration)) diff --git a/src/util.ts b/src/util.ts index aed7cb36c2..7296e0a24e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -39,6 +39,10 @@ export function tempfile(name?: string, data?: string | Buffer): string { export function noop() {} +export function identity(v: T): T { + return v +} + export function randomId() { return Math.random().toString(36).slice(2) } @@ -282,17 +286,16 @@ export const proxyOverride = ( }, }) as T -// https://stackoverflow.com/a/7888303 export const camelToSnake = (str: string) => str .split(/(?=[A-Z])/) .map((s) => s.toUpperCase()) .join('_') -// https://stackoverflow.com/a/61375162 export const snakeToCamel = (str: string) => - str - .toLowerCase() - .replace(/([-_][a-z])/g, (group) => - group.toUpperCase().replace('-', '').replace('_', '') - ) + str.toLowerCase().replace(/([a-z])[_-]+([a-z])/g, (_, p1, p2) => { + return p1 + p2.toUpperCase() + }) + +export const parseBool = (v: string): boolean | string => + ({ true: true, false: false })[v] ?? v diff --git a/test/goods.test.js b/test/goods.test.js index 43ece50c48..dd2e60c899 100644 --- a/test/goods.test.js +++ b/test/goods.test.js @@ -32,15 +32,8 @@ describe('goods', () => { assert.match((await p).stdout, /Answer is foo/) }) - test('globby available', async () => { + test('globby() works', async () => { assert.equal(globby, glob) - assert.equal(typeof globby, 'function') - assert.equal(typeof globby.globbySync, 'function') - assert.equal(typeof globby.globbyStream, 'function') - assert.equal(typeof globby.generateGlobTasks, 'function') - assert.equal(typeof globby.isDynamicPattern, 'function') - assert.equal(typeof globby.isGitIgnored, 'function') - assert.equal(typeof globby.isGitIgnoredSync, 'function') assert.deepEqual(await globby('*.md'), ['README.md']) }) @@ -178,4 +171,45 @@ describe('goods', () => { assert(out.exitCode !== 0) }) }) + + test('parseArgv() works', () => { + assert.deepEqual( + parseArgv( + // prettier-ignore + [ + '--foo-bar', 'baz', + '-a', '5', + '-a', '42', + '--aaa', 'AAA', + '--force', + './some.file', + '--b1', 'true', + '--b2', 'false', + '--b3', + '--b4', 'false', + '--b5', 'true', + '--b6', 'str' + ], + { + boolean: ['force', 'b3', 'b4', 'b5', 'b6'], + camelCase: true, + parseBoolean: true, + alias: { a: 'aaa' }, + } + ), + { + a: [5, 42, 'AAA'], + aaa: [5, 42, 'AAA'], + fooBar: 'baz', + force: true, + _: ['./some.file', 'str'], + b1: true, + b2: false, + b3: true, + b4: false, + b5: true, + b6: true, + } + ) + }) }) diff --git a/test/index.test.js b/test/index.test.js index 2c7a094dcc..0d8a2b3255 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -39,8 +39,9 @@ import { quote, quotePowerShell, within, - argv, os, + argv, + parseArgv, updateArgv, globby, glob, @@ -82,8 +83,9 @@ describe('index', () => { assert(useBash) // goods - assert(argv) assert(os) + assert(argv) + assert(parseArgv) assert(updateArgv) assert(globby) assert(glob) diff --git a/test/util.test.js b/test/util.test.js index 234ed6c5c6..e7d10e3e81 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -145,5 +145,6 @@ describe('util', () => { assert.equal(snakeToCamel('NOTHROW'), 'nothrow') assert.equal(snakeToCamel('PREFER_LOCAL'), 'preferLocal') assert.equal(snakeToCamel('SOME_MORE_BIG_STR'), 'someMoreBigStr') + assert.equal(snakeToCamel('kebab-input-str'), 'kebabInputStr') }) })