utilful 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/cli/args.d.mts +44 -0
- package/dist/cli/args.mjs +109 -0
- package/dist/cli/command.d.mts +33 -0
- package/dist/cli/command.mjs +76 -0
- package/dist/cli/errors.d.mts +20 -0
- package/dist/cli/errors.mjs +43 -0
- package/dist/cli/index.d.mts +5 -0
- package/dist/cli/index.mjs +5 -0
- package/dist/cli/log.d.mts +10 -0
- package/dist/cli/log.mjs +28 -0
- package/dist/cli/style.mjs +7 -0
- package/dist/cli/testing.d.mts +29 -0
- package/dist/cli/testing.mjs +120 -0
- package/dist/cli/usage.mjs +66 -0
- package/dist/csv.mjs +10 -9
- package/dist/emitter.mjs +4 -2
- package/package.json +25 -8
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A collection of TypeScript utilities that I use across my projects.
|
|
|
7
7
|
- [Installation](#installation)
|
|
8
8
|
- [API](#api)
|
|
9
9
|
- [Array](#array)
|
|
10
|
+
- [CLI](#cli)
|
|
10
11
|
- [CSV](#csv)
|
|
11
12
|
- [Defu](#defu)
|
|
12
13
|
- [Emitter](#emitter)
|
|
@@ -37,6 +38,8 @@ import { defu } from 'utilful' // Everything
|
|
|
37
38
|
import { joinURL } from 'utilful/path' // Just the path helpers
|
|
38
39
|
```
|
|
39
40
|
|
|
41
|
+
The `cli` module is the exception: it is Node-only (22.13 or later) and lives on its subpaths `utilful/cli` and `utilful/cli/testing` alone.
|
|
42
|
+
|
|
40
43
|
## API
|
|
41
44
|
|
|
42
45
|
### Array
|
|
@@ -51,6 +54,67 @@ type MaybeArray<T> = T | T[]
|
|
|
51
54
|
declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[]
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
### CLI
|
|
58
|
+
|
|
59
|
+
A command runner on top of Node's `util.parseArgs`: strict option parsing, one level of sub-commands, `--help` and `--version`, and an error boundary that prints a recognized error as a message and anything else with its stack. `-h` and `-v` belong to the runner, so no option may take either letter as its alias.
|
|
60
|
+
|
|
61
|
+
#### `defineCommand`
|
|
62
|
+
|
|
63
|
+
Types a command definition, so `run` receives its `args` typed by their definitions.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { CliError, commonArgs, defineCommand } from 'utilful/cli'
|
|
67
|
+
|
|
68
|
+
const build = defineCommand({
|
|
69
|
+
meta: { name: 'build', description: 'Compile the entry file' },
|
|
70
|
+
args: {
|
|
71
|
+
...commonArgs, // Adds --verbose
|
|
72
|
+
'file': { type: 'positional', description: 'Entry file', required: true }, // string
|
|
73
|
+
'out-dir': { type: 'string', alias: 'd', description: 'Output directory', valueHint: 'path' }, // string | undefined
|
|
74
|
+
'watch': { type: 'boolean', alias: 'w', description: 'Rebuild on change' }, // boolean, --no-watch turns it off
|
|
75
|
+
},
|
|
76
|
+
run({ args }) {
|
|
77
|
+
if (args.watch && args['out-dir'] === undefined)
|
|
78
|
+
throw new CliError('--watch needs an --out-dir') // Printed as a message, no stack
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `runMain`
|
|
84
|
+
|
|
85
|
+
Runs a command tree from `process.argv`, or from `argv` when given, and sets `process.exitCode` instead of exiting. A tree with a `run` of its own runs it when no sub-command is named.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { defineCommand, runMain } from 'utilful/cli'
|
|
89
|
+
|
|
90
|
+
const main = defineCommand({
|
|
91
|
+
meta: { name: 'tool', version: '1.0.0', description: 'Does things' },
|
|
92
|
+
subCommands: { build },
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
void runMain(main, { expectedErrors: [MyLibraryError] }) // Reported like a `CliError`
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Also exported: `runCommand` (runs the tree and throws instead of reporting), `parseArgs`, `reportFailure` (for a failure that outlives `run`, such as a watch rebuild), and `log` with `error`, `warn`, `info`, `success` and `blankLine`, all writing to stderr.
|
|
99
|
+
|
|
100
|
+
#### `createCliHarness`
|
|
101
|
+
|
|
102
|
+
From `utilful/cli/testing`, which needs `vitest`. `runCli` runs the tree in-process and captures both streams and the exit code, `runCliProcess` runs the entry file as a child process. `useTemporaryDirectories` and `mockStdin` ship alongside.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { createCliHarness, useTemporaryDirectories } from 'utilful/cli/testing'
|
|
106
|
+
|
|
107
|
+
const { runCli, runCliProcess } = createCliHarness(main, { entry: 'src/entry.ts' }) // `entry` only matters to `runCliProcess`
|
|
108
|
+
const createDirectory = useTemporaryDirectories()
|
|
109
|
+
|
|
110
|
+
it('rejects an unknown option', async () => {
|
|
111
|
+
const { stderr, exitCode } = await runCli(['build', 'x.js', '--typo'])
|
|
112
|
+
|
|
113
|
+
expect(stderr).toContain('Unknown option \'--typo\'')
|
|
114
|
+
expect(exitCode).toBe(1)
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
54
118
|
### CSV
|
|
55
119
|
|
|
56
120
|
#### `createCSV`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/cli/args.d.ts
|
|
2
|
+
interface StringArgDef {
|
|
3
|
+
type: "string";
|
|
4
|
+
/** One letter, as in `-d`. */
|
|
5
|
+
alias?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
default?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
/** Placeholder in the help, as in `--out-dir=<path>`. */
|
|
10
|
+
valueHint?: string;
|
|
11
|
+
}
|
|
12
|
+
interface BooleanArgDef {
|
|
13
|
+
type: "boolean";
|
|
14
|
+
alias?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
default?: boolean;
|
|
17
|
+
}
|
|
18
|
+
interface PositionalArgDef {
|
|
19
|
+
type: "positional";
|
|
20
|
+
description?: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
}
|
|
23
|
+
type ArgDef = StringArgDef | BooleanArgDef | PositionalArgDef;
|
|
24
|
+
type ArgsDef = Record<string, ArgDef>;
|
|
25
|
+
type ParsedArgs<T extends ArgsDef = ArgsDef> = {
|
|
26
|
+
/** Every positional, bound by a definition or not. */
|
|
27
|
+
_: string[];
|
|
28
|
+
} & { [K in keyof T]: ArgDef extends T[K] ? string | boolean | undefined : T[K] extends {
|
|
29
|
+
type: "boolean";
|
|
30
|
+
} ? boolean : T[K] extends {
|
|
31
|
+
default: string;
|
|
32
|
+
} | {
|
|
33
|
+
required: true;
|
|
34
|
+
} ? string : string | undefined; };
|
|
35
|
+
interface CommonArgs extends ArgsDef {
|
|
36
|
+
verbose: BooleanArgDef;
|
|
37
|
+
}
|
|
38
|
+
declare const commonArgs: CommonArgs;
|
|
39
|
+
/** Parses `argv` against a definition. An absent boolean reads as `false`, and `--no-<name>` turns one off. */
|
|
40
|
+
declare function parseArgs$1<T extends ArgsDef>(argv: readonly string[], argsDef: T, { allowExtraPositionals }?: {
|
|
41
|
+
allowExtraPositionals?: boolean;
|
|
42
|
+
}): ParsedArgs<T>;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ArgDef, ArgsDef, BooleanArgDef, CommonArgs, ParsedArgs, PositionalArgDef, StringArgDef, commonArgs, parseArgs$1 as parseArgs };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ArgumentError } from "./errors.mjs";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
//#region src/cli/args.ts
|
|
4
|
+
const commonArgs = { verbose: {
|
|
5
|
+
type: "boolean",
|
|
6
|
+
description: "Print the cause chain and stack trace on failure"
|
|
7
|
+
} };
|
|
8
|
+
/** Parses `argv` against a definition. An absent boolean reads as `false`, and `--no-<name>` turns one off. */
|
|
9
|
+
function parseArgs$1(argv, argsDef, { allowExtraPositionals = false } = {}) {
|
|
10
|
+
let parseResult;
|
|
11
|
+
try {
|
|
12
|
+
parseResult = parseArgs({
|
|
13
|
+
args: joinNegativeValues(splitShortOptionValues(argv), argsDef),
|
|
14
|
+
options: toNodeOptions(argsDef),
|
|
15
|
+
strict: true,
|
|
16
|
+
allowPositionals: true,
|
|
17
|
+
allowNegative: true
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (isNodeArgumentError(error)) throw new ArgumentError(error.message.split(". ")[0]);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const args = { _: parseResult.positionals };
|
|
24
|
+
const positionalNames = [];
|
|
25
|
+
for (const [name, definition] of Object.entries(argsDef)) {
|
|
26
|
+
if (definition.type === "positional") {
|
|
27
|
+
positionalNames.push(name);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const value = parseResult.values[name];
|
|
31
|
+
if (definition.type === "boolean") args[name] = value ?? false;
|
|
32
|
+
else if (value === void 0 && definition.required === true) throw new ArgumentError(`Missing required argument: --${name}`);
|
|
33
|
+
else args[name] = value;
|
|
34
|
+
}
|
|
35
|
+
positionalNames.forEach((name, index) => {
|
|
36
|
+
const definition = argsDef[name];
|
|
37
|
+
const value = parseResult.positionals[index];
|
|
38
|
+
if (value === void 0 && definition.required === true) throw new ArgumentError(`Missing required positional argument: ${name.toUpperCase()}`);
|
|
39
|
+
args[name] = value;
|
|
40
|
+
});
|
|
41
|
+
if (!allowExtraPositionals && parseResult.positionals.length > positionalNames.length) throw new ArgumentError(`Unexpected argument: ${JSON.stringify(parseResult.positionals[positionalNames.length])}`);
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
function toNodeOptions(argsDef) {
|
|
45
|
+
const options = {};
|
|
46
|
+
for (const [name, definition] of Object.entries(argsDef)) {
|
|
47
|
+
if (definition.type === "positional") continue;
|
|
48
|
+
const option = { type: definition.type };
|
|
49
|
+
if (definition.alias !== void 0) option.short = definition.alias;
|
|
50
|
+
if (definition.default !== void 0) option.default = definition.default;
|
|
51
|
+
options[name] = option;
|
|
52
|
+
}
|
|
53
|
+
return options;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Splits an inline value off a short option. Node's `parseArgs` splits
|
|
57
|
+
* `--name=value` but leaves `-n=value` whole, so `-o=report.json` would write to
|
|
58
|
+
* a file named `=report.json`. Past `--` every token is an operand and stays as
|
|
59
|
+
* it was written.
|
|
60
|
+
*/
|
|
61
|
+
function splitShortOptionValues(argv) {
|
|
62
|
+
const splitArguments = [];
|
|
63
|
+
let isTerminated = false;
|
|
64
|
+
for (const argument of argv) {
|
|
65
|
+
const match = /^(-[^-])=(.*)$/.exec(argument);
|
|
66
|
+
if (isTerminated || match === null) {
|
|
67
|
+
splitArguments.push(argument);
|
|
68
|
+
isTerminated ||= argument === "--";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
splitArguments.push(match[1], match[2]);
|
|
72
|
+
}
|
|
73
|
+
return splitArguments;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Joins a negative number onto the value-taking option before it, as
|
|
77
|
+
* `--start=-3`. Node refuses `--start -3` as ambiguous, but a token that starts
|
|
78
|
+
* with a digit after its dash is a number wherever a value is due.
|
|
79
|
+
*/
|
|
80
|
+
function joinNegativeValues(argv, argsDef) {
|
|
81
|
+
const optionNamesBySpelling = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const [name, definition] of Object.entries(argsDef)) {
|
|
83
|
+
if (definition.type !== "string") continue;
|
|
84
|
+
optionNamesBySpelling.set(`--${name}`, name);
|
|
85
|
+
if (definition.alias !== void 0) optionNamesBySpelling.set(`-${definition.alias}`, name);
|
|
86
|
+
}
|
|
87
|
+
const joinedArguments = [];
|
|
88
|
+
for (let index = 0; index < argv.length; index++) {
|
|
89
|
+
const argument = argv[index];
|
|
90
|
+
const next = argv[index + 1];
|
|
91
|
+
const name = optionNamesBySpelling.get(argument);
|
|
92
|
+
if (argument === "--") {
|
|
93
|
+
joinedArguments.push(...argv.slice(index));
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (name !== void 0 && next !== void 0 && /^-\d/.test(next)) {
|
|
97
|
+
joinedArguments.push(`--${name}=${next}`);
|
|
98
|
+
index++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
joinedArguments.push(argument);
|
|
102
|
+
}
|
|
103
|
+
return joinedArguments;
|
|
104
|
+
}
|
|
105
|
+
function isNodeArgumentError(error) {
|
|
106
|
+
return error instanceof Error && String(error.code).startsWith("ERR_PARSE_ARGS");
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { commonArgs, parseArgs$1 as parseArgs, toNodeOptions };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ArgsDef, ParsedArgs } from "./args.mjs";
|
|
2
|
+
import { ReportOptions } from "./errors.mjs";
|
|
3
|
+
//#region src/cli/command.d.ts
|
|
4
|
+
interface CommandMeta {
|
|
5
|
+
name?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
interface CommandContext<T extends ArgsDef = ArgsDef> {
|
|
10
|
+
args: ParsedArgs<T>;
|
|
11
|
+
}
|
|
12
|
+
interface CommandDef<T extends ArgsDef = ArgsDef> {
|
|
13
|
+
meta?: CommandMeta;
|
|
14
|
+
args?: T;
|
|
15
|
+
subCommands?: Record<string, CommandDef<any>>;
|
|
16
|
+
allowExtraPositionals?: boolean;
|
|
17
|
+
run?: (context: CommandContext<T>) => unknown;
|
|
18
|
+
}
|
|
19
|
+
interface RunMainOptions extends Omit<ReportOptions, "verbose"> {
|
|
20
|
+
/** @default process.argv.slice(2) */
|
|
21
|
+
argv?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
declare function defineCommand<T extends ArgsDef>(command: CommandDef<T>): CommandDef<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Runs the command tree and reports whatever it throws. Help someone asked for
|
|
26
|
+
* is the result of the run and goes to stdout; usage that follows a wrong
|
|
27
|
+
* argument is diagnostics and joins its message on stderr.
|
|
28
|
+
*/
|
|
29
|
+
declare function runMain<T extends ArgsDef>(command: CommandDef<T>, options?: RunMainOptions): Promise<void>;
|
|
30
|
+
/** Runs like `runMain`, but without the error boundary. */
|
|
31
|
+
declare function runCommand<T extends ArgsDef>(command: CommandDef<T>, argv: readonly string[]): Promise<void>;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { CommandContext, CommandDef, CommandMeta, RunMainOptions, defineCommand, runCommand, runMain };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ArgumentError, reportFailure } from "./errors.mjs";
|
|
2
|
+
import { parseArgs as parseArgs$1, toNodeOptions } from "./args.mjs";
|
|
3
|
+
import { renderUsage } from "./usage.mjs";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
//#region src/cli/command.ts
|
|
7
|
+
const HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h"]);
|
|
8
|
+
const VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
|
|
9
|
+
function defineCommand(command) {
|
|
10
|
+
return command;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs the command tree and reports whatever it throws. Help someone asked for
|
|
14
|
+
* is the result of the run and goes to stdout; usage that follows a wrong
|
|
15
|
+
* argument is diagnostics and joins its message on stderr.
|
|
16
|
+
*/
|
|
17
|
+
async function runMain(command, options = {}) {
|
|
18
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
19
|
+
const beforeTerminator = argv.slice(0, argv.includes("--") ? argv.indexOf("--") : void 0);
|
|
20
|
+
const hasFlag = (flags) => beforeTerminator.some((argument) => flags.has(argument));
|
|
21
|
+
try {
|
|
22
|
+
if (hasFlag(HELP_FLAGS)) process.stdout.write(`${usageFor(command, argv, process.stdout)}\n`);
|
|
23
|
+
else if (hasFlag(VERSION_FLAGS)) process.stdout.write(`${command.meta?.version ?? ""}\n`);
|
|
24
|
+
else await runCommand(command, argv);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof ArgumentError) process.stderr.write(`${usageFor(command, argv, process.stderr)}\n\n`);
|
|
27
|
+
const verbose = !(error instanceof ArgumentError) && beforeTerminator.includes("--verbose");
|
|
28
|
+
reportFailure(error, {
|
|
29
|
+
...options,
|
|
30
|
+
verbose
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Runs like `runMain`, but without the error boundary. */
|
|
35
|
+
async function runCommand(command, argv) {
|
|
36
|
+
const { name, firstOperand, rest } = findSubCommand(command, argv);
|
|
37
|
+
if (name !== void 0) return runCommand(command.subCommands[name], rest);
|
|
38
|
+
if (command.subCommands !== void 0 && command.run === void 0) throw new ArgumentError(firstOperand === void 0 ? "Missing command" : `Unknown command: ${firstOperand}`);
|
|
39
|
+
const args = parseArgs$1(rest, command.args ?? {}, { allowExtraPositionals: command.allowExtraPositionals });
|
|
40
|
+
await command.run?.({ args });
|
|
41
|
+
}
|
|
42
|
+
function usageFor(command, argv, stream) {
|
|
43
|
+
const { name } = findSubCommand(command, argv);
|
|
44
|
+
return name === void 0 ? renderUsage(command, { stream }) : renderUsage(command.subCommands[name], {
|
|
45
|
+
parent: command,
|
|
46
|
+
stream
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Finds the sub-command the first operand names, wherever it stands among the
|
|
51
|
+
* options. Telling an operand from the value of an option needs every
|
|
52
|
+
* value-taking option of every sub-command, since the command is still unknown.
|
|
53
|
+
*/
|
|
54
|
+
function findSubCommand(command, argv) {
|
|
55
|
+
if (command.subCommands === void 0) return { rest: [...argv] };
|
|
56
|
+
const options = Object.assign(toNodeOptions(command.args ?? {}), ...Object.values(command.subCommands).map((subCommand) => toNodeOptions(subCommand.args ?? {})));
|
|
57
|
+
const { tokens } = parseArgs({
|
|
58
|
+
args: [...argv],
|
|
59
|
+
options,
|
|
60
|
+
strict: false,
|
|
61
|
+
allowPositionals: true,
|
|
62
|
+
tokens: true
|
|
63
|
+
});
|
|
64
|
+
const firstOperand = tokens.find((token) => token.kind === "positional" || token.kind === "option-terminator");
|
|
65
|
+
if (firstOperand?.kind !== "positional") return { rest: [...argv] };
|
|
66
|
+
if (Object.hasOwn(command.subCommands, firstOperand.value)) return {
|
|
67
|
+
name: firstOperand.value,
|
|
68
|
+
rest: argv.toSpliced(firstOperand.index, 1)
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
firstOperand: firstOperand.value,
|
|
72
|
+
rest: [...argv]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { defineCommand, runCommand, runMain };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/cli/errors.d.ts
|
|
2
|
+
type ErrorClass = abstract new (...args: never[]) => Error;
|
|
3
|
+
interface ReportOptions {
|
|
4
|
+
verbose?: boolean;
|
|
5
|
+
/** Treated like `CliError`: message only, no stack. */
|
|
6
|
+
expectedErrors?: readonly ErrorClass[];
|
|
7
|
+
/** Renders an error where its message alone will not do; `undefined` falls back to the message. */
|
|
8
|
+
describe?: (error: Error) => string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A condition the CLI recognized and phrased for a human. Anything else
|
|
12
|
+
* reaching the boundary is a defect in the tool and prints its stack unasked.
|
|
13
|
+
*/
|
|
14
|
+
declare class CliError extends Error {}
|
|
15
|
+
/** Gets usage printed alongside its message. */
|
|
16
|
+
declare class ArgumentError extends CliError {}
|
|
17
|
+
/** Reports a failure the way the boundary does, for one that outlives `run`, such as a watch rebuild. */
|
|
18
|
+
declare function reportFailure(error: unknown, { verbose, expectedErrors, describe }?: ReportOptions): void;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ArgumentError, CliError, ErrorClass, ReportOptions, reportFailure };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { error } from "./log.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
//#region src/cli/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* A condition the CLI recognized and phrased for a human. Anything else
|
|
6
|
+
* reaching the boundary is a defect in the tool and prints its stack unasked.
|
|
7
|
+
*/
|
|
8
|
+
var CliError = class extends Error {};
|
|
9
|
+
/** Gets usage printed alongside its message. */
|
|
10
|
+
var ArgumentError = class extends CliError {};
|
|
11
|
+
/** Reports a failure the way the boundary does, for one that outlives `run`, such as a watch rebuild. */
|
|
12
|
+
function reportFailure(error$1, { verbose = false, expectedErrors = [], describe } = {}) {
|
|
13
|
+
const sections = [error$1 instanceof Error ? describe?.(error$1) ?? error$1.message : String(error$1)];
|
|
14
|
+
if (verbose || !isExpected(error$1, expectedErrors)) {
|
|
15
|
+
const causeChain = formatCauseChain(error$1);
|
|
16
|
+
if (causeChain) sections.push(causeChain);
|
|
17
|
+
if (error$1 instanceof Error && error$1.stack) sections.push(error$1.stack);
|
|
18
|
+
}
|
|
19
|
+
error(sections.join("\n\n"));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reports whether the CLI raised this error deliberately rather than tripping
|
|
24
|
+
* over it. A system error such as `ENOENT` reaches the boundary as the honest
|
|
25
|
+
* answer to what the user asked for, so it reads as deliberate too, while an
|
|
26
|
+
* `ERR_*` error from a Node API is a wrong call and stays a defect.
|
|
27
|
+
*/
|
|
28
|
+
function isExpected(error, expectedErrors) {
|
|
29
|
+
if (error instanceof CliError) return true;
|
|
30
|
+
if (expectedErrors.some((expectedError) => error instanceof expectedError)) return true;
|
|
31
|
+
return error instanceof Error && /^E[A-Z0-9]+$/.test(String(error.code));
|
|
32
|
+
}
|
|
33
|
+
function formatCauseChain(error) {
|
|
34
|
+
const causeLines = [];
|
|
35
|
+
let current = error instanceof Error ? error.cause : void 0;
|
|
36
|
+
while (current instanceof Error) {
|
|
37
|
+
causeLines.push(`Caused by: ${current.name || "Error"}: ${current.message}`);
|
|
38
|
+
current = current.cause;
|
|
39
|
+
}
|
|
40
|
+
return causeLines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ArgumentError, CliError, reportFailure };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ArgDef, ArgsDef, BooleanArgDef, CommonArgs, ParsedArgs, PositionalArgDef, StringArgDef, commonArgs, parseArgs } from "./args.mjs";
|
|
2
|
+
import { ArgumentError, CliError, ErrorClass, ReportOptions, reportFailure } from "./errors.mjs";
|
|
3
|
+
import { CommandContext, CommandDef, CommandMeta, RunMainOptions, defineCommand, runCommand, runMain } from "./command.mjs";
|
|
4
|
+
import { log_d_exports } from "./log.mjs";
|
|
5
|
+
export { type ArgDef, type ArgsDef, ArgumentError, type BooleanArgDef, CliError, type CommandContext, type CommandDef, type CommandMeta, type CommonArgs, type ErrorClass, type ParsedArgs, type PositionalArgDef, type ReportOptions, type RunMainOptions, type StringArgDef, commonArgs, defineCommand, log_d_exports as log, parseArgs, reportFailure, runCommand, runMain };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { log_exports } from "./log.mjs";
|
|
2
|
+
import { ArgumentError, CliError, reportFailure } from "./errors.mjs";
|
|
3
|
+
import { commonArgs, parseArgs } from "./args.mjs";
|
|
4
|
+
import { defineCommand, runCommand, runMain } from "./command.mjs";
|
|
5
|
+
export { ArgumentError, CliError, commonArgs, defineCommand, log_exports as log, parseArgs, reportFailure, runCommand, runMain };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare namespace log_d_exports {
|
|
2
|
+
export { blankLine, error, info, success, warn };
|
|
3
|
+
}
|
|
4
|
+
declare function error(message: string): void;
|
|
5
|
+
declare function warn(message: string): void;
|
|
6
|
+
declare function info(message: string): void;
|
|
7
|
+
declare function success(message: string): void;
|
|
8
|
+
declare function blankLine(): void;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { blankLine, error, info, log_d_exports, success, warn };
|
package/dist/cli/log.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { paint } from "./style.mjs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
//#region src/cli/log.ts
|
|
5
|
+
var log_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
blankLine: () => blankLine,
|
|
7
|
+
error: () => error,
|
|
8
|
+
info: () => info,
|
|
9
|
+
success: () => success,
|
|
10
|
+
warn: () => warn
|
|
11
|
+
});
|
|
12
|
+
function error(message) {
|
|
13
|
+
console.error(`${paint("red", "✖", process.stderr)} ${message}`);
|
|
14
|
+
}
|
|
15
|
+
function warn(message) {
|
|
16
|
+
console.error(`${paint("yellow", "⚠", process.stderr)} ${message}`);
|
|
17
|
+
}
|
|
18
|
+
function info(message) {
|
|
19
|
+
console.error(`${paint("cyan", "●", process.stderr)} ${message}`);
|
|
20
|
+
}
|
|
21
|
+
function success(message) {
|
|
22
|
+
console.error(`${paint("green", "✔", process.stderr)} ${message}`);
|
|
23
|
+
}
|
|
24
|
+
function blankLine() {
|
|
25
|
+
console.error("");
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { blankLine, error, info, log_exports, success, warn };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ArgsDef } from "./args.mjs";
|
|
2
|
+
import { CommandDef, RunMainOptions } from "./command.mjs";
|
|
3
|
+
//#region src/cli/testing.d.ts
|
|
4
|
+
interface FileMap {
|
|
5
|
+
[relativePath: string]: string;
|
|
6
|
+
}
|
|
7
|
+
interface CliResult {
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
}
|
|
12
|
+
interface RunOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
interface CliHarnessOptions extends Omit<RunMainOptions, "argv"> {
|
|
16
|
+
/** Only `runCliProcess` needs it. */
|
|
17
|
+
entry?: string;
|
|
18
|
+
}
|
|
19
|
+
interface CliHarness {
|
|
20
|
+
runCli: (argv: readonly string[], options?: RunOptions) => Promise<CliResult>;
|
|
21
|
+
/** Runs the entry file as a child process, where the exit code is real and stdout can be cut short by exiting. */
|
|
22
|
+
runCliProcess: (argv: readonly string[], options?: RunOptions) => Promise<CliResult>;
|
|
23
|
+
}
|
|
24
|
+
declare function createCliHarness<T extends ArgsDef>(command: CommandDef<T>, { entry, ...runOptions }?: CliHarnessOptions): CliHarness;
|
|
25
|
+
/** Registers an `afterEach` cleanup, so call it at the top level of a test file. */
|
|
26
|
+
declare function useTemporaryDirectories(prefix?: string): (files?: FileMap) => string;
|
|
27
|
+
declare function mockStdin(input: string): () => void;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { CliHarness, CliHarnessOptions, CliResult, FileMap, RunOptions, createCliHarness, mockStdin, useTemporaryDirectories };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runMain } from "./command.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import { afterEach, vi } from "vitest";
|
|
9
|
+
//#region src/cli/testing.ts
|
|
10
|
+
var ProcessExitError = class extends Error {
|
|
11
|
+
constructor(exitCode) {
|
|
12
|
+
super(`process.exit(${exitCode})`);
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function createCliHarness(command, { entry, ...runOptions } = {}) {
|
|
17
|
+
return {
|
|
18
|
+
async runCli(argv, options = {}) {
|
|
19
|
+
const stdout = [];
|
|
20
|
+
const stderr = [];
|
|
21
|
+
const previousExitCode = process.exitCode;
|
|
22
|
+
const previousCwd = process.cwd();
|
|
23
|
+
process.exitCode = void 0;
|
|
24
|
+
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
|
25
|
+
stdout.push(String(chunk));
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
29
|
+
stderr.push(String(chunk));
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation((...parts) => {
|
|
33
|
+
stdout.push(`${parts.map(String).join(" ")}\n`);
|
|
34
|
+
});
|
|
35
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((...parts) => {
|
|
36
|
+
stderr.push(`${parts.map(String).join(" ")}\n`);
|
|
37
|
+
});
|
|
38
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => {
|
|
39
|
+
throw new ProcessExitError(typeof code === "number" ? code : 0);
|
|
40
|
+
});
|
|
41
|
+
let exitCode;
|
|
42
|
+
try {
|
|
43
|
+
if (options.cwd !== void 0) process.chdir(options.cwd);
|
|
44
|
+
await runMain(command, {
|
|
45
|
+
...runOptions,
|
|
46
|
+
argv
|
|
47
|
+
});
|
|
48
|
+
exitCode = process.exitCode ?? 0;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!(error instanceof ProcessExitError)) throw error;
|
|
51
|
+
exitCode = error.exitCode;
|
|
52
|
+
} finally {
|
|
53
|
+
process.chdir(previousCwd);
|
|
54
|
+
process.exitCode = previousExitCode;
|
|
55
|
+
exitSpy.mockRestore();
|
|
56
|
+
consoleErrorSpy.mockRestore();
|
|
57
|
+
consoleLogSpy.mockRestore();
|
|
58
|
+
stderrSpy.mockRestore();
|
|
59
|
+
stdoutSpy.mockRestore();
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
stdout: stdout.join(""),
|
|
63
|
+
stderr: stderr.join(""),
|
|
64
|
+
exitCode
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
runCliProcess(argv, options = {}) {
|
|
68
|
+
if (entry === void 0) return Promise.reject(/* @__PURE__ */ new Error("runCliProcess needs the entry file of the CLI"));
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
execFile(process.execPath, [entry, ...argv], {
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
maxBuffer: 67108864
|
|
73
|
+
}, (spawnError, stdout, stderr) => {
|
|
74
|
+
if (spawnError && typeof spawnError.code !== "number") reject(spawnError);
|
|
75
|
+
else resolve({
|
|
76
|
+
stdout,
|
|
77
|
+
stderr,
|
|
78
|
+
exitCode: spawnError ? spawnError.code : 0
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Registers an `afterEach` cleanup, so call it at the top level of a test file. */
|
|
86
|
+
function useTemporaryDirectories(prefix = "cli-test-") {
|
|
87
|
+
const directories = [];
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
while (directories.length > 0) rmSync(directories.pop(), {
|
|
90
|
+
recursive: true,
|
|
91
|
+
force: true
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
return (files = {}) => {
|
|
95
|
+
const directory = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
96
|
+
directories.push(directory);
|
|
97
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
98
|
+
const filePath = path.join(directory, relativePath);
|
|
99
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
100
|
+
writeFileSync(filePath, contents, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
return directory;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function mockStdin(input) {
|
|
106
|
+
const stream = Readable.from([new TextEncoder().encode(input)]);
|
|
107
|
+
const originalStdin = process.stdin;
|
|
108
|
+
Object.defineProperty(process, "stdin", {
|
|
109
|
+
value: stream,
|
|
110
|
+
writable: true
|
|
111
|
+
});
|
|
112
|
+
return () => {
|
|
113
|
+
Object.defineProperty(process, "stdin", {
|
|
114
|
+
value: originalStdin,
|
|
115
|
+
writable: true
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { createCliHarness, mockStdin, useTemporaryDirectories };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { paint } from "./style.mjs";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
//#region src/cli/usage.ts
|
|
5
|
+
function renderUsage(command, { parent, stream = process.stdout } = {}) {
|
|
6
|
+
const color = (style, text) => paint(style, text, stream);
|
|
7
|
+
const heading = (title) => color("bold", title);
|
|
8
|
+
const meta = command.meta ?? {};
|
|
9
|
+
const parentMeta = parent?.meta ?? {};
|
|
10
|
+
const commandName = [parentMeta.name, meta.name].filter((name) => name !== void 0).join(" ");
|
|
11
|
+
const version = meta.version ?? parentMeta.version;
|
|
12
|
+
const positionalLines = [];
|
|
13
|
+
const optionLines = [];
|
|
14
|
+
const commandLines = [];
|
|
15
|
+
const usageLine = [];
|
|
16
|
+
for (const [name, definition] of Object.entries(command.args ?? {})) {
|
|
17
|
+
const isRequired = definition.type !== "boolean" && definition.required === true;
|
|
18
|
+
const hasDefault = definition.type !== "positional" && definition.default !== void 0 && definition.default !== false;
|
|
19
|
+
const hints = [
|
|
20
|
+
definition.description,
|
|
21
|
+
isRequired ? color("gray", "(Required)") : void 0,
|
|
22
|
+
hasDefault ? color("gray", `(Default: ${definition.default})`) : void 0
|
|
23
|
+
].filter((hint) => hint !== void 0).join(" ");
|
|
24
|
+
if (definition.type === "positional") {
|
|
25
|
+
const label = name.toUpperCase();
|
|
26
|
+
positionalLines.push([color("cyan", label), hints]);
|
|
27
|
+
usageLine.push(isRequired ? `<${label}>` : `[${label}]`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const spellings = [definition.alias === void 0 ? void 0 : `-${definition.alias}`, `--${name}`].filter((spelling) => spelling !== void 0).join(", ");
|
|
31
|
+
const value = definition.type === "string" ? `<${definition.valueHint ?? name}>` : void 0;
|
|
32
|
+
optionLines.push([color("cyan", spellings) + (value === void 0 ? "" : color("dim", `=${value}`)), hints]);
|
|
33
|
+
if (definition.type === "boolean" && definition.default === true) optionLines.push([color("cyan", `--no-${name}`), ""]);
|
|
34
|
+
if (isRequired) usageLine.push(`--${name}=${value}`);
|
|
35
|
+
}
|
|
36
|
+
for (const [name, subCommand] of Object.entries(command.subCommands ?? {})) commandLines.push([color("cyan", name), subCommand.meta?.description ?? ""]);
|
|
37
|
+
const hasArguments = positionalLines.length > 0 || optionLines.length > 0;
|
|
38
|
+
if (commandLines.length > 0 && !hasArguments) usageLine.push(Object.keys(command.subCommands).join("|"));
|
|
39
|
+
const lines = [
|
|
40
|
+
color("bold", version === void 0 ? commandName : `${commandName} v${version}`),
|
|
41
|
+
...meta.description === void 0 ? [] : [meta.description],
|
|
42
|
+
"",
|
|
43
|
+
`${heading("USAGE")} ${color("cyan", [
|
|
44
|
+
commandName,
|
|
45
|
+
hasArguments ? "[OPTIONS]" : void 0,
|
|
46
|
+
...usageLine
|
|
47
|
+
].filter((part) => part !== void 0).join(" "))}`,
|
|
48
|
+
""
|
|
49
|
+
];
|
|
50
|
+
for (const [title, rows] of [
|
|
51
|
+
["ARGUMENTS", positionalLines],
|
|
52
|
+
["OPTIONS", optionLines],
|
|
53
|
+
["COMMANDS", commandLines]
|
|
54
|
+
]) if (rows.length > 0) lines.push(heading(title), "", formatColumns(rows), "");
|
|
55
|
+
if (commandLines.length > 0) lines.push(`Use ${color("cyan", `${commandName} <command> --help`)} for more information about a command.`);
|
|
56
|
+
return lines.join("\n").trimEnd();
|
|
57
|
+
}
|
|
58
|
+
function formatColumns(rows) {
|
|
59
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => visibleLength(row[column]))));
|
|
60
|
+
return rows.map((row) => row.map((cell, column) => ` ${cell}${" ".repeat(widths[column] - visibleLength(cell))}`).join("").trimEnd()).join("\n");
|
|
61
|
+
}
|
|
62
|
+
function visibleLength(text) {
|
|
63
|
+
return stripVTControlCharacters(text).length;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { renderUsage };
|
package/dist/csv.mjs
CHANGED
|
@@ -154,15 +154,16 @@ var CSVParserCore = class {
|
|
|
154
154
|
const character = text[i];
|
|
155
155
|
const nextCharacter = i + 1 < text.length ? text[i + 1] : "";
|
|
156
156
|
if (this.isFieldQuoted && !this.inQuotes && character !== this.delimiter && (character === SPACE || character === TAB)) continue;
|
|
157
|
-
if (character === DOUBLE_QUOTE)
|
|
158
|
-
this.inQuotes
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
if (character === DOUBLE_QUOTE) {
|
|
158
|
+
if (this.currentField.length === 0 && !this.inQuotes) {
|
|
159
|
+
this.inQuotes = true;
|
|
160
|
+
this.isFieldQuoted = true;
|
|
161
|
+
} else if (this.inQuotes && nextCharacter === DOUBLE_QUOTE) {
|
|
162
|
+
this.currentField += DOUBLE_QUOTE;
|
|
163
|
+
i++;
|
|
164
|
+
} else if (this.inQuotes) this.inQuotes = false;
|
|
165
|
+
else this.currentField += character;
|
|
166
|
+
} else if (character === this.delimiter && !this.inQuotes) this.appendField();
|
|
166
167
|
else if ((character === NEWLINE || character === CARRIAGE_RETURN) && !this.inQuotes) {
|
|
167
168
|
if (character === CARRIAGE_RETURN && nextCharacter === NEWLINE) i++;
|
|
168
169
|
this.appendRow();
|
package/dist/emitter.mjs
CHANGED
|
@@ -28,8 +28,10 @@ function createEmitter(events) {
|
|
|
28
28
|
*/
|
|
29
29
|
off(type, handler) {
|
|
30
30
|
const handlers = events.get(type);
|
|
31
|
-
if (handlers)
|
|
32
|
-
|
|
31
|
+
if (handlers) {
|
|
32
|
+
if (handler) handlers.splice(handlers.indexOf(handler) >>> 0, 1);
|
|
33
|
+
else events.set(type, []);
|
|
34
|
+
}
|
|
33
35
|
},
|
|
34
36
|
/**
|
|
35
37
|
* Invokes all handlers for the given type.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "utilful",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
5
|
-
"packageManager": "pnpm@11.
|
|
4
|
+
"version": "3.3.0",
|
|
5
|
+
"packageManager": "pnpm@11.25.0",
|
|
6
6
|
"description": "A collection of TypeScript utilities",
|
|
7
7
|
"author": "Johann Schopplich <hello@johannschopplich.com>",
|
|
8
8
|
"license": "MIT",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/johannschopplich/utilful/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
+
"cli",
|
|
18
19
|
"csv",
|
|
19
20
|
"defu",
|
|
20
21
|
"emitter",
|
|
@@ -33,6 +34,14 @@
|
|
|
33
34
|
"types": "./dist/array.d.mts",
|
|
34
35
|
"default": "./dist/array.mjs"
|
|
35
36
|
},
|
|
37
|
+
"./cli": {
|
|
38
|
+
"types": "./dist/cli/index.d.mts",
|
|
39
|
+
"default": "./dist/cli/index.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./cli/testing": {
|
|
42
|
+
"types": "./dist/cli/testing.d.mts",
|
|
43
|
+
"default": "./dist/cli/testing.mjs"
|
|
44
|
+
},
|
|
36
45
|
"./csv": {
|
|
37
46
|
"types": "./dist/csv.d.mts",
|
|
38
47
|
"default": "./dist/csv.mjs"
|
|
@@ -86,14 +95,22 @@
|
|
|
86
95
|
"test:types": "tsc --noEmit",
|
|
87
96
|
"release": "bumpp"
|
|
88
97
|
},
|
|
98
|
+
"peerDependencies": {
|
|
99
|
+
"vitest": ">=4"
|
|
100
|
+
},
|
|
101
|
+
"peerDependenciesMeta": {
|
|
102
|
+
"vitest": {
|
|
103
|
+
"optional": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
89
106
|
"devDependencies": {
|
|
90
|
-
"@antfu/eslint-config": "^9.
|
|
91
|
-
"@types/node": "^26.
|
|
92
|
-
"bumpp": "^12.
|
|
93
|
-
"eslint": "^10.
|
|
107
|
+
"@antfu/eslint-config": "^9.5.0",
|
|
108
|
+
"@types/node": "^26.4.1",
|
|
109
|
+
"bumpp": "^12.2.2",
|
|
110
|
+
"eslint": "^10.9.1",
|
|
94
111
|
"eslint-flat-config-utils": "^3.2.0",
|
|
95
|
-
"tsdown": "^0.22.
|
|
112
|
+
"tsdown": "^0.22.14",
|
|
96
113
|
"typescript": "^6.0.3",
|
|
97
|
-
"vitest": "^4.1.
|
|
114
|
+
"vitest": "^4.1.11"
|
|
98
115
|
}
|
|
99
116
|
}
|