This is an automated email from the ASF dual-hosted git repository.
kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-js.git
The following commit(s) were added to refs/heads/main by this push:
new 6485275 chore: drop command-line-args/usage and @swc/helpers from
runtime deps (#441)
6485275 is described below
commit 6485275012fe948ede70ecf9c7a86c9668c75fa9
Author: Maor Leger <[email protected]>
AuthorDate: Tue Jun 30 22:04:02 2026 -0700
chore: drop command-line-args/usage and @swc/helpers from runtime deps
(#441)
This PR trims the list of runtime dependencies from 9 down to 4
(`@types/node`, `flatbuffers`, `json-with-bigint`, `tslib` stay).
Two pieces here:
## @swc/helpers
Moving to devDependencies. The published library and CLI are both
compiled by TSC (`gulp-typescript` + `tsconfig.bin.cjs.json`), and
`importHelpers: true` in our tsconfig means the emitted code imports
from `tslib`, never `@swc/helpers` - `grep -r '@swc/helpers' targets/`
on a freshly built tree returns zero matches in any
`.js`/`.mjs`/`.d.ts`.
The only path that touches SWC at all is the dev-time bin shebangs that
go through `@swc-node/register/esm-register`, and that path keeps
working with `@swc/helpers` in devDeps (verified by removing it from
`dependencies`, blowing away node_modules + the lockfile, reinstalling,
and running both the test suite and `bin/file-to-stream.ts` end-to-end).
I considered deleting it entirely since `@swc-node/register` indirectly
pulls it in, but keeping it as an explicit devDep felt safer in case
upstream changes that.
## command-line-args and command-line-usage
The only piece of the published package that actually needs an argv
parser is the `arrow2csv` CLI. `node:util.parseArgs` has been stable
since Node 18.3, which is well below the min version specified here,
so we don't strictly need the two libraries (plus their
transitive trees of `chalk`, `lodash`, `table-layout`, etc.) to be
installed alongside `apache-arrow` for every consumer.
This PR adds a small internal helper at `src/bin/cli.ts` that wraps
`parseArgs` and provides a plain-text `formatUsage`.
### Callout
The one tricky bit was that `command-line-args` treats `multiple: true`
greedily -
`-s a b c` puts all three into `schema` - while `parseArgs` only
honors the repeated-flag form. I wanted to keep `arrow2csv` behavior
identical, so the helper opts in to `tokens: true` and reattributes
positionals that follow a `multiple: true` flag back to that flag's
array. The unit tests in `test/unit/bin/cli-tests.ts` lock in the
greedy and non-greedy cases side-by-side. The dev-only
`bin/integration.ts`,
`bin/json-to-arrow.ts`, and `gulp/argv.js` get migrated over in the
same pass.
We could significantly simplify the cli.ts file if we can forgo that
requirement,
but I opted to keep full compat for now.
A few small things that `command-line-args` and `command-line-usage`
support that this helper doesn't (none currently used by anything in
`arrow2csv`):
- Custom `type: (val) => ...` coercion functions
- `lazyMultiple`, `defaultOption`, `group`, `camelCase`,
`stopAtFirstUnknown`
- ANSI styling (`{bold ...}` / `{underline ...}` markers are stripped
to plain text rather than colored)
- Text wrapping of descriptions and `typeLabel` rendering in the help
screen - the `typeLabel` field is accepted on the spec but not
rendered. None of our help text is wide enough to need wrapping.
Happy to add any of these back if reviewers prefer; my read is that
the unused-feature surface isn't worth re-implementing.
Validation:
- `npm run lint:ci` clean
- `npm test -- -t src` passes (46 suites, ~11.7k tests, plus the new
cli-tests.ts)
- `npm run build` succeeds for all 10 target/format combos
- `npm run test:bundle` (esbuild + rollup + webpack) all run clean
- End-to-end smoke tests of `node targets/apache-arrow/bin/arrow2csv.js`
pass for `--help`, `-f <file>`, greedy `-s id name -f <file>`,
and `--sep ' , '`
Resolves #55
---
bin/arrow2csv.cjs | 2 +-
bin/integration.ts | 13 ++--
bin/json-to-arrow.ts | 11 +--
gulp/argv.js | 54 +++++++++++---
package-lock.json | 141 +++----------------------------------
package.json | 6 +-
src/bin/arrow2csv.ts | 11 +--
src/bin/cli.ts | 124 ++++++++++++++++++++++++++++++++
test/unit/bin/cli-tests.ts | 171 +++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 367 insertions(+), 166 deletions(-)
diff --git a/bin/arrow2csv.cjs b/bin/arrow2csv.cjs
index e1ead17..8bf61e2 100755
--- a/bin/arrow2csv.cjs
+++ b/bin/arrow2csv.cjs
@@ -24,6 +24,6 @@ const env = { ...process.env, TS_NODE_TRANSPILE_ONLY: `true`
};
require('child_process').spawn(`node`, [
`--import`,
- `@swc-node/register/esm-register`
+ `@swc-node/register/esm-register`,
arrow2csv, ...process.argv.slice(2)
], { cwd: here, env, stdio: `inherit` });
diff --git a/bin/integration.ts b/bin/integration.ts
index d1ba87c..de7101a 100755
--- a/bin/integration.ts
+++ b/bin/integration.ts
@@ -21,7 +21,7 @@ import * as fs from 'node:fs';
import * as Path from 'node:path';
import { glob } from 'glob';
import { zip } from 'ix/iterable/zip';
-import commandLineArgs from 'command-line-args';
+import { parseCliArgs, formatUsage } from '../src/bin/cli.ts';
import { parseArrowJSON } from '../src/util/json.ts';
import {
@@ -35,7 +35,7 @@ import {
} from '../index.ts';
const { createElementComparator } = util;
-const argv = commandLineArgs(cliOpts(), { partial: true });
+const { values: argv, positionals } = parseCliArgs(cliOpts(),
process.argv.slice(2));
const exists = async (p: string) => {
try {
@@ -47,9 +47,9 @@ const exists = async (p: string) => {
if (!argv.mode) { return print_usage(); }
- const mode = argv.mode.toUpperCase();
- let jsonPaths = [...(argv.json || [])];
- let arrowPaths = [...(argv.arrow || [])];
+ const mode = (argv.mode as string).toUpperCase();
+ let jsonPaths = [...(argv.json as string[] | undefined ?? [])];
+ let arrowPaths = [...(argv.arrow as string[] | undefined ?? [])];
if (mode === 'VALIDATE' && jsonPaths.length === 0) {
[jsonPaths, arrowPaths] = await
loadLocalJSONAndArrowPathsForDebugging(jsonPaths, arrowPaths);
@@ -104,7 +104,7 @@ function cliOpts() {
}
function print_usage() {
- console.log(require('command-line-usage')([
+ console.log(formatUsage([
{
header: 'integration',
content: 'Script for running Arrow integration tests'
@@ -121,6 +121,7 @@ function print_usage() {
...cliOpts(),
{
name: 'help',
+ type: Boolean,
description: 'Print this usage guide.'
}
]
diff --git a/bin/json-to-arrow.ts b/bin/json-to-arrow.ts
index 2600964..edfd4cf 100755
--- a/bin/json-to-arrow.ts
+++ b/bin/json-to-arrow.ts
@@ -19,14 +19,14 @@
import * as fs from 'node:fs';
import * as Path from 'node:path';
-import commandLineArgs from 'command-line-args';
+import { parseCliArgs, formatUsage } from '../src/bin/cli.ts';
import { finished as eos } from 'node:stream/promises';
import { parseArrowJSON } from '../src/util/json.ts';
import { RecordBatchReader, RecordBatchFileWriter, RecordBatchStreamWriter }
from '../index.ts';
-const argv = commandLineArgs(cliOpts(), { partial: true });
-const jsonPaths = [...(argv.json || [])];
-const arrowPaths = [...(argv.arrow || [])];
+const { values: argv } = parseCliArgs(cliOpts(), process.argv.slice(2));
+const jsonPaths = [...(argv.json as string[] | undefined ?? [])];
+const arrowPaths = [...(argv.arrow as string[] | undefined ?? [])];
(async () => {
@@ -81,7 +81,7 @@ function cliOpts() {
}
function print_usage() {
- console.log(require('command-line-usage')([
+ console.log(formatUsage([
{
header: 'json-to-arrow',
content: 'Script for converting a JSON Arrow file to a binary
Arrow file'
@@ -98,6 +98,7 @@ function print_usage() {
...cliOpts(),
{
name: 'help',
+ type: Boolean,
description: 'Print this usage guide.'
}
]
diff --git a/gulp/argv.js b/gulp/argv.js
index 967644e..d884a23 100644
--- a/gulp/argv.js
+++ b/gulp/argv.js
@@ -15,18 +15,50 @@
// specific language governing permissions and limitations
// under the License.
-import args from 'command-line-args';
-export const argv = args([
- { name: `all`, type: Boolean },
- { name: 'verbose', alias: `v`, type: Boolean },
- { name: `target`, type: String, defaultValue: `` },
- { name: `module`, type: String, defaultValue: `` },
- { name: `coverage`, type: Boolean, defaultValue: false },
- { name: `tests`, type: String, multiple: true, defaultValue:
[`test/unit/`] },
- { name: `targets`, alias: `t`, type: String, multiple: true, defaultValue:
[] },
- { name: `modules`, alias: `m`, type: String, multiple: true, defaultValue:
[] },
-], { partial: true });
+import { parseArgs } from 'node:util';
+const options = {
+ all: { type: 'boolean' },
+ verbose: { type: 'boolean', short: 'v' },
+ target: { type: 'string', default: '' },
+ module: { type: 'string', default: '' },
+ coverage: { type: 'boolean', default: false },
+ tests: { type: 'string', multiple: true, default: ['test/unit/'] },
+ targets: { type: 'string', short: 't', multiple: true, default: [] },
+ modules: { type: 'string', short: 'm', multiple: true, default: [] },
+};
+
+const { values, tokens } = parseArgs({
+ options,
+ args: process.argv.slice(2),
+ strict: false,
+ allowPositionals: true,
+ tokens: true,
+});
+
+// Rebuild the `_unknown` array that command-line-args' `{ partial: true }`
mode
+// used to produce. gulp/test-task.js forwards _unknown to Jest, so any flag or
+// positional we don't recognise here (e.g. `--testNamePattern foo`, `--bail`)
+// needs to round-trip through with its original raw form preserved. parseArgs
+// only flags positionals as such; unknown options land in `values` and need to
+// be reconstructed from the token stream.
+const knownNames = new Set(Object.keys(options));
+const unknown = [];
+for (const tok of tokens) {
+ if (tok.kind === 'positional') {
+ unknown.push(tok.value);
+ } else if (tok.kind === 'option' && !knownNames.has(tok.name)) {
+ unknown.push(tok.rawName);
+ // `--foo=bar` parses to a single token with `inlineValue: true`; the
+ // separate-arg form `--foo bar` puts `bar` in a following positional
+ // token, which we'll pick up on the next loop iteration.
+ if (tok.inlineValue) unknown.push(tok.value);
+ delete values[tok.name];
+ }
+}
+values._unknown = unknown;
+
+export const argv = values;
export const { targets, modules } = argv;
if (argv.target === `src`) {
diff --git a/package-lock.json b/package-lock.json
index 7e8fa4f..d2935aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,12 +9,7 @@
"version": "21.1.0",
"license": "Apache-2.0",
"dependencies": {
- "@swc/helpers": "^0.5.11",
- "@types/command-line-args": "^5.2.3",
- "@types/command-line-usage": "^5.0.4",
"@types/node": "^25.2.0",
- "command-line-args": "^6.0.1",
- "command-line-usage": "^7.0.1",
"flatbuffers": "^25.1.24",
"json-with-bigint": "^3.5.3",
"tslib": "^2.6.2"
@@ -29,6 +24,7 @@
"@rollup/stream": "3.0.1",
"@swc-node/register": "1.11.1",
"@swc/core": "1.15.32",
+ "@swc/helpers": "^0.5.11",
"@types/benchmark": "2.1.5",
"@types/glob": "8.1.0",
"@types/jest": "29.5.14",
@@ -3800,6 +3796,7 @@
"version": "0.5.23",
"resolved":
"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity":
"sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
@@ -3878,18 +3875,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/command-line-args": {
- "version": "5.2.3",
- "resolved":
"https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
- "integrity":
"sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
- "license": "MIT"
- },
- "node_modules/@types/command-line-usage": {
- "version": "5.0.4",
- "resolved":
"https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
- "integrity":
"sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
- "license": "MIT"
- },
"node_modules/@types/eslint": {
"version": "8.56.7",
"resolved":
"https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz",
@@ -4692,6 +4677,7 @@
"version": "4.3.0",
"resolved":
"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity":
"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -4767,15 +4753,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/array-back": {
- "version": "6.2.3",
- "resolved":
"https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz",
- "integrity":
"sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
- "license": "MIT",
- "engines": {
- "node": ">=12.17"
- }
- },
"node_modules/array-each": {
"version": "1.0.1",
"resolved":
"https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
@@ -5360,6 +5337,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity":
"sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -5372,21 +5350,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/chalk-template": {
- "version": "0.4.0",
- "resolved":
"https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
- "integrity":
"sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk-template?sponsor=1"
- }
- },
"node_modules/char-regex": {
"version": "1.0.2",
"resolved":
"https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -5651,6 +5614,7 @@
"version": "2.0.1",
"resolved":
"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity":
"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -5663,6 +5627,7 @@
"version": "1.1.4",
"resolved":
"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity":
"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/color-support": {
@@ -5682,44 +5647,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/command-line-args": {
- "version": "6.0.2",
- "resolved":
"https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.2.tgz",
- "integrity":
"sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==",
- "license": "MIT",
- "dependencies": {
- "array-back": "^6.2.3",
- "find-replace": "^5.0.2",
- "lodash.camelcase": "^4.3.0",
- "typical": "^7.3.0"
- },
- "engines": {
- "node": ">=12.20"
- },
- "peerDependencies": {
- "@75lb/nature": "latest"
- },
- "peerDependenciesMeta": {
- "@75lb/nature": {
- "optional": true
- }
- }
- },
- "node_modules/command-line-usage": {
- "version": "7.0.4",
- "resolved":
"https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz",
- "integrity":
"sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==",
- "license": "MIT",
- "dependencies": {
- "array-back": "^6.2.2",
- "chalk-template": "^0.4.0",
- "table-layout": "^4.1.1",
- "typical": "^7.3.0"
- },
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
@@ -6956,23 +6883,6 @@
"node": ">=8"
}
},
- "node_modules/find-replace": {
- "version": "5.0.2",
- "resolved":
"https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz",
- "integrity":
"sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==",
- "license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@75lb/nature": "latest"
- },
- "peerDependenciesMeta": {
- "@75lb/nature": {
- "optional": true
- }
- }
- },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -8262,6 +8172,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity":
"sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -10094,12 +10005,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash.camelcase": {
- "version": "4.3.0",
- "resolved":
"https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
- "integrity":
"sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
- "license": "MIT"
- },
"node_modules/lodash.clone": {
"version": "4.5.0",
"resolved":
"https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
@@ -12320,6 +12225,7 @@
"version": "7.2.0",
"resolved":
"https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity":
"sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -12362,19 +12268,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/table-layout": {
- "version": "4.1.1",
- "resolved":
"https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
- "integrity":
"sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
- "license": "MIT",
- "dependencies": {
- "array-back": "^6.2.2",
- "wordwrapjs": "^5.1.0"
- },
- "engines": {
- "node": ">=12.17"
- }
- },
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -12983,15 +12876,6 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/typical": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
- "integrity":
"sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
- "license": "MIT",
- "engines": {
- "node": ">=12.17"
- }
- },
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -13590,15 +13474,6 @@
"node": ">= 8"
}
},
- "node_modules/wordwrapjs": {
- "version": "5.1.0",
- "resolved":
"https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz",
- "integrity":
"sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==",
- "license": "MIT",
- "engines": {
- "node": ">=12.17"
- }
- },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index be6ed3a..5098723 100644
--- a/package.json
+++ b/package.json
@@ -46,12 +46,7 @@
"jest.config.js"
],
"dependencies": {
- "@swc/helpers": "^0.5.11",
- "@types/command-line-args": "^5.2.3",
- "@types/command-line-usage": "^5.0.4",
"@types/node": "^25.2.0",
- "command-line-args": "^6.0.1",
- "command-line-usage": "^7.0.1",
"flatbuffers": "^25.1.24",
"json-with-bigint": "^3.5.3",
"tslib": "^2.6.2"
@@ -63,6 +58,7 @@
"@rollup/stream": "3.0.1",
"@swc-node/register": "1.11.1",
"@swc/core": "1.15.32",
+ "@swc/helpers": "^0.5.11",
"@types/benchmark": "2.1.5",
"@types/glob": "8.1.0",
"@types/jest": "29.5.14",
diff --git a/src/bin/arrow2csv.ts b/src/bin/arrow2csv.ts
index 2835b25..314e947 100755
--- a/src/bin/arrow2csv.ts
+++ b/src/bin/arrow2csv.ts
@@ -23,12 +23,13 @@ import * as fs from 'node:fs';
import * as stream from 'node:stream';
import { Schema, RecordBatch, RecordBatchReader, AsyncByteQueue, util } from
'../Arrow.js';
-import * as commandLineUsage from 'command-line-usage';
-import * as commandLineArgs from 'command-line-args';
+import { parseCliArgs, formatUsage } from './cli.js';
import { parseArrowJSON } from '../util/json.js';
-const argv = commandLineArgs(cliOpts(), { partial: true });
-const files = argv.help ? [] : [...(argv.file || []), ...(argv._unknown ||
[])].filter(Boolean);
+const { values: argv, positionals } = parseCliArgs(cliOpts(),
process.argv.slice(2));
+const files = argv.help
+ ? []
+ : [...(argv.file as string[] | undefined ?? []),
...positionals].filter(Boolean);
const state = { ...argv, closed: false, maxColWidths: [10] };
@@ -301,7 +302,7 @@ function cliOpts() {
}
function print_usage() {
- console.log(commandLineUsage([
+ console.log(formatUsage([
{
header: 'arrow2csv',
content: 'Print a CSV from an Arrow file'
diff --git a/src/bin/cli.ts b/src/bin/cli.ts
new file mode 100644
index 0000000..b39f6d0
--- /dev/null
+++ b/src/bin/cli.ts
@@ -0,0 +1,124 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Thin wrapper over `node:util.parseArgs` for the apache-arrow bin scripts.
+// Replaces `command-line-args` + `command-line-usage` to drop two runtime deps
+// (and their transitive trees) from the published package.
+
+import { parseArgs, type ParseArgsConfig } from 'node:util';
+
+export interface OptionSpec {
+ name: string;
+ alias?: string;
+ type: StringConstructor | BooleanConstructor | NumberConstructor;
+ multiple?: boolean;
+ defaultValue?: unknown;
+ description?: string;
+ typeLabel?: string;
+ // Accepted for source-compat with the previous command-line-args specs;
ignored.
+ optional?: boolean;
+ default?: unknown;
+}
+
+export function parseCliArgs(spec: OptionSpec[], args: string[]) {
+ const options: NonNullable<ParseArgsConfig['options']> = {};
+ for (const o of spec) {
+ options[o.name] = {
+ type: o.type === Boolean ? 'boolean' : 'string',
+ ...(o.alias && { short: o.alias }),
+ ...(o.multiple && { multiple: true }),
+ ...(o.defaultValue !== undefined && { default: o.defaultValue as
never }),
+ };
+ }
+
+ const { values, tokens } = parseArgs({
+ options, args, strict: false, allowPositionals: true, tokens: true,
+ });
+
+ // `parseArgs` only honours repeated multi-flags (`-s a -s b`). To match
+ // command-line-args' greedy behaviour (`-s a b c`), walk the token stream
+ // and route positionals to the most recently seen `multiple: true` flag
+ // until another option appears.
+ const multi = new Set(spec.filter((o) => o.multiple).map((o) => o.name));
+ const out = values as Record<string, unknown>;
+ const positionals: string[] = [];
+ let owner: string | null = null;
+ for (const tok of tokens) {
+ if (tok.kind === 'option') {
+ owner = multi.has(tok.name) ? tok.name : null;
+ } else if (tok.kind === 'option-terminator') {
+ owner = null;
+ } else if (tok.kind === 'positional') {
+ if (owner) {
+ const list = (out[owner] as string[] | undefined) ?? [];
+ out[owner] = [...list, tok.value];
+ } else {
+ positionals.push(tok.value);
+ }
+ }
+ }
+
+ // Coerce Number-typed values; parseArgs only parses as string or boolean.
+ for (const o of spec) {
+ if (o.type !== Number) continue;
+ const v = out[o.name];
+ if (Array.isArray(v)) out[o.name] = v.map(Number);
+ else if (typeof v === 'string') out[o.name] = Number(v);
+ }
+
+ return { values: out, positionals };
+}
+
+export interface UsageSection {
+ header: string;
+ content?: string | string[];
+ optionList?: OptionSpec[];
+}
+
+// Drops the {bold ...} / {underline ...} chalk markup that command-line-usage
+// recognised, so existing call sites can keep their content strings unchanged.
+const stripStyles = (s: string) =>
s.replaceAll(/\{(?:bold|underline)\s+(.*?)\}/g, '$1');
+
+// Match command-line-usage's option-line layout: `-s, --schema columns`
+// (uses the explicit `typeLabel`) or `-f, --file string[]` (synthesised when
+// no `typeLabel` was provided for a string option). Boolean options get no
+// trailing label. Options without an `alias` are left-flush; aliased ones get
+// the `-x, ` prefix.
+const formatOptionHead = (o: OptionSpec) => {
+ const flag = `${o.alias ? `-${o.alias}, --` : '--'}${o.name}`;
+ if (o.typeLabel) return `${flag} ${stripStyles(o.typeLabel)}`;
+ if (o.type === Boolean) return flag;
+ return `${flag} ${o.multiple ? 'string[]' : 'string'}`;
+};
+
+export function formatUsage(sections: UsageSection[]): string {
+ const out: string[] = [];
+ for (const s of sections) {
+ out.push('', stripStyles(s.header), '');
+ const content = typeof s.content === 'string' ? [s.content] :
s.content ?? [];
+ for (const line of content) out.push(' ' + stripStyles(line));
+ if (s.optionList?.length) {
+ const heads = s.optionList.map((o) => formatOptionHead(o));
+ const width = Math.max(...heads.map((h) => h.length));
+ for (const [i, o] of s.optionList.entries()) {
+ out.push(' ' + heads[i].padEnd(width + 3) + (o.description ??
''));
+ }
+ }
+ }
+ out.push('');
+ return out.join('\n');
+}
diff --git a/test/unit/bin/cli-tests.ts b/test/unit/bin/cli-tests.ts
new file mode 100644
index 0000000..0104ec9
--- /dev/null
+++ b/test/unit/bin/cli-tests.ts
@@ -0,0 +1,171 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { parseCliArgs, formatUsage, OptionSpec } from
'../../../src/bin/cli.js';
+
+describe(`parseCliArgs`, () => {
+ const spec: OptionSpec[] = [
+ { name: 'schema', alias: 's', type: String, multiple: true },
+ { name: 'file', alias: 'f', type: String, multiple: true },
+ { name: 'sep', type: String, defaultValue: ' | ' },
+ { name: 'count', type: Number },
+ { name: 'metadata', alias: 'm', type: Boolean },
+ { name: 'help', type: Boolean },
+ ];
+
+ test('parses a boolean long flag', () => {
+ const r = parseCliArgs(spec, ['--help']);
+ expect(r.values.help).toBe(true);
+ expect(r.positionals).toEqual([]);
+ });
+
+ test('parses a short alias', () => {
+ const r = parseCliArgs(spec, ['-m']);
+ expect(r.values.metadata).toBe(true);
+ });
+
+ test('parses repeated multi-flag', () => {
+ const r = parseCliArgs(spec, ['-s', 'a', '-s', 'b', '-s', 'c']);
+ expect(r.values.schema).toEqual(['a', 'b', 'c']);
+ expect(r.positionals).toEqual([]);
+ });
+
+ test('parses space-delimited multi-flag (greedy)', () => {
+ const r = parseCliArgs(spec, ['-s', 'a', 'b', 'c']);
+ expect(r.values.schema).toEqual(['a', 'b', 'c']);
+ expect(r.positionals).toEqual([]);
+ });
+
+ test('greedy attribution stops at the next flag', () => {
+ const r = parseCliArgs(spec, ['-s', 'a', 'b', 'c', '-f', 'x.arrow']);
+ expect(r.values.schema).toEqual(['a', 'b', 'c']);
+ expect(r.values.file).toEqual(['x.arrow']);
+ expect(r.positionals).toEqual([]);
+ });
+
+ test('greedy attribution spans across multi-flags', () => {
+ const r = parseCliArgs(spec, ['-s', 'a', 'b', 'c', '-f', 'x.arrow',
'pos.arrow']);
+ expect(r.values.schema).toEqual(['a', 'b', 'c']);
+ expect(r.values.file).toEqual(['x.arrow', 'pos.arrow']);
+ expect(r.positionals).toEqual([]);
+ });
+
+ test('positionals with no preceding multi-flag stay in positionals', () =>
{
+ const r = parseCliArgs(spec, ['file1.arrow', 'file2.arrow']);
+ expect(r.values.schema).toBeUndefined();
+ expect(r.positionals).toEqual(['file1.arrow', 'file2.arrow']);
+ });
+
+ test('default values are populated when flag is absent', () => {
+ const r = parseCliArgs(spec, []);
+ expect(r.values.sep).toBe(' | ');
+ });
+
+ test('user-supplied value overrides default', () => {
+ const r = parseCliArgs(spec, ['--sep', ' , ']);
+ expect(r.values.sep).toBe(' , ');
+ });
+
+ test('unknown flags do not throw, and break greedy attribution', () => {
+ const r = parseCliArgs(spec, ['-s', 'a', '--unknown', 'b', '-f',
'x.arrow']);
+ expect(r.values.schema).toEqual(['a']);
+ expect(r.values.unknown).toBe(true);
+ expect(r.positionals).toEqual(['b']);
+ expect(r.values.file).toEqual(['x.arrow']);
+ });
+
+ test('Number type is coerced from string', () => {
+ const r = parseCliArgs(spec, ['--count', '42']);
+ expect(r.values.count).toBe(42);
+ expect(typeof r.values.count).toBe('number');
+ });
+
+ test('combined: typical arrow2csv invocation', () => {
+ const r = parseCliArgs(spec, ['-s', 'foo', 'bar', '-f',
'simple.arrow', '-m']);
+ expect(r.values.schema).toEqual(['foo', 'bar']);
+ expect(r.values.file).toEqual(['simple.arrow']);
+ expect(r.values.metadata).toBe(true);
+ });
+});
+
+describe(`formatUsage`, () => {
+ test('renders header and content sections', () => {
+ const out = formatUsage([
+ { header: 'arrow2csv', content: 'Print a CSV from an Arrow file' },
+ ]);
+ expect(out).toContain('arrow2csv');
+ expect(out).toContain('Print a CSV from an Arrow file');
+ });
+
+ test('strips {bold ...} and {underline ...} markup', () => {
+ const out = formatUsage([
+ { header: 'Synopsis', content: ['$ arrow2csv {bold -s} col1
{underline file.arrow}'] },
+ ]);
+ expect(out).toContain('$ arrow2csv -s col1 file.arrow');
+ expect(out).not.toMatch(/\{bold/);
+ expect(out).not.toMatch(/\{underline/);
+ });
+
+ test('renders an option list with aliases and descriptions', () => {
+ const out = formatUsage([
+ {
+ header: 'Options',
+ optionList: [
+ { name: 'schema', alias: 's', type: String, multiple:
true, description: 'Column names' },
+ { name: 'help', type: Boolean, description: 'Print this
usage guide.' },
+ ],
+ },
+ ]);
+ expect(out).toContain('-s, --schema');
+ expect(out).toContain('Column names');
+ expect(out).toContain('--help');
+ expect(out).toContain('Print this usage guide.');
+ });
+
+ test('option list renders correctly when no options have aliases', () => {
+ const out = formatUsage([
+ {
+ header: 'Options',
+ optionList: [
+ { name: 'mode', type: String, description: 'The mode' },
+ ],
+ },
+ ]);
+ expect(out).toContain('--mode');
+ expect(out).toContain('The mode');
+ });
+
+ test('renders type labels and synthesised type hints', () => {
+ const out = formatUsage([
+ {
+ header: 'Options',
+ optionList: [
+ { name: 'schema', alias: 's', type: String, multiple:
true, typeLabel: '{underline columns}', description: 'Column names' },
+ { name: 'file', alias: 'f', type: String, multiple: true,
description: 'The Arrow file to read' },
+ { name: 'sep', type: String, description: 'Column
separator' },
+ { name: 'metadata', alias: 'm', type: Boolean,
description: 'Print metadata' },
+ ],
+ },
+ ]);
+ expect(out).toContain('-s, --schema columns');
+ expect(out).toContain('-f, --file string[]');
+ expect(out).toContain('--sep string');
+ // Boolean options get no type label
+ expect(out).toMatch(/-m, --metadata\s+Print metadata/);
+ expect(out).not.toMatch(/--metadata\s+(string|boolean)/);
+ });
+});