cli-kiss 0.2.3 → 0.2.5
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 +1 -1
- package/dist/index.d.ts +696 -734
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +2 -3
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/.vitepress/theme/style.css +4 -0
- package/docs/guide/01_getting_started.md +12 -13
- package/docs/guide/02_commands.md +71 -52
- package/docs/guide/03_options.md +25 -33
- package/docs/guide/04_positionals.md +45 -55
- package/docs/guide/05_types.md +66 -66
- package/docs/guide/06_run.md +28 -40
- package/docs/index.md +8 -3
- package/docs/public/favicon.ico +0 -0
- package/docs/public/hero.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +0 -2
- package/src/lib/Command.ts +45 -123
- package/src/lib/Operation.ts +23 -32
- package/src/lib/Option.ts +150 -170
- package/src/lib/Positional.ts +44 -94
- package/src/lib/Reader.ts +123 -99
- package/src/lib/Run.ts +86 -45
- package/src/lib/Type.ts +246 -156
- package/src/lib/Typo.ts +98 -107
- package/src/lib/Usage.ts +163 -82
- package/tests/unit.Reader.aliases.ts +31 -15
- package/tests/unit.Reader.commons.ts +99 -43
- package/tests/unit.Reader.parsings.ts +50 -0
- package/tests/unit.Reader.shortBig.ts +75 -31
- package/tests/unit.command.execute.ts +86 -43
- package/tests/unit.command.usage.ts +88 -82
- package/tests/unit.runner.colors.ts +197 -0
- package/tests/unit.runner.cycle.ts +77 -63
- package/tests/unit.runner.errors.ts +23 -15
package/src/lib/Type.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "fs";
|
|
1
2
|
import {
|
|
2
3
|
TypoError,
|
|
3
4
|
TypoString,
|
|
@@ -8,21 +9,21 @@ import {
|
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Decodes a raw CLI string into a typed value.
|
|
11
|
-
* A pair of a human-readable `content` name
|
|
12
|
+
* A pair of a human-readable `content` name and a `decoder` function.
|
|
12
13
|
*
|
|
13
|
-
* Built-in: {@link
|
|
14
|
-
* {@link typeInteger}, {@link
|
|
15
|
-
* Composite: {@link
|
|
14
|
+
* Built-in: {@link type}, {@link typeBoolean}, {@link typeNumber},
|
|
15
|
+
* {@link typeInteger}, {@link typeDatetime}, {@link typeUrl}.
|
|
16
|
+
* Composite: {@link typeChoice}, {@link typeConverted}, {@link typeTuple}, {@link typeList}.
|
|
16
17
|
*
|
|
17
18
|
* @typeParam Value - Type produced by the decoder.
|
|
18
19
|
*/
|
|
19
20
|
export type Type<Value> = {
|
|
20
21
|
/**
|
|
21
|
-
* Human-readable name shown in help and
|
|
22
|
+
* Human-readable name shown in help and errors (e.g. `"name"`, `"number"`).
|
|
22
23
|
*/
|
|
23
24
|
content: string;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* Decodes a raw CLI string into `Value`.
|
|
26
27
|
*
|
|
27
28
|
* @param input - Raw string from the command line.
|
|
28
29
|
* @returns The decoded value.
|
|
@@ -32,200 +33,211 @@ export type Type<Value> = {
|
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* Decodes
|
|
36
|
-
* Used
|
|
36
|
+
* Decodes a string to `boolean` (case-insensitive).
|
|
37
|
+
* Used by {@link optionFlag} for `--flag=<value>`.
|
|
37
38
|
*
|
|
38
39
|
* @example
|
|
39
40
|
* ```ts
|
|
40
|
-
* typeBoolean.decoder("
|
|
41
|
-
* typeBoolean.decoder("
|
|
42
|
-
* typeBoolean.decoder("
|
|
41
|
+
* typeBoolean("flag").decoder("true") // → true
|
|
42
|
+
* typeBoolean("flag").decoder("yes") // → true
|
|
43
|
+
* typeBoolean("flag").decoder("y") // → true
|
|
44
|
+
* typeBoolean("flag").decoder("false") // → false
|
|
45
|
+
* typeBoolean("flag").decoder("no") // → false
|
|
46
|
+
* typeBoolean("flag").decoder("n") // → false
|
|
43
47
|
* ```
|
|
44
48
|
*/
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
new
|
|
57
|
-
new
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
49
|
+
export function typeBoolean(name?: string): Type<boolean> {
|
|
50
|
+
return {
|
|
51
|
+
content: name ?? "boolean",
|
|
52
|
+
decoder(input: string) {
|
|
53
|
+
const lower = input.toLowerCase();
|
|
54
|
+
if (booleanValuesTrue.has(lower)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (booleanValuesFalse.has(lower)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
throw new TypoError(
|
|
61
|
+
new TypoText(
|
|
62
|
+
new TypoString(`Not a boolean: `),
|
|
63
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const booleanValuesTrue = new Set(["true", "yes", "on", "1", "y", "t"]);
|
|
70
|
+
const booleanValuesFalse = new Set(["false", "no", "off", "0", "n", "f"]);
|
|
63
71
|
|
|
64
72
|
/**
|
|
65
|
-
* Parses a date/time string via `Date.parse
|
|
73
|
+
* Parses a date/time string via `Date.parse`.
|
|
66
74
|
* Accepts any format supported by `Date.parse`, including ISO 8601.
|
|
67
75
|
*
|
|
68
76
|
* @example
|
|
69
77
|
* ```ts
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
78
|
+
* typeDatetime("my-datetime").decoder("2024-01-15") // → Date object for 2024-01-15
|
|
79
|
+
* typeDatetime("my-datetime").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
|
|
80
|
+
* typeDatetime("my-datetime").decoder("not a date") // throws TypoError
|
|
73
81
|
* ```
|
|
74
82
|
*/
|
|
75
|
-
export
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
export function typeDatetime(name?: string): Type<Date> {
|
|
84
|
+
return {
|
|
85
|
+
content: name ?? "datetime",
|
|
86
|
+
decoder(input: string) {
|
|
87
|
+
try {
|
|
88
|
+
const timestampMs = Date.parse(input);
|
|
89
|
+
if (isNaN(timestampMs)) {
|
|
90
|
+
throw new Error();
|
|
91
|
+
}
|
|
92
|
+
return new Date(timestampMs);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new TypoError(
|
|
95
|
+
new TypoText(
|
|
96
|
+
new TypoString(`Not a valid ISO_8601 datetime: `),
|
|
97
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
82
100
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
new TypoText(
|
|
87
|
-
new TypoString(`Not a valid ISO_8601: `),
|
|
88
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
89
|
-
),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
94
104
|
|
|
95
105
|
/**
|
|
96
|
-
* Parses a string
|
|
97
|
-
* Accepts integers, floats, and scientific notation; `NaN` throws a {@link TypoError}.
|
|
106
|
+
* Parses a string to `number` via `Number()`; `NaN` throws {@link TypoError}.
|
|
98
107
|
*
|
|
99
108
|
* @example
|
|
100
109
|
* ```ts
|
|
101
|
-
* typeNumber.decoder("3.14") // → 3.14
|
|
102
|
-
* typeNumber.decoder("-1") // → -1
|
|
103
|
-
* typeNumber.decoder("hello") // throws TypoError
|
|
110
|
+
* typeNumber("my-number").decoder("3.14") // → 3.14
|
|
111
|
+
* typeNumber("my-number").decoder("-1") // → -1
|
|
112
|
+
* typeNumber("my-number").decoder("hello") // throws TypoError
|
|
104
113
|
* ```
|
|
105
114
|
*/
|
|
106
|
-
export
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
export function typeNumber(name?: string): Type<number> {
|
|
116
|
+
return {
|
|
117
|
+
content: name ?? "number",
|
|
118
|
+
decoder(input: string) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = Number(input);
|
|
121
|
+
if (isNaN(parsed)) {
|
|
122
|
+
throw new Error();
|
|
123
|
+
}
|
|
124
|
+
return parsed;
|
|
125
|
+
} catch {
|
|
126
|
+
throw new TypoError(
|
|
127
|
+
new TypoText(
|
|
128
|
+
new TypoString(`Not a number: `),
|
|
129
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
113
132
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
new TypoText(
|
|
118
|
-
new TypoString(`Unable to parse: `),
|
|
119
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
120
|
-
),
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
125
136
|
|
|
126
137
|
/**
|
|
127
|
-
* Parses an integer string
|
|
128
|
-
* Floats and non-numeric strings throw
|
|
138
|
+
* Parses an integer string to `bigint` via `BigInt()`.
|
|
139
|
+
* Floats and non-numeric strings throw {@link TypoError}.
|
|
129
140
|
*
|
|
130
141
|
* @example
|
|
131
142
|
* ```ts
|
|
132
|
-
* typeInteger.decoder("42") // → 42n
|
|
133
|
-
* typeInteger.decoder("3.14") // throws TypoError
|
|
134
|
-
* typeInteger.decoder("abc") // throws TypoError
|
|
143
|
+
* typeInteger("my-integer").decoder("42") // → 42n
|
|
144
|
+
* typeInteger("my-integer").decoder("3.14") // throws TypoError
|
|
145
|
+
* typeInteger("my-integer").decoder("abc") // throws TypoError
|
|
135
146
|
* ```
|
|
136
147
|
*/
|
|
137
|
-
export
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
new
|
|
145
|
-
new
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
148
|
+
export function typeInteger(name?: string): Type<bigint> {
|
|
149
|
+
return {
|
|
150
|
+
content: name ?? "integer",
|
|
151
|
+
decoder(input: string) {
|
|
152
|
+
try {
|
|
153
|
+
return BigInt(input);
|
|
154
|
+
} catch {
|
|
155
|
+
throw new TypoError(
|
|
156
|
+
new TypoText(
|
|
157
|
+
new TypoString(`Not an integer: `),
|
|
158
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
152
165
|
|
|
153
166
|
/**
|
|
154
|
-
* Parses an absolute URL string
|
|
155
|
-
* Relative or malformed URLs throw
|
|
167
|
+
* Parses an absolute URL string to a `URL` object.
|
|
168
|
+
* Relative or malformed URLs throw {@link TypoError}.
|
|
156
169
|
*
|
|
157
170
|
* @example
|
|
158
171
|
* ```ts
|
|
159
|
-
* typeUrl.decoder("https://example.com") // → URL { href: "https://example.com/", ... }
|
|
160
|
-
* typeUrl.decoder("not-a-url") // throws TypoError
|
|
172
|
+
* typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
|
|
173
|
+
* typeUrl("my-url").decoder("not-a-url") // throws TypoError
|
|
161
174
|
* ```
|
|
162
175
|
*/
|
|
163
|
-
export
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
new
|
|
171
|
-
new
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
176
|
+
export function typeUrl(name?: string): Type<URL> {
|
|
177
|
+
return {
|
|
178
|
+
content: name ?? "url",
|
|
179
|
+
decoder(input: string) {
|
|
180
|
+
try {
|
|
181
|
+
return new URL(input);
|
|
182
|
+
} catch {
|
|
183
|
+
throw new TypoError(
|
|
184
|
+
new TypoText(
|
|
185
|
+
new TypoString(`Not an URL: `),
|
|
186
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
178
193
|
|
|
179
194
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
195
|
+
* A named type that accepts any string as input.
|
|
196
|
+
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
182
197
|
* @example
|
|
183
198
|
* ```ts
|
|
184
|
-
*
|
|
185
|
-
*
|
|
199
|
+
* type("greeting").decoder("hello") // → "hello"
|
|
200
|
+
* type("greeting").decoder("") // → ""
|
|
186
201
|
* ```
|
|
187
202
|
*/
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
}
|
|
203
|
+
export function type(name?: string): Type<string> {
|
|
204
|
+
return {
|
|
205
|
+
content: name ?? "string",
|
|
206
|
+
decoder: (input: string) => input,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
194
209
|
|
|
195
210
|
/**
|
|
196
|
-
*
|
|
211
|
+
* Chains `before`'s decoder with an `after` transformation.
|
|
197
212
|
* `before` errors are prefixed with `"from: <content>"` for traceability.
|
|
198
213
|
*
|
|
199
214
|
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
200
215
|
* @typeParam After - Final type from `after.decoder`.
|
|
201
216
|
*
|
|
202
|
-
* @param
|
|
203
|
-
* @param
|
|
204
|
-
* @param
|
|
205
|
-
* @param after.decoder - Converts a `Before` value to `After`.
|
|
217
|
+
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
218
|
+
* @param before - Base type to decode the raw string.
|
|
219
|
+
* @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
|
|
206
220
|
* @returns A {@link Type}`<After>`.
|
|
207
221
|
*
|
|
208
222
|
* @example
|
|
209
223
|
* ```ts
|
|
210
|
-
* const typePort =
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* if (n < 1 || n > 65535) throw new Error("Out of range");
|
|
214
|
-
* return n;
|
|
215
|
-
* },
|
|
224
|
+
* const typePort = typeConverted("port", typeNumber(), (n) => {
|
|
225
|
+
* if (n < 1 || n > 65535) throw new Error("Out of range");
|
|
226
|
+
* return n;
|
|
216
227
|
* });
|
|
217
228
|
* // "--port 8080" → 8080
|
|
218
229
|
* // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
|
|
219
230
|
* ```
|
|
220
231
|
*/
|
|
221
|
-
export function
|
|
232
|
+
export function typeConverted<Before, After>(
|
|
233
|
+
name: string,
|
|
222
234
|
before: Type<Before>,
|
|
223
|
-
|
|
235
|
+
mapper: (value: Before) => After,
|
|
224
236
|
): Type<After> {
|
|
225
237
|
return {
|
|
226
|
-
content:
|
|
238
|
+
content: name,
|
|
227
239
|
decoder: (input: string) => {
|
|
228
|
-
return
|
|
240
|
+
return mapper(
|
|
229
241
|
TypoError.tryWithContext(
|
|
230
242
|
() => before.decoder(input),
|
|
231
243
|
() =>
|
|
@@ -240,31 +252,109 @@ export function typeMapped<Before, After>(
|
|
|
240
252
|
}
|
|
241
253
|
|
|
242
254
|
/**
|
|
243
|
-
*
|
|
244
|
-
* Out-of-set inputs throw a {@link TypoError} listing up to 5 valid options.
|
|
255
|
+
* Adds a name to a {@link Type} for clearer error messages and help text.
|
|
245
256
|
*
|
|
246
|
-
* @param
|
|
257
|
+
* @param name - Name to use for the type.
|
|
258
|
+
* @param type - Base type to name.
|
|
259
|
+
* @returns A {@link Type} with the given name.
|
|
260
|
+
*/
|
|
261
|
+
export function typeRenamed<Value>(
|
|
262
|
+
type: Type<Value>,
|
|
263
|
+
name: string,
|
|
264
|
+
): Type<Value> {
|
|
265
|
+
return {
|
|
266
|
+
content: name,
|
|
267
|
+
decoder: (input: string) => {
|
|
268
|
+
return TypoError.tryWithContext(
|
|
269
|
+
() => type.decoder(input),
|
|
270
|
+
() =>
|
|
271
|
+
new TypoText(
|
|
272
|
+
new TypoString("from: "),
|
|
273
|
+
new TypoString(type.content, typoStyleLogic),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Creates a {@link Type} for filesystem paths with optional existence checks.
|
|
282
|
+
* @param checks - Optional checks for path existence and type (file/directory).
|
|
283
|
+
* @returns A {@link Type}`<string>` representing the path.
|
|
284
|
+
*/
|
|
285
|
+
export function typePath(
|
|
286
|
+
name?: string,
|
|
287
|
+
checks?: { checkSyncExistAs?: "file" | "directory" | "anything" },
|
|
288
|
+
): Type<string> {
|
|
289
|
+
return {
|
|
290
|
+
content: name ?? "path",
|
|
291
|
+
decoder(input: string) {
|
|
292
|
+
if (input.length === 0) {
|
|
293
|
+
throw new Error(`Path cannot be empty`);
|
|
294
|
+
}
|
|
295
|
+
if (input.includes("\0")) {
|
|
296
|
+
throw new Error(`Path cannot contain null characters`);
|
|
297
|
+
}
|
|
298
|
+
if (checks?.checkSyncExistAs !== undefined) {
|
|
299
|
+
const stats = statSync(input);
|
|
300
|
+
const preview = stats.isDirectory()
|
|
301
|
+
? "directory"
|
|
302
|
+
: stats.isFile()
|
|
303
|
+
? "file"
|
|
304
|
+
: "unknown";
|
|
305
|
+
if (checks.checkSyncExistAs === "file" && !stats.isFile()) {
|
|
306
|
+
throw new TypoError(
|
|
307
|
+
new TypoText(
|
|
308
|
+
new TypoString(`Expected a 'file' but found '${preview}': `),
|
|
309
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (checks.checkSyncExistAs === "directory" && !stats.isDirectory()) {
|
|
314
|
+
throw new TypoError(
|
|
315
|
+
new TypoText(
|
|
316
|
+
new TypoString(`Expected a 'directory' but found '${preview}': `),
|
|
317
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return input;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Creates a {@link Type}`<string>` that only accepts a fixed set of values.
|
|
329
|
+
* Out-of-set inputs throw {@link TypoError} listing up to 5 valid options.
|
|
330
|
+
*
|
|
331
|
+
* @param name - Name shown in help and errors.
|
|
247
332
|
* @param values - Ordered list of accepted values.
|
|
248
333
|
* @returns A {@link Type}`<string>`.
|
|
249
334
|
*
|
|
250
335
|
* @example
|
|
251
336
|
* ```ts
|
|
252
|
-
* const typeEnv =
|
|
337
|
+
* const typeEnv = typeChoice("environment", ["dev", "staging", "prod"]);
|
|
253
338
|
* typeEnv.decoder("prod") // → "prod"
|
|
254
339
|
* typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
|
|
255
340
|
* ```
|
|
256
341
|
*/
|
|
257
|
-
export function
|
|
258
|
-
|
|
342
|
+
export function typeChoice<const Value extends string>(
|
|
343
|
+
name: string,
|
|
259
344
|
values: Array<Value>,
|
|
345
|
+
caseSensitive: boolean = false,
|
|
260
346
|
): Type<Value> {
|
|
347
|
+
const normalize = caseSensitive
|
|
348
|
+
? (s: string) => s
|
|
349
|
+
: (s: string) => s.toLowerCase();
|
|
350
|
+
const valueMap = new Map(values.map((value) => [normalize(value), value]));
|
|
261
351
|
return {
|
|
262
|
-
content:
|
|
352
|
+
content: name,
|
|
263
353
|
decoder(input: string) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
354
|
+
const normalized = normalize(input);
|
|
355
|
+
const original = valueMap.get(normalized);
|
|
356
|
+
if (original !== undefined) {
|
|
357
|
+
return original;
|
|
268
358
|
}
|
|
269
359
|
const valuesPreview = [];
|
|
270
360
|
for (const value of values) {
|
|
@@ -291,7 +381,7 @@ export function typeOneOf<const Value extends string>(
|
|
|
291
381
|
}
|
|
292
382
|
|
|
293
383
|
/**
|
|
294
|
-
* Splits a delimited string into a
|
|
384
|
+
* Splits a delimited string into a typed tuple.
|
|
295
385
|
* Each part is decoded by the corresponding element type; wrong count or decode failure throws {@link TypoError}.
|
|
296
386
|
*
|
|
297
387
|
* @typeParam Elements - Tuple of decoded value types (inferred from `elementTypes`).
|
|
@@ -302,7 +392,7 @@ export function typeOneOf<const Value extends string>(
|
|
|
302
392
|
*
|
|
303
393
|
* @example
|
|
304
394
|
* ```ts
|
|
305
|
-
* const typePoint = typeTuple([typeNumber, typeNumber]);
|
|
395
|
+
* const typePoint = typeTuple([typeNumber("x"), typeNumber("y")]);
|
|
306
396
|
* typePoint.decoder("3.14,2.71") // → [3.14, 2.71]
|
|
307
397
|
* typePoint.decoder("1,2,3") // → [1, 2]
|
|
308
398
|
* typePoint.decoder("x,2") // throws TypoError: at 0: Number: Unable to parse: "x"
|
|
@@ -316,14 +406,14 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
316
406
|
content: elementTypes
|
|
317
407
|
.map((elementType) => elementType.content)
|
|
318
408
|
.join(separator),
|
|
319
|
-
decoder(
|
|
320
|
-
const splits =
|
|
409
|
+
decoder(input: string) {
|
|
410
|
+
const splits = input.split(separator, elementTypes.length);
|
|
321
411
|
if (splits.length !== elementTypes.length) {
|
|
322
412
|
throw new TypoError(
|
|
323
413
|
new TypoText(
|
|
324
414
|
new TypoString(`Found ${splits.length} splits: `),
|
|
325
415
|
new TypoString(`Expected ${elementTypes.length} splits from: `),
|
|
326
|
-
new TypoString(`"${
|
|
416
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
327
417
|
),
|
|
328
418
|
);
|
|
329
419
|
}
|
|
@@ -343,7 +433,7 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
343
433
|
}
|
|
344
434
|
|
|
345
435
|
/**
|
|
346
|
-
* Splits a delimited string into a
|
|
436
|
+
* Splits a delimited string into a typed array.
|
|
347
437
|
* Each part is decoded by `elementType`; failed decodes throw {@link TypoError}.
|
|
348
438
|
* Note: splitting an empty string yields one empty element — prefer {@link optionRepeatable} for a zero-default.
|
|
349
439
|
*
|
|
@@ -359,7 +449,7 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
359
449
|
* typeNumbers.decoder("1,2,3") // → [1, 2, 3]
|
|
360
450
|
* typeNumbers.decoder("1,x,3") // throws TypoError: at 1: Number: Unable to parse: "x"
|
|
361
451
|
*
|
|
362
|
-
* const typePaths = typeList(
|
|
452
|
+
* const typePaths = typeList(typePath(), ":");
|
|
363
453
|
* typePaths.decoder("/usr/bin:/usr/local/bin") // → ["/usr/bin", "/usr/local/bin"]
|
|
364
454
|
* ```
|
|
365
455
|
*/
|