hanzio 3.2.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 +2 -1
- package/dist/cli/fields.d.ts +16 -6
- package/dist/cli/index.d.ts +57 -4
- package/dist/crypto/node.d.ts +17 -0
- package/dist/crypto/sealer.d.ts +34 -2
- package/dist/{index-nfvq3zax.js → index-818f7jz4.js} +228 -88
- package/dist/index-zz773050.js +135 -0
- package/dist/redis/concurrency-limit.d.ts +17 -7
- package/dist/src/cli/index.js +3 -1
- package/dist/src/crypto/index.js +15 -106
- package/dist/src/crypto/node.js +67 -0
- package/dist/src/i18n/cli.js +1 -1
- package/dist/src/redis/index.js +5 -1
- package/dist/src/testing/index.js +23 -4
- package/dist/string/index.d.ts +1 -1
- package/dist/testing/module-mocker.d.ts +9 -9
- package/dist/testing/prisma.d.ts +5 -1
- package/package.json +8 -2
- package/src/cli/README.md +63 -6
- package/src/cli/fields.ts +95 -38
- package/src/cli/index.ts +302 -91
- package/src/crypto/README.md +21 -7
- package/src/crypto/node.ts +90 -0
- package/src/crypto/sealer.ts +120 -62
- package/src/redis/README.md +9 -7
- package/src/redis/concurrency-limit.ts +26 -8
- package/src/string/index.ts +1 -1
- package/src/testing/README.md +12 -3
- package/src/testing/module-mocker.ts +40 -10
- package/src/testing/prisma.ts +6 -1
package/src/cli/fields.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
1
|
+
import type { z } from 'zod'
|
|
2
2
|
import { unwrapOnce } from '../zod/introspect'
|
|
3
|
-
import { defOf, type Schema } from '../zod/zodToTypeString'
|
|
3
|
+
import { type Def, defOf, type Schema } from '../zod/zodToTypeString'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// No runtime `zod` import: a CLI made only of raw commands loads without it.
|
|
6
|
+
|
|
7
|
+
/** Thrown for bad argv; `cli.run()` turns it into a `CliUsageError`. */
|
|
8
|
+
export class UsageError extends Error {
|
|
9
|
+
constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
readonly kind:
|
|
12
|
+
| 'missing-command'
|
|
13
|
+
| 'unknown-command'
|
|
14
|
+
| 'invalid-arguments' = 'invalid-arguments'
|
|
15
|
+
) {
|
|
16
|
+
super(message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
/** One key of an input or globals object, read as a flag or a positional. */
|
|
9
21
|
export type Field = {
|
|
@@ -16,11 +28,53 @@ export type Field = {
|
|
|
16
28
|
hasDefault: boolean
|
|
17
29
|
defaultValue: unknown
|
|
18
30
|
description?: string
|
|
31
|
+
/** One-letter short flag. */
|
|
19
32
|
alias?: string
|
|
20
|
-
|
|
33
|
+
/** Extra long flag names, from multi-character aliases. */
|
|
34
|
+
aliases: string[]
|
|
35
|
+
env: string[]
|
|
36
|
+
/** `--name` alone is `true`, `--name=value` is the value. */
|
|
37
|
+
optionalValue: boolean
|
|
21
38
|
choices?: string[]
|
|
22
39
|
}
|
|
23
40
|
|
|
41
|
+
/** The `parseArgs` option name. A `noX` key is its own flag, never `--x` negated. */
|
|
42
|
+
export const optionName = (field: Field) =>
|
|
43
|
+
field.flag.startsWith('no-') ? field.key : field.flag
|
|
44
|
+
|
|
45
|
+
const strings = (value: unknown, what: string, key: string): string[] => {
|
|
46
|
+
const list = value === undefined ? [] : Array.isArray(value) ? value : [value]
|
|
47
|
+
if (list.some((item) => typeof item !== 'string' || !item))
|
|
48
|
+
throw new Error(
|
|
49
|
+
`hanzio/cli: ${what} ${JSON.stringify(value)} of "${key}" must be a string or string[]`
|
|
50
|
+
)
|
|
51
|
+
return list
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const kindOf = (def: Def) =>
|
|
55
|
+
def.type === 'boolean'
|
|
56
|
+
? 'boolean'
|
|
57
|
+
: def.type === 'number'
|
|
58
|
+
? 'number'
|
|
59
|
+
: 'string'
|
|
60
|
+
|
|
61
|
+
function choicesOf(def: Def): string[] | undefined {
|
|
62
|
+
if (def.type === 'enum') return Object.values(def.entries).map(String)
|
|
63
|
+
if (def.type === 'literal') return def.values.map(String)
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// `z.union([z.boolean(), z.string()])` (or `z.literal(true)`): an optional value.
|
|
68
|
+
function optionalValueOf(def: Def): Def | undefined {
|
|
69
|
+
if (def.type !== 'union' || def.options.length !== 2) return undefined
|
|
70
|
+
const [a, b] = def.options.map(defOf) as [Def, Def]
|
|
71
|
+
const flag = (d: Def) =>
|
|
72
|
+
d.type === 'boolean' || (d.type === 'literal' && d.values.includes(true))
|
|
73
|
+
if (flag(a) && kindOf(b) === 'string' && b.type !== 'literal') return b
|
|
74
|
+
if (flag(b) && kindOf(a) === 'string' && a.type !== 'literal') return a
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
24
78
|
const kebab = (key: string) =>
|
|
25
79
|
key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
|
26
80
|
|
|
@@ -37,7 +91,10 @@ function toField(key: string, schema: Schema): Field {
|
|
|
37
91
|
let defaultValue: unknown
|
|
38
92
|
let current = schema
|
|
39
93
|
for (;;) {
|
|
40
|
-
|
|
94
|
+
// `.meta()` with no argument reads Zod's global registry.
|
|
95
|
+
const meta = (
|
|
96
|
+
current as { meta?: () => Record<string, unknown> | undefined }
|
|
97
|
+
).meta?.()
|
|
41
98
|
if (meta) metas.unshift(meta)
|
|
42
99
|
const def = defOf(current)
|
|
43
100
|
if (def.type === 'optional') optional = true
|
|
@@ -61,20 +118,12 @@ function toField(key: string, schema: Schema): Field {
|
|
|
61
118
|
}
|
|
62
119
|
def = defOf(element)
|
|
63
120
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
throw new Error(
|
|
67
|
-
`hanzio/cli: alias ${JSON.stringify(alias)} of "${key}" must be one character`
|
|
68
|
-
)
|
|
121
|
+
const valueDef = multiple ? undefined : optionalValueOf(def)
|
|
122
|
+
const names = strings(meta.alias, 'alias', key)
|
|
69
123
|
return {
|
|
70
124
|
key,
|
|
71
125
|
flag: kebab(key),
|
|
72
|
-
kind:
|
|
73
|
-
def.type === 'boolean'
|
|
74
|
-
? 'boolean'
|
|
75
|
-
: def.type === 'number'
|
|
76
|
-
? 'number'
|
|
77
|
-
: 'string',
|
|
126
|
+
kind: valueDef ? 'string' : kindOf(def),
|
|
78
127
|
multiple,
|
|
79
128
|
positional: meta.positional === true,
|
|
80
129
|
optional,
|
|
@@ -82,14 +131,11 @@ function toField(key: string, schema: Schema): Field {
|
|
|
82
131
|
defaultValue,
|
|
83
132
|
description:
|
|
84
133
|
typeof meta.description === 'string' ? meta.description : undefined,
|
|
85
|
-
alias,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
: def.type === 'literal'
|
|
91
|
-
? def.values.map(String)
|
|
92
|
-
: undefined
|
|
134
|
+
alias: names.find((name) => name.length === 1),
|
|
135
|
+
aliases: names.filter((name) => name.length > 1).map(kebab),
|
|
136
|
+
env: strings(meta.env, 'env', key),
|
|
137
|
+
optionalValue: valueDef !== undefined,
|
|
138
|
+
choices: choicesOf(valueDef ?? def)
|
|
93
139
|
}
|
|
94
140
|
}
|
|
95
141
|
|
|
@@ -108,14 +154,16 @@ function coerce(field: Field, value: string | boolean): unknown {
|
|
|
108
154
|
|
|
109
155
|
/**
|
|
110
156
|
* Builds the raw object Zod validates: flag values and positionals by key,
|
|
111
|
-
* numbers coerced, `.meta({ env })` as fallback
|
|
112
|
-
* boolean or array read as `false` or `[]`.
|
|
157
|
+
* numbers coerced, `.meta({ env })` as fallback (blank variables count as
|
|
158
|
+
* unset), and a missing required boolean or array read as `false` or `[]`.
|
|
159
|
+
* Extra positionals are a usage error unless `strict` is false.
|
|
113
160
|
*/
|
|
114
161
|
export function readFields(
|
|
115
162
|
fields: Field[],
|
|
116
163
|
values: Record<string, string | boolean | (string | boolean)[] | undefined>,
|
|
117
164
|
positionals: string[],
|
|
118
|
-
env: Record<string, string | undefined
|
|
165
|
+
env: Record<string, string | undefined>,
|
|
166
|
+
strict = true
|
|
119
167
|
): Record<string, unknown> {
|
|
120
168
|
const result: Record<string, unknown> = {}
|
|
121
169
|
const queue = [...positionals]
|
|
@@ -127,11 +175,13 @@ export function readFields(
|
|
|
127
175
|
? queue.splice(0)
|
|
128
176
|
: undefined
|
|
129
177
|
: queue.shift()
|
|
130
|
-
else raw = values[field
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
raw
|
|
134
|
-
|
|
178
|
+
else raw = values[optionName(field)]
|
|
179
|
+
if (field.optionalValue && raw === '') raw = true
|
|
180
|
+
const value =
|
|
181
|
+
raw === undefined
|
|
182
|
+
? field.env.map((name) => env[name]).find((value) => value?.trim())
|
|
183
|
+
: undefined
|
|
184
|
+
if (value !== undefined) raw = field.multiple ? [value] : value
|
|
135
185
|
if (raw === undefined) {
|
|
136
186
|
if (field.optional || field.hasDefault) continue
|
|
137
187
|
if (field.multiple) raw = []
|
|
@@ -142,7 +192,8 @@ export function readFields(
|
|
|
142
192
|
? raw.map((item) => coerce(field, item))
|
|
143
193
|
: coerce(field, raw)
|
|
144
194
|
}
|
|
145
|
-
if (queue.length)
|
|
195
|
+
if (strict && queue.length)
|
|
196
|
+
throw new UsageError(`Unexpected argument "${queue[0]}"`)
|
|
146
197
|
return result
|
|
147
198
|
}
|
|
148
199
|
|
|
@@ -154,14 +205,20 @@ export function fieldLabel(field: Field): string {
|
|
|
154
205
|
? `[${field.flag}]`
|
|
155
206
|
: `<${field.flag}>`
|
|
156
207
|
const name =
|
|
157
|
-
field.kind === 'boolean' &&
|
|
208
|
+
field.kind === 'boolean' &&
|
|
209
|
+
field.defaultValue === true &&
|
|
210
|
+
!field.flag.startsWith('no-')
|
|
158
211
|
? `--[no-]${field.flag}`
|
|
159
212
|
: `--${field.flag}`
|
|
213
|
+
const type = `<${field.choices?.join('|') ?? field.kind}>`
|
|
160
214
|
const value =
|
|
161
215
|
field.kind === 'boolean'
|
|
162
216
|
? ''
|
|
163
|
-
:
|
|
164
|
-
|
|
217
|
+
: field.optionalValue
|
|
218
|
+
? `[=${type}]`
|
|
219
|
+
: ` ${type}${field.multiple ? '...' : ''}`
|
|
220
|
+
const aliases = field.aliases.map((alias) => `, --${alias}`).join('')
|
|
221
|
+
return `${field.alias ? `-${field.alias}, ` : ''}${name}${aliases}${value}`
|
|
165
222
|
}
|
|
166
223
|
|
|
167
224
|
export function fieldHelp(field: Field): string {
|
|
@@ -172,7 +229,7 @@ export function fieldHelp(field: Field): string {
|
|
|
172
229
|
return [
|
|
173
230
|
field.description,
|
|
174
231
|
showDefault && `(default: ${JSON.stringify(field.defaultValue)})`,
|
|
175
|
-
field.env && `[env: ${field.env}]`
|
|
232
|
+
field.env.length > 0 && `[env: ${field.env.join(', ')}]`
|
|
176
233
|
]
|
|
177
234
|
.filter(Boolean)
|
|
178
235
|
.join(' ')
|