@mirta/staged-args 0.0.1 → 0.4.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/LICENSE +24 -0
- package/README.md +247 -27
- package/README.ru.md +272 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +388 -0
- package/package.json +49 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
CHANGED
|
@@ -1,45 +1,265 @@
|
|
|
1
1
|
# @mirta/staged-args
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/wb-mirta/core/blob/latest/packages/mirta-staged-args/README.md)
|
|
4
|
+
[](https://github.com/wb-mirta/core/blob/latest/packages/mirta-staged-args/README.ru.md)
|
|
5
|
+
[](https://npmjs.com/package/@mirta/staged-args)
|
|
6
|
+
[](https://npmjs.com/package/@mirta/staged-args)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
> A staged command-line argument parser for building complex, multi-level CLI tools with support for global flags, safe error handling, and smart suggestions.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
`@mirta/staged-args` is a **minimal, type-safe utility** built on `parseArgs` from `node:util` that enables parsing command-line arguments in multiple stages. It's ideal for frameworks, generators, and orchestrators that require:
|
|
11
|
+
- Processing global flags first (e.g. `--config`, `--verbose`),
|
|
12
|
+
- Then parsing command-specific options,
|
|
13
|
+
- Without aborting execution on input errors,
|
|
14
|
+
- And supporting localized error messages.
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
The `@mirta/staged-args` package is intended **exclusively for Node.js tools** (≥ 20.10.0) and is not used in Duktape runtime.
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@mirta/staged-args`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
18
|
+
## 📦 Installation
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
```sh
|
|
21
|
+
pnpm add @mirta-staged-args
|
|
22
|
+
```
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
## 🚀 Quick Start
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
Create a parser instance with command-line arguments:
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
```ts
|
|
29
|
+
import { createStagedArgs } from '@mirta/staged-args'
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
31
|
+
const staged = createStagedArgs(process.argv.slice(2))
|
|
32
|
+
```
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
Define an option schema:
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
```ts
|
|
37
|
+
const schema = {
|
|
38
|
+
config: { type: 'string', default: 'mirta.json' },
|
|
39
|
+
verbose: { type: 'boolean' },
|
|
40
|
+
} as const
|
|
41
|
+
```
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
Perform staged parsing:
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
```ts
|
|
46
|
+
const result = staged.parse(schema)
|
|
47
|
+
|
|
48
|
+
if (result.hasErrors) {
|
|
49
|
+
// Handle errors
|
|
50
|
+
result.errors.forEach(error => {
|
|
51
|
+
console.error(`Error: ${error.type}, option: ${error.option}`)
|
|
52
|
+
})
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { data: globals } = result
|
|
57
|
+
console.log('Global flags:', globals)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Positional arguments (e.g. command and its parameters) are available via `positionals`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const { positionals } = result.data
|
|
64
|
+
// → ['deploy', 'staging']
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> ⚠️ **Unknown options** (e.g. `--force`) **do not become positional** — they remain as options and can be caught as `unknown-option` errors when using `parseFinal`.
|
|
68
|
+
|
|
69
|
+
To continue parsing, use `stagedArgs`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const { stagedArgs } = result.data
|
|
73
|
+
const commandResult = stagedArgs.parseFinal(commandSchema)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 🔍 Architecture
|
|
77
|
+
|
|
78
|
+
### Staged Parsing
|
|
79
|
+
|
|
80
|
+
`@mirta/staged-args` enables:
|
|
81
|
+
- Splitting argument parsing into multiple stages.
|
|
82
|
+
- First processing configuration-affecting flags.
|
|
83
|
+
- Then parsing command-specific options based on loaded configuration.
|
|
84
|
+
|
|
85
|
+
This is critical for tools like `@mirta/cli`, `create-mirta`, or `nx`, where `--config` must be processed **before** command selection.
|
|
86
|
+
|
|
87
|
+
> ⚠️ The parser tracks **which positional arguments have already been used as option values** (e.g. `--port 3000`) and marks their indices to prevent reuse.
|
|
88
|
+
> However, **the options themselves (e.g. `--port`) are not "consumed"** — they can be processed again in later stages if included in the schema.
|
|
89
|
+
> This ensures that a value like `deploy` won’t be mistakenly used as a value for `--port` in a subsequent stage.
|
|
90
|
+
|
|
91
|
+
### Safe Result: `Result<T, E>`
|
|
92
|
+
|
|
93
|
+
The `parse` and `parseFinal` methods return:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
type Result<TData, TError>
|
|
97
|
+
= | { hasErrors: false, data: TData }
|
|
98
|
+
| { hasErrors: true, errors: TError[] }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
➡️ Until you check `hasErrors`, the `data` field is **inaccessible in the type system**.
|
|
102
|
+
➡️ This **enforces explicit error handling** and prevents misuse of invalid data.
|
|
103
|
+
|
|
104
|
+
#### Why this matters
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
if (result.hasErrors) {
|
|
108
|
+
// ❌ TypeScript won't allow access to result.data
|
|
109
|
+
console.log(result.data.values) // → Compile-time error
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ✅ Only after checking
|
|
113
|
+
if (!result.hasErrors) {
|
|
114
|
+
console.log(result.data.values) // → OK
|
|
115
|
+
console.log(result.data.positionals) // → OK
|
|
116
|
+
console.log(result.data.stagedArgs) // → OK — safe to continue
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This approach:
|
|
121
|
+
- Ensures you don’t use parsing data when errors are present.
|
|
122
|
+
- Makes `stagedArgs` available **only on successful parsing**.
|
|
123
|
+
- Works seamlessly with localization systems like `@mirta/i18n`.
|
|
124
|
+
|
|
125
|
+
### Flexible Suggestions: `suggest?: SuggestFunc`
|
|
126
|
+
|
|
127
|
+
You can provide a suggestion function:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const args = createStagedArgs(process.argv.slice(2), {
|
|
131
|
+
suggest: (unknown, known) => {
|
|
132
|
+
return known.includes('config') ? 'config' : undefined
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or use `suggestClosest` from `@mirta/basics/fuzzy`:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { suggestClosest } from '@mirta/basics/fuzzy'
|
|
141
|
+
|
|
142
|
+
const staged = createStagedArgs(process.argv.slice(2), {
|
|
143
|
+
suggest: suggestClosest,
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This is **not a required dependency** — you choose the strategy.
|
|
148
|
+
|
|
149
|
+
### Runtime vs Development Errors
|
|
150
|
+
|
|
151
|
+
- `ParseError` — returned in `Result`:
|
|
152
|
+
```ts
|
|
153
|
+
{ type: 'unknown-option', option: '--confog', suggestion: 'config' }
|
|
154
|
+
```
|
|
155
|
+
Localizable, does not terminate execution.
|
|
156
|
+
|
|
157
|
+
- `SchemaError` — thrown as exception:
|
|
158
|
+
For example, on duplicate option names.
|
|
159
|
+
This is a **development-time error**, not exposed to end users.
|
|
160
|
+
|
|
161
|
+
## 🧰 API
|
|
162
|
+
|
|
163
|
+
### `createStagedArgs(args: string[], options?: { suggest?: SuggestFunc }): StagedArgs`
|
|
164
|
+
|
|
165
|
+
Creates a parser instance.
|
|
166
|
+
|
|
167
|
+
#### Parameters:
|
|
168
|
+
- `args` — array of strings (typically `process.argv.slice(2)`).
|
|
169
|
+
- `options.suggest` — function returning a suggested correction for an unknown option.
|
|
170
|
+
|
|
171
|
+
#### Returns:
|
|
172
|
+
An object with `parse` and `parseFinal` methods.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### `parse<TSchema>(schema: TSchema): Result<ParsedArgs<TSchema>, ParseError>`
|
|
177
|
+
|
|
178
|
+
Parses arguments according to the schema.
|
|
179
|
+
|
|
180
|
+
#### Returns:
|
|
181
|
+
`Result<ParsedArgs<TSchema>, ParseError>` where `data` includes:
|
|
182
|
+
- `values` — parsed option values,
|
|
183
|
+
- `positionals` — unprocessed positional arguments,
|
|
184
|
+
- `stagedArgs` — a new parsing stage including the current schema (can continue parsing).
|
|
185
|
+
|
|
186
|
+
> ✅ Use `parse` for **multi-stage** parsing.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### `parseFinal<TSchema>(schema: TSchema): Result<ParsedArgsFinal<TSchema>, ParseError>`
|
|
191
|
+
|
|
192
|
+
Similar to `parse`, but considered a **final stage**:
|
|
193
|
+
- Checks for unknown options → `unknown-option` error.
|
|
194
|
+
- Does not return `stagedArgs` — further parsing is not possible.
|
|
195
|
+
|
|
196
|
+
#### Returns:
|
|
197
|
+
`Result<ParsedArgsFinal<TSchema>, ParseError>` where `data` includes:
|
|
198
|
+
- `values` — parsed values,
|
|
199
|
+
- `positionals` — positional arguments.
|
|
200
|
+
|
|
201
|
+
> ⚠️ Use `parseFinal` for commands or final validation.
|
|
42
202
|
|
|
43
203
|
---
|
|
44
204
|
|
|
45
|
-
|
|
205
|
+
### `type ParseError`
|
|
206
|
+
|
|
207
|
+
Supported error types:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
| { type: 'unknown-option', option: string, suggestion?: string }
|
|
211
|
+
| { type: 'missing-value', option: string }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## ✅ Example: Multi-Stage CLI
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const staged = createStagedArgs(process.argv.slice(2), { suggest: suggestClosest })
|
|
218
|
+
|
|
219
|
+
// Stage 1: global flags
|
|
220
|
+
const globalSchema = { config: { type: 'string' }, verbose: { type: 'boolean' } } as const
|
|
221
|
+
const globalResult = staged.parse(globalSchema)
|
|
222
|
+
|
|
223
|
+
if (globalResult.hasErrors) {
|
|
224
|
+
// Show localized messages
|
|
225
|
+
logErrors(globalResult.errors)
|
|
226
|
+
process.exit(1)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ✅ data is available — no errors
|
|
230
|
+
const { positionals, stagedArgs } = globalResult.data
|
|
231
|
+
|
|
232
|
+
// Load config based on --config
|
|
233
|
+
const config = loadConfig(globalResult.data.values.config)
|
|
234
|
+
|
|
235
|
+
// Stage 2: command and its positional params
|
|
236
|
+
const command = positionals[0]
|
|
237
|
+
if (!command) {
|
|
238
|
+
console.error('Command not specified')
|
|
239
|
+
process.exit(1)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const commandSchema = config.commands[command]
|
|
243
|
+
if (!commandSchema) {
|
|
244
|
+
console.error(`Unknown command: ${command}`)
|
|
245
|
+
process.exit(1)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Continue parsing: options like --verbose remain available
|
|
249
|
+
const commandResult = stagedArgs.parseFinal(commandSchema)
|
|
250
|
+
|
|
251
|
+
if (commandResult.hasErrors) {
|
|
252
|
+
logErrors(commandResult.errors)
|
|
253
|
+
process.exit(1)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Execute
|
|
257
|
+
run(command, globalResult.data.values, commandResult.data.values)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## 🛠 Internal Architecture
|
|
261
|
+
|
|
262
|
+
- **Modular**: each component is a separate file.
|
|
263
|
+
- **No dependencies**: only `node:util`.
|
|
264
|
+
- **ESM-first**: supports `#src/*` via `imports`.
|
|
265
|
+
- **TypeScript**: full typing, including `Values<TSchema>` inference.
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# @mirta/staged-args
|
|
2
|
+
|
|
3
|
+
[](https://github.com/wb-mirta/core/blob/latest/packages/mirta-staged-args/README.md)
|
|
4
|
+
[](https://github.com/wb-mirta/core/blob/latest/packages/mirta-staged-args/README.ru.md)
|
|
5
|
+
[](https://npmjs.com/package/@mirta/staged-args)
|
|
6
|
+
[](https://npmjs.com/package/@mirta/staged-args)
|
|
7
|
+
|
|
8
|
+
> Утилита поэтапного разбора аргументов командной строки. Предназначена для создания сложных, многоуровневых CLI-инструментов с поддержкой глобальных флагов, безопасной обработки ошибок и гибких подсказок.
|
|
9
|
+
|
|
10
|
+
`@mirta/staged-args` — это **минималистичный, типобезопасный инструмент** на основе `parseArgs` из `node:util` для разбора аргументов в несколько этапов. Подходит для фреймворков, генераторов и оркестраторов, где требуется:
|
|
11
|
+
- Сначала обработать глобальные флаги (`--config`, `--verbose`),
|
|
12
|
+
- Затем — командные опции,
|
|
13
|
+
- При этом не прерывать выполнение при ошибках ввода,
|
|
14
|
+
- Иметь возможность локализации сообщений.
|
|
15
|
+
|
|
16
|
+
Пакет `@mirta/staged-args` предназначен исключительно для **Node.js-инструментов** (≥ 20.10.0), не используется в рантайме Duktape.
|
|
17
|
+
|
|
18
|
+
## 📦 Установка
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pnpm add @mirta/staged-args
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 🚀 Быстрый старт
|
|
25
|
+
|
|
26
|
+
Создайте экземпляр парсера с аргументами командной строки:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createStagedArgs } from '@mirta/staged-args'
|
|
30
|
+
|
|
31
|
+
const staged = createStagedArgs(process.argv.slice(2))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Определите схему опций:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const schema = {
|
|
38
|
+
config: { type: 'string', default: 'mirta.json' },
|
|
39
|
+
verbose: { type: 'boolean' },
|
|
40
|
+
} as const
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Выполните поэтапный разбор:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const result = staged.parse(schema)
|
|
47
|
+
|
|
48
|
+
if (result.hasErrors) {
|
|
49
|
+
// Обработка ошибок
|
|
50
|
+
result.errors.forEach(error => {
|
|
51
|
+
console.error(`Ошибка: ${error.type}, опция: ${error.option}`)
|
|
52
|
+
})
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { data: globals } = result
|
|
57
|
+
console.log('Глобальные флаги:', globals)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Позиционные аргументы (например, команда и её параметры) доступны через `positionals`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const { positionals } = result.data
|
|
64
|
+
// → ['deploy', 'staging']
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> ⚠️ **Неизвестные опции** (например, `--force`) **не попадают в `positionals`** — они остаются опциями и могут быть отловлены как `unknown-option` при использовании `parseFinal`.
|
|
68
|
+
|
|
69
|
+
Для продолжения разбора используйте `stagedArgs`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const { stagedArgs } = result.data
|
|
73
|
+
const commandResult = stagedArgs.parseFinal(commandSchema)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 🔍 Архитектура
|
|
77
|
+
|
|
78
|
+
### Поэтапный парсинг (Staged Parsing)
|
|
79
|
+
|
|
80
|
+
`@mirta/staged-args` позволяет:
|
|
81
|
+
- Разделить разбор аргументов на этапы.
|
|
82
|
+
- Сначала обработать флаги, влияющие на конфигурацию.
|
|
83
|
+
- Позже — разобрать команду и её опции, основываясь на загруженных данных.
|
|
84
|
+
|
|
85
|
+
Это критично для инструментов вроде `@mirta/cli`, `create-mirta`, `nx`, где `--config` должен быть обработан **до** выбора команды.
|
|
86
|
+
|
|
87
|
+
> ⚠️ Парсер отслеживает, **какие позиционные аргументы уже использовались как значения** (например, `--port 3000`), и помечает их индексы, чтобы избежать повторного присваивания.
|
|
88
|
+
> Однако **сами опции (например, `--port`) не "исчезают"** — они могут быть обработаны снова на следующих этапах, если указаны в схеме.
|
|
89
|
+
> Это гарантирует, что значение `deploy` не будет ошибочно использовано как значение для `--port` на втором этапе.
|
|
90
|
+
|
|
91
|
+
### Безопасный результат: `Result<T, E>`
|
|
92
|
+
|
|
93
|
+
Функции `parse` и `parseFinal` возвращают:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
type Result<TData, TError>
|
|
97
|
+
= | { hasErrors: false, data: TData }
|
|
98
|
+
| { hasErrors: true, errors: TError[] }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
➡️ Пока вы не проверите `hasErrors`, **поле `data` недоступно в типовой системе**.
|
|
102
|
+
➡️ Это **принуждает к явной обработке ошибок** и предотвращает использование некорректных данных.
|
|
103
|
+
|
|
104
|
+
#### Почему это важно
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
if (result.hasErrors) {
|
|
108
|
+
// ❌ TypeScript не позволит обратиться к result.data
|
|
109
|
+
console.log(result.data.values) // → Ошибка компиляции
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ✅ Только после проверки
|
|
113
|
+
if (!result.hasErrors) {
|
|
114
|
+
console.log(result.data.values) // → OK
|
|
115
|
+
console.log(result.data.positionals) // → OK
|
|
116
|
+
console.log(result.data.stagedArgs) // → OK — можно продолжить разбор
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Такой подход:
|
|
121
|
+
- Гарантирует, что вы не используете данные при наличии ошибок.
|
|
122
|
+
- Делает `stagedArgs` доступным **только при успешном разборе**.
|
|
123
|
+
- Совместим с системами локализации, такими как `@mirta/i18n`.
|
|
124
|
+
|
|
125
|
+
### Гибкие подсказки: `suggest?: SuggestFunc`
|
|
126
|
+
|
|
127
|
+
Можно передать функцию подсказки:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const args = createStagedArgs(process.argv.slice(2), {
|
|
131
|
+
suggest: (unknown, known) => {
|
|
132
|
+
return known.includes('config') ? 'config' : undefined
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Или использовать `suggestClosest` из `@mirta/basics/fuzzy`:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { suggestClosest } from '@mirta/basics/fuzzy'
|
|
141
|
+
|
|
142
|
+
const staged = createStagedArgs(process.argv.slice(2), {
|
|
143
|
+
suggest: suggestClosest,
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Это **не обязательная зависимость** — вы решаете, какую стратегию использовать.
|
|
148
|
+
|
|
149
|
+
### Ошибки времени выполнения и разработки
|
|
150
|
+
|
|
151
|
+
- `ParseError` — возвращается в `Result`:
|
|
152
|
+
```ts
|
|
153
|
+
{ type: 'unknown-option', option: '--confog', suggestion: 'config' }
|
|
154
|
+
```
|
|
155
|
+
Подлежит локализации, не прерывает выполнение.
|
|
156
|
+
|
|
157
|
+
- `SchemaError` — выбрасывается в виде исключения:
|
|
158
|
+
Например, при дублировании имён опций.
|
|
159
|
+
Это **ошибка разработки**, не может возникнуть у конечного пользователя.
|
|
160
|
+
|
|
161
|
+
## 🧰 API
|
|
162
|
+
|
|
163
|
+
### `createStagedArgs(args: string[], options?: { suggest?: SuggestFunc }): StagedArgs`
|
|
164
|
+
|
|
165
|
+
Создаёт экземпляр парсера.
|
|
166
|
+
|
|
167
|
+
#### Параметры:
|
|
168
|
+
- `args` — массив строк (обычно `process.argv.slice(2)`).
|
|
169
|
+
- `options.suggest` — функция, возвращающая возможную коррекцию для неизвестной опции.
|
|
170
|
+
|
|
171
|
+
#### Возвращает:
|
|
172
|
+
Объект с методами `parse`, `parseFinal`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### `parse<TSchema>(schema: TSchema): Result<Values<TSchema>, ParseError>`
|
|
177
|
+
|
|
178
|
+
Разбирает аргументы по схеме.
|
|
179
|
+
Сохраняет состояние: какие опции и значения уже обработаны.
|
|
180
|
+
|
|
181
|
+
#### Возвращает:
|
|
182
|
+
`Result<ParsedArgs<TSchema>, ParseError>`, где поле `data` содержит:
|
|
183
|
+
- `values` — распарсенные значения опций,
|
|
184
|
+
- `positionals` — необработанные позиционные аргументы,
|
|
185
|
+
- `stagedArgs` — новый этап парсинга, включающий текущую схему (можно продолжать разбор).
|
|
186
|
+
|
|
187
|
+
> ✅ Используйте `parse` для **многоэтапного** разбора.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### `parseFinal<TSchema>(schema: TSchema): Result<Values<TSchema>, ParseError>`
|
|
192
|
+
|
|
193
|
+
Аналог `parse`, но считается **финальным этапом**:
|
|
194
|
+
- Проверяет наличие неизвестных опций → ошибка `unknown-option`.
|
|
195
|
+
- Не возвращает `stagedArgs` — дальнейший парсинг невозможен.
|
|
196
|
+
|
|
197
|
+
#### Возвращает:
|
|
198
|
+
`Result<ParsedArgsFinal<TSchema>, ParseError>`, где поле `data` содержит:
|
|
199
|
+
- `values` — распарсенные значения,
|
|
200
|
+
- `positionals` — позиционные аргументы.
|
|
201
|
+
|
|
202
|
+
> ⚠️ Используйте `parseFinal` для команд или финальной валидации.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### `getRemainingArgs(): string[]`
|
|
207
|
+
|
|
208
|
+
Возвращает необработанные аргументы для передачи следующему этапу.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `type ParseError`
|
|
213
|
+
|
|
214
|
+
Поддерживаемые типы ошибок:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
| { type: 'unknown-option', option: string, suggestion?: string }
|
|
218
|
+
| { type: 'missing-value', option: string }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## ✅ Пример: многоэтапный CLI
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const staged = createStagedArgs(process.argv.slice(2), { suggest: suggestClosest })
|
|
225
|
+
|
|
226
|
+
// Этап 1: глобальные флаги
|
|
227
|
+
const globalSchema = { config: { type: 'string' }, verbose: { type: 'boolean' } } as const
|
|
228
|
+
const globalResult = staged.parse(globalSchema)
|
|
229
|
+
|
|
230
|
+
if (globalResult.hasErrors) {
|
|
231
|
+
// Показываем локализованные сообщения
|
|
232
|
+
logErrors(globalResult.errors)
|
|
233
|
+
process.exit(1)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ✅ data доступно — ошибок нет
|
|
237
|
+
const { positionals, stagedArgs } = globalResult.data
|
|
238
|
+
|
|
239
|
+
// Загружаем конфиг на основе --config
|
|
240
|
+
const config = loadConfig(globalResult.data.values.config)
|
|
241
|
+
|
|
242
|
+
// Этап 2: команда и её позиционные параметры
|
|
243
|
+
const command = positionals[0]
|
|
244
|
+
if (!command) {
|
|
245
|
+
console.error('Команда не указана')
|
|
246
|
+
process.exit(1)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const commandSchema = config.commands[command]
|
|
250
|
+
if (!commandSchema) {
|
|
251
|
+
console.error(`Неизвестная команда: ${command}`)
|
|
252
|
+
process.exit(1)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Продолжаем разбор: опции вроде --verbose остаются доступными
|
|
256
|
+
const commandResult = stagedArgs.parseFinal(commandSchema)
|
|
257
|
+
|
|
258
|
+
if (commandResult.hasErrors) {
|
|
259
|
+
logErrors(commandResult.errors)
|
|
260
|
+
process.exit(1)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Выполняем
|
|
264
|
+
run(command, globalResult.data.values, commandResult.data.values)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## 🛠 Внутренняя архитектура
|
|
268
|
+
|
|
269
|
+
- **Модульность**: каждый компонент — отдельный файл.
|
|
270
|
+
- **Без зависимостей**: только `node:util`.
|
|
271
|
+
- **ESM-first**: поддержка `#src/*` через `imports`.
|
|
272
|
+
- **TypeScript**: полная типизация, включая вывод `Values<TSchema>`.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Контейнер результата операции.
|
|
3
|
+
*
|
|
4
|
+
* @template TData - Тип данных при успешном результате.
|
|
5
|
+
* @template TError - Тип ошибки при неудачном результате.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.4.0
|
|
8
|
+
*
|
|
9
|
+
**/
|
|
10
|
+
type Result<TData, TError> = {
|
|
11
|
+
hasErrors: false;
|
|
12
|
+
data: TData;
|
|
13
|
+
} | {
|
|
14
|
+
hasErrors: true;
|
|
15
|
+
errors: TError[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Описание типа опции в схеме.
|
|
20
|
+
*
|
|
21
|
+
**/
|
|
22
|
+
type Option = {
|
|
23
|
+
type: 'boolean';
|
|
24
|
+
short?: string;
|
|
25
|
+
default?: boolean;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'string';
|
|
28
|
+
short?: string;
|
|
29
|
+
default?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Схема опций командной строки.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Ключ — имя опции (например, `verbose`), значение — её конфигурация.
|
|
36
|
+
*
|
|
37
|
+
* @since 0.4.0
|
|
38
|
+
*
|
|
39
|
+
**/
|
|
40
|
+
type OptionSchema = Record<string, Option>;
|
|
41
|
+
/**
|
|
42
|
+
* Выводит тип значений на основе схемы опций.
|
|
43
|
+
*
|
|
44
|
+
* @typeParam TSchema — Схема опций.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* Если у опции указано значение по умолчанию, оно становится обязательным.
|
|
48
|
+
* Иначе — может быть `undefined`.
|
|
49
|
+
*
|
|
50
|
+
* @since 0.4.0
|
|
51
|
+
*
|
|
52
|
+
**/
|
|
53
|
+
type Values<TSchema extends OptionSchema> = {
|
|
54
|
+
[K in keyof TSchema]: TSchema[K] extends {
|
|
55
|
+
type: 'string';
|
|
56
|
+
} ? TSchema[K] extends {
|
|
57
|
+
default: infer _TDefault extends string;
|
|
58
|
+
} ? string : string | undefined : TSchema[K] extends {
|
|
59
|
+
type: 'boolean';
|
|
60
|
+
} ? TSchema[K] extends {
|
|
61
|
+
default: infer _TDefault extends boolean;
|
|
62
|
+
} ? boolean : boolean | undefined : never;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Результат разбора опций на промежуточной стадии.
|
|
66
|
+
*
|
|
67
|
+
* @typeParam TSchema - Схема опций.
|
|
68
|
+
*
|
|
69
|
+
* @since 0.4.0
|
|
70
|
+
*
|
|
71
|
+
**/
|
|
72
|
+
interface ParsedArgs<TSchema extends OptionSchema> {
|
|
73
|
+
/**
|
|
74
|
+
* Значения опций, включая значения по умолчанию.
|
|
75
|
+
*
|
|
76
|
+
**/
|
|
77
|
+
values: Values<TSchema>;
|
|
78
|
+
/**
|
|
79
|
+
* Позиционные аргументы, не связанные с опциями.
|
|
80
|
+
*
|
|
81
|
+
**/
|
|
82
|
+
positionals: string[];
|
|
83
|
+
/**
|
|
84
|
+
* Новый экземпляр `StagedArgs` для дальнейшего разбора.
|
|
85
|
+
*
|
|
86
|
+
**/
|
|
87
|
+
stagedArgs: StagedArgs;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Результат окончательного разбора опций.
|
|
91
|
+
*
|
|
92
|
+
* @typeParam TSchema - Схема опций.
|
|
93
|
+
*
|
|
94
|
+
**/
|
|
95
|
+
interface ParsedArgsFinal<TSchema extends OptionSchema> {
|
|
96
|
+
/**
|
|
97
|
+
* Значения опций.
|
|
98
|
+
*
|
|
99
|
+
**/
|
|
100
|
+
values: Values<TSchema>;
|
|
101
|
+
/**
|
|
102
|
+
* Позиционные аргументы.
|
|
103
|
+
*
|
|
104
|
+
**/
|
|
105
|
+
positionals: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Интерфейс для поэтапного разбора аргументов командной строки.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* Позволяет разбирать опции частями, что полезно при наличии глобальных и командных флагов.
|
|
112
|
+
*
|
|
113
|
+
* @since 0.4.0
|
|
114
|
+
*
|
|
115
|
+
**/
|
|
116
|
+
interface StagedArgs {
|
|
117
|
+
/**
|
|
118
|
+
* Разбирает аргументы по указанной схеме, не выбрасывая ошибки на неизвестные опции.
|
|
119
|
+
*
|
|
120
|
+
* @typeParam TSchema - Схема опций.
|
|
121
|
+
* @param schema - Схема опций для разбора.
|
|
122
|
+
* @returns Результат разбора и новый `StagedArgs` для последующих шагов.
|
|
123
|
+
*
|
|
124
|
+
**/
|
|
125
|
+
parse<TSchema extends OptionSchema>(schema: TSchema): Result<ParsedArgs<TSchema>, ParseError>;
|
|
126
|
+
/**
|
|
127
|
+
* Окончательный разбор аргументов. Проверяет наличие неизвестных опций и выбрасывает ошибку.
|
|
128
|
+
*
|
|
129
|
+
* @typeParam TSchema - Схема опций.
|
|
130
|
+
* @param schema - Схема опций.
|
|
131
|
+
* @returns Результат разбора или ошибка, если обнаружены неизвестные опции.
|
|
132
|
+
*
|
|
133
|
+
**/
|
|
134
|
+
parseFinal<TSchema extends OptionSchema>(schema: TSchema): Result<ParsedArgsFinal<TSchema>, ParseError>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Функция, возвращающая возможный вариант опции при опечатке.
|
|
138
|
+
*
|
|
139
|
+
* Используется для подсказок вроде "Did you mean '--config'?".
|
|
140
|
+
*
|
|
141
|
+
* @param input - Введённое пользователем имя опции.
|
|
142
|
+
* @param options - Список доступных имён опций.
|
|
143
|
+
* @returns Предложенное исправление или `undefined`, если нет подходящего.
|
|
144
|
+
*
|
|
145
|
+
* @since 0.4.0
|
|
146
|
+
*
|
|
147
|
+
**/
|
|
148
|
+
type SuggestFunc = (input: string, options: readonly string[]) => string | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Параметры для создания `StagedArgs`.
|
|
151
|
+
*
|
|
152
|
+
* @since 0.4.0
|
|
153
|
+
*
|
|
154
|
+
**/
|
|
155
|
+
interface StagedArgsOptions {
|
|
156
|
+
/** Функция подсказки для неизвестных опций. */
|
|
157
|
+
suggest?: SuggestFunc;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Описание ошибки парсинга аргументов.
|
|
161
|
+
*
|
|
162
|
+
* @since 0.4.0
|
|
163
|
+
*
|
|
164
|
+
**/
|
|
165
|
+
type ParseError = {
|
|
166
|
+
type: 'unknown-option';
|
|
167
|
+
option: string;
|
|
168
|
+
suggestion?: string;
|
|
169
|
+
} | {
|
|
170
|
+
type: 'missing-value';
|
|
171
|
+
option: string;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Создаёт начальный экземпляр `StagedArgs` для разбора аргументов командной строки.
|
|
176
|
+
*
|
|
177
|
+
* @param args - Массив строк, представляющих аргументы (например, `process.argv.slice(2)`).
|
|
178
|
+
* @returns Объект для поэтапного разбора аргументов.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const staged = createStagedArgs(process.argv.slice(2));
|
|
182
|
+
* const { values, stagedArgs } = staged.parse(globalSchema);
|
|
183
|
+
* const { values: cmdValues } = stagedArgs.parseFinal(commandSchema);
|
|
184
|
+
*
|
|
185
|
+
* @since 0.4.0
|
|
186
|
+
*
|
|
187
|
+
**/
|
|
188
|
+
declare function createStagedArgs(args: string[], options?: StagedArgsOptions): StagedArgs;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Шаблоны сообщений об ошибках в схеме.
|
|
192
|
+
*
|
|
193
|
+
* Каждая функция принимает параметры, специфичные для типа ошибки.
|
|
194
|
+
*
|
|
195
|
+
* @since 0.4.0
|
|
196
|
+
*
|
|
197
|
+
**/
|
|
198
|
+
declare const errorMessages: {
|
|
199
|
+
/**
|
|
200
|
+
* Вызывается при попытке использовать уже занятое имя опции.
|
|
201
|
+
*
|
|
202
|
+
* @param name - Имя, которое пытается занять.
|
|
203
|
+
* @param knownName - Имя, которое уже занято.
|
|
204
|
+
*
|
|
205
|
+
**/
|
|
206
|
+
readonly duplicateName: (name: string, knownName: string) => string;
|
|
207
|
+
/**
|
|
208
|
+
* Вызывается, если у опции, требующей значение, оно отсутствует.
|
|
209
|
+
*
|
|
210
|
+
* @param name - Имя опции без значения.
|
|
211
|
+
*
|
|
212
|
+
**/
|
|
213
|
+
readonly missingValue: (name: string) => string;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Коды ошибок, которые могут возникнуть при проверке схемы.
|
|
217
|
+
*
|
|
218
|
+
* @since 0.4.0
|
|
219
|
+
*
|
|
220
|
+
**/
|
|
221
|
+
type ErrorCode = keyof typeof errorMessages;
|
|
222
|
+
/**
|
|
223
|
+
* Ошибки этапа разработки, связанные с валидацией схемы.
|
|
224
|
+
*
|
|
225
|
+
* @since 0.4.0
|
|
226
|
+
*
|
|
227
|
+
**/
|
|
228
|
+
declare class SchemaError extends Error {
|
|
229
|
+
/**
|
|
230
|
+
* Код ошибки — идентификатор типа проблемы.
|
|
231
|
+
*
|
|
232
|
+
* Используется для точной идентификации причины.
|
|
233
|
+
*/
|
|
234
|
+
readonly code: ErrorCode;
|
|
235
|
+
/**
|
|
236
|
+
* Создаёт экземпляр ошибки схемы.
|
|
237
|
+
*
|
|
238
|
+
* @param message - Полное сообщение об ошибке.
|
|
239
|
+
* @param code - Код ошибки для программной обработки.
|
|
240
|
+
*
|
|
241
|
+
**/
|
|
242
|
+
private constructor();
|
|
243
|
+
/**
|
|
244
|
+
* Фабричный метод для создания типизированных ошибок схемы.
|
|
245
|
+
*
|
|
246
|
+
* @param code - Код ошибки (ключ из `errorMessages`).
|
|
247
|
+
* @param args - Аргументы, зависящие от типа ошибки.
|
|
248
|
+
* @returns Экземпляр `SchemaError` с готовым сообщением.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* const error = SchemaError.get('duplicateName', 'config', 'source');
|
|
253
|
+
* // [package] Option name "config" is already used for "source"
|
|
254
|
+
* ```
|
|
255
|
+
**/
|
|
256
|
+
static get<TError extends keyof typeof errorMessages>(code: TError, ...args: Parameters<typeof errorMessages[TError]>): SchemaError;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { SchemaError, createStagedArgs };
|
|
260
|
+
export type { OptionSchema, ParseError, ParsedArgs, ParsedArgsFinal, Result, StagedArgs, SuggestFunc, Values };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Имя текущего пакета.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.4.0
|
|
7
|
+
*
|
|
8
|
+
**/
|
|
9
|
+
const THIS_PACKAGE_NAME = '@mirta/staged-args';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Шаблоны сообщений об ошибках в схеме.
|
|
13
|
+
*
|
|
14
|
+
* Каждая функция принимает параметры, специфичные для типа ошибки.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.4.0
|
|
17
|
+
*
|
|
18
|
+
**/
|
|
19
|
+
const errorMessages = {
|
|
20
|
+
/**
|
|
21
|
+
* Вызывается при попытке использовать уже занятое имя опции.
|
|
22
|
+
*
|
|
23
|
+
* @param name - Имя, которое пытается занять.
|
|
24
|
+
* @param knownName - Имя, которое уже занято.
|
|
25
|
+
*
|
|
26
|
+
**/
|
|
27
|
+
'duplicateName': (name, knownName) => `Option name "${name}" is already used for "${knownName}"`,
|
|
28
|
+
/**
|
|
29
|
+
* Вызывается, если у опции, требующей значение, оно отсутствует.
|
|
30
|
+
*
|
|
31
|
+
* @param name - Имя опции без значения.
|
|
32
|
+
*
|
|
33
|
+
**/
|
|
34
|
+
'missingValue': (name) => `Missing value for option "${name}"`,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Ошибки этапа разработки, связанные с валидацией схемы.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.4.0
|
|
40
|
+
*
|
|
41
|
+
**/
|
|
42
|
+
class SchemaError extends Error {
|
|
43
|
+
/**
|
|
44
|
+
* Код ошибки — идентификатор типа проблемы.
|
|
45
|
+
*
|
|
46
|
+
* Используется для точной идентификации причины.
|
|
47
|
+
*/
|
|
48
|
+
code;
|
|
49
|
+
/**
|
|
50
|
+
* Создаёт экземпляр ошибки схемы.
|
|
51
|
+
*
|
|
52
|
+
* @param message - Полное сообщение об ошибке.
|
|
53
|
+
* @param code - Код ошибки для программной обработки.
|
|
54
|
+
*
|
|
55
|
+
**/
|
|
56
|
+
constructor(message, code) {
|
|
57
|
+
super(`[${THIS_PACKAGE_NAME}] ${message}`);
|
|
58
|
+
Object.setPrototypeOf(this, SchemaError.prototype);
|
|
59
|
+
this.name = 'SchemaError';
|
|
60
|
+
this.code = code;
|
|
61
|
+
Error.captureStackTrace(this, SchemaError);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Фабричный метод для создания типизированных ошибок схемы.
|
|
65
|
+
*
|
|
66
|
+
* @param code - Код ошибки (ключ из `errorMessages`).
|
|
67
|
+
* @param args - Аргументы, зависящие от типа ошибки.
|
|
68
|
+
* @returns Экземпляр `SchemaError` с готовым сообщением.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const error = SchemaError.get('duplicateName', 'config', 'source');
|
|
73
|
+
* // [package] Option name "config" is already used for "source"
|
|
74
|
+
* ```
|
|
75
|
+
**/
|
|
76
|
+
static get(code, ...args) {
|
|
77
|
+
const messageFn = errorMessages[code];
|
|
78
|
+
const message = messageFn(...args);
|
|
79
|
+
return new SchemaError(message, code);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Утилита для создания значений типа {@link Result}.
|
|
85
|
+
*
|
|
86
|
+
* Содержит статические методы для создания успешных и неудачных результатов.
|
|
87
|
+
*
|
|
88
|
+
* @since 0.4.0
|
|
89
|
+
*
|
|
90
|
+
**/
|
|
91
|
+
const ResultHandler = {
|
|
92
|
+
/**
|
|
93
|
+
* Создаёт успешный результат, содержащий данные.
|
|
94
|
+
*
|
|
95
|
+
* @param data - Данные, которые будут включены в результат.
|
|
96
|
+
* @returns Объект результата с `hasErrors: false` и указанными данными.
|
|
97
|
+
*
|
|
98
|
+
* @template TData - Тип передаваемых данных.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const result = ResultHandler.ok({ name: 'Alice', age: 30 });
|
|
103
|
+
* // { hasErrors: false, data: { name: 'Alice', age: 30 } }
|
|
104
|
+
* ```
|
|
105
|
+
**/
|
|
106
|
+
ok: (data) => ({
|
|
107
|
+
hasErrors: false,
|
|
108
|
+
data,
|
|
109
|
+
}),
|
|
110
|
+
/**
|
|
111
|
+
* Создаёт неудачный результат, содержащий список ошибок.
|
|
112
|
+
*
|
|
113
|
+
* Если передан пустой массив ошибок, выбрасывается исключение,
|
|
114
|
+
* так как неудачный результат должен содержать хотя бы одну ошибку.
|
|
115
|
+
*
|
|
116
|
+
* @param errors - Массив объектов ошибок, описывающих проблемы ввода.
|
|
117
|
+
* @returns Объект результата с `hasErrors: true` и указанным списком ошибок.
|
|
118
|
+
*
|
|
119
|
+
* @template TError - Тип объекта ошибки (например, строка или объект с типом и деталями).
|
|
120
|
+
*
|
|
121
|
+
* @throws {Error} Если передан пустой массив ошибок.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const errors = [{ type: 'unknown-option', option: '--confog' }];
|
|
126
|
+
* const result = ResultHandler.failed(errors);
|
|
127
|
+
* // { hasErrors: true, errors: [...] }
|
|
128
|
+
* ```
|
|
129
|
+
**/
|
|
130
|
+
failed: (errors) => {
|
|
131
|
+
if (errors.length === 0)
|
|
132
|
+
throw new Error('Errors array cannot be empty');
|
|
133
|
+
return {
|
|
134
|
+
hasErrors: true,
|
|
135
|
+
errors,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Расширяет схему, добавляя ссылки на исходный ключ и проверяя дубликаты.
|
|
142
|
+
*
|
|
143
|
+
* @param schema - Исходная схема опций.
|
|
144
|
+
* @returns Расширенная схема, где каждая опция содержит свой `key` и доступна по короткому имени.
|
|
145
|
+
* @throws Ошибка при обнаружении дубликатов имён или алиасов.
|
|
146
|
+
*
|
|
147
|
+
* @since 0.4.0
|
|
148
|
+
*
|
|
149
|
+
**/
|
|
150
|
+
function expandSchema(schema) {
|
|
151
|
+
const result = {};
|
|
152
|
+
for (const key of Object.keys(schema)) {
|
|
153
|
+
// Может задвоиться через short, когда длина 1 символ
|
|
154
|
+
if (key in result)
|
|
155
|
+
throw SchemaError.get('duplicateName', key, result[key].key);
|
|
156
|
+
const option = { ...schema[key], key };
|
|
157
|
+
result[key] = option;
|
|
158
|
+
if (option.short) {
|
|
159
|
+
// Может повториться в разных опциях
|
|
160
|
+
if (option.short in result)
|
|
161
|
+
throw SchemaError.get('duplicateName', option.short, result[option.short].key);
|
|
162
|
+
result[option.short] = option;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Сопоставляет токены с опциями по схеме и извлекает значения.
|
|
169
|
+
*
|
|
170
|
+
* @param schema - Схема опций.
|
|
171
|
+
* @param tokens - Разобранные токены.
|
|
172
|
+
* @param consumedIndices - Индексы токенов, уже обработанных на предыдущих стадиях.
|
|
173
|
+
* @returns Объект с значениями опций, позиционными аргументами и обновлённым списком потреблённых индексов.
|
|
174
|
+
* @throws Ошибка, если строковой опции не задано значение и нет значения по умолчанию.
|
|
175
|
+
*
|
|
176
|
+
* @since 0.4.0
|
|
177
|
+
*
|
|
178
|
+
**/
|
|
179
|
+
function mapToSchema(schema, tokens, consumedIndices = []) {
|
|
180
|
+
const values = {};
|
|
181
|
+
const positionals = [];
|
|
182
|
+
const errors = [];
|
|
183
|
+
const expandedSchema = expandSchema(schema);
|
|
184
|
+
const localConsumedIndices = new Set(consumedIndices);
|
|
185
|
+
const foundKeys = new Set();
|
|
186
|
+
let nextIndex = 0;
|
|
187
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
188
|
+
const token = tokens[i];
|
|
189
|
+
nextIndex = i + 1;
|
|
190
|
+
if (token.kind === 'positional') {
|
|
191
|
+
if (localConsumedIndices.has(i))
|
|
192
|
+
continue;
|
|
193
|
+
positionals.push(token.value);
|
|
194
|
+
}
|
|
195
|
+
if (token.kind !== 'option' || !token.name || !(token.name in expandedSchema))
|
|
196
|
+
continue;
|
|
197
|
+
const option = expandedSchema[token.name];
|
|
198
|
+
foundKeys.add(option.key);
|
|
199
|
+
if (option.type === 'boolean') {
|
|
200
|
+
values[option.key] = token.value !== 'false';
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
let value;
|
|
204
|
+
if (token.value !== undefined) {
|
|
205
|
+
value = token.value;
|
|
206
|
+
}
|
|
207
|
+
else if (nextIndex < tokens.length) {
|
|
208
|
+
const nextToken = tokens[nextIndex];
|
|
209
|
+
if (nextToken.kind === 'positional') {
|
|
210
|
+
value = nextToken.value;
|
|
211
|
+
localConsumedIndices.add(nextIndex);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (value !== undefined)
|
|
215
|
+
values[option.key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const key of Object.keys(schema)) {
|
|
219
|
+
// Пропускаем явно установленные значения.
|
|
220
|
+
if (key in values)
|
|
221
|
+
continue;
|
|
222
|
+
// Строковые опции можно указывать только вместе со значениями.
|
|
223
|
+
if (foundKeys.has(key) && schema[key].type === 'string') {
|
|
224
|
+
errors.push({ type: 'missing-value', option: key });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const defaultValue = schema[key].default;
|
|
228
|
+
// Для отсутствующих ключей применяем значения по умолчанию.
|
|
229
|
+
if (defaultValue !== undefined)
|
|
230
|
+
values[key] = defaultValue;
|
|
231
|
+
}
|
|
232
|
+
return errors.length > 0
|
|
233
|
+
? ResultHandler.failed(errors)
|
|
234
|
+
: ResultHandler.ok({
|
|
235
|
+
values,
|
|
236
|
+
positionals,
|
|
237
|
+
consumedIndices: [...localConsumedIndices],
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Проверяет, что все опции в токенах известны. Если нет — формирует набор ошибок с подсказками.
|
|
243
|
+
*
|
|
244
|
+
* @param tokens - Список токенов.
|
|
245
|
+
* @param knownArgs - Список известных имён опций и их коротких алиасов.
|
|
246
|
+
* @returns Набор ошибок или `undefined`, если ошибок нет.
|
|
247
|
+
*
|
|
248
|
+
* @since 0.4.0
|
|
249
|
+
*
|
|
250
|
+
**/
|
|
251
|
+
function restrictUnknownOptions(tokens, knownArgs, suggest) {
|
|
252
|
+
// Находим незадействованные опции.
|
|
253
|
+
const unknownTokens = tokens.filter((token) => token.kind === 'option'
|
|
254
|
+
&& !knownArgs.includes(token.name));
|
|
255
|
+
if (unknownTokens.length === 0)
|
|
256
|
+
return;
|
|
257
|
+
const errors = unknownTokens.map((token) => {
|
|
258
|
+
const name = token.name;
|
|
259
|
+
const error = {
|
|
260
|
+
type: 'unknown-option',
|
|
261
|
+
option: token.rawName,
|
|
262
|
+
suggestion: suggest && name.length > 1
|
|
263
|
+
? suggest(name, knownArgs)
|
|
264
|
+
: undefined,
|
|
265
|
+
};
|
|
266
|
+
return error;
|
|
267
|
+
});
|
|
268
|
+
return errors;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Дополняет список известных аргументов именами из новой схемы.
|
|
272
|
+
*
|
|
273
|
+
* @param knownArgs - Текущий список известных имён.
|
|
274
|
+
* @param schema - Новая схема опций.
|
|
275
|
+
* @returns Обновлённый список имён, включая длинные и короткие имена из схемы.
|
|
276
|
+
*
|
|
277
|
+
* @since 0.4.0
|
|
278
|
+
*
|
|
279
|
+
**/
|
|
280
|
+
function extendKnownArgs(knownArgs, schema) {
|
|
281
|
+
const knownSet = new Set(knownArgs ?? []);
|
|
282
|
+
for (const key of Object.keys(schema)) {
|
|
283
|
+
knownSet.add(key);
|
|
284
|
+
const short = schema[key].short;
|
|
285
|
+
if (short)
|
|
286
|
+
knownSet.add(short);
|
|
287
|
+
}
|
|
288
|
+
return [...knownSet];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Внутренняя функция для поэтапного разбора аргументов.
|
|
292
|
+
*
|
|
293
|
+
* @param args - Аргументы командной строки.
|
|
294
|
+
* @param schema - Схема опций для текущего этапа.
|
|
295
|
+
* @param context - Контекст предыдущих этапов (схема, известные аргументы, suggest и т.д.).
|
|
296
|
+
* @param options - Дополнительные настройки.
|
|
297
|
+
* @returns Результат разбора: данные или ошибки.
|
|
298
|
+
*
|
|
299
|
+
* @since 0.4.0
|
|
300
|
+
*
|
|
301
|
+
* @internal
|
|
302
|
+
*
|
|
303
|
+
**/
|
|
304
|
+
function parseInternal(args, schema, context, options = {}) {
|
|
305
|
+
// 1. Объединяем схемы для parseArgs
|
|
306
|
+
const stagedSchema = { ...context.schema, ...schema };
|
|
307
|
+
// 2. Токенизируем с полным контекстом
|
|
308
|
+
const { tokens } = parseArgs({
|
|
309
|
+
args,
|
|
310
|
+
options: stagedSchema,
|
|
311
|
+
tokens: true,
|
|
312
|
+
strict: false,
|
|
313
|
+
allowPositionals: true,
|
|
314
|
+
});
|
|
315
|
+
const knownArgs = extendKnownArgs(context.knownArgs, schema);
|
|
316
|
+
// Проверяем неизвестные опции, если запрещены
|
|
317
|
+
if (options.noUnknown) {
|
|
318
|
+
const errors = restrictUnknownOptions(tokens, knownArgs, context.suggest);
|
|
319
|
+
if (errors)
|
|
320
|
+
return ResultHandler.failed(errors);
|
|
321
|
+
}
|
|
322
|
+
const mapResult = mapToSchema(schema, tokens, context.consumedIndices);
|
|
323
|
+
if (mapResult.hasErrors)
|
|
324
|
+
return mapResult;
|
|
325
|
+
const { values, positionals, consumedIndices: localConsumedIndices, } = mapResult.data;
|
|
326
|
+
let stagedArgs;
|
|
327
|
+
return ResultHandler.ok({
|
|
328
|
+
values: values,
|
|
329
|
+
positionals,
|
|
330
|
+
get stagedArgs() {
|
|
331
|
+
return stagedArgs ??= createStage(args, {
|
|
332
|
+
suggest: context.suggest,
|
|
333
|
+
schema: stagedSchema,
|
|
334
|
+
knownArgs: knownArgs,
|
|
335
|
+
consumedIndices: localConsumedIndices,
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Создаёт экземпляр `StagedArgs` с заданными параметрами.
|
|
342
|
+
*
|
|
343
|
+
* @param args - Массив аргументов командной строки.
|
|
344
|
+
* @param mergedSchema - Объединённая схема опций с предыдущих стадий.
|
|
345
|
+
* @param knownArgs - Список уже известных имён опций.
|
|
346
|
+
* @param consumedIndices - Индексы токенов, уже обработанных ранее.
|
|
347
|
+
* @returns Объект `StagedArgs` с методами `parse` и `parseFinal`.
|
|
348
|
+
*
|
|
349
|
+
* @since 0.4.0
|
|
350
|
+
*
|
|
351
|
+
**/
|
|
352
|
+
function createStage(args, context = {}) {
|
|
353
|
+
const parse = (schema) => {
|
|
354
|
+
return parseInternal(args, schema, context);
|
|
355
|
+
};
|
|
356
|
+
const parseFinal = (schema) => {
|
|
357
|
+
const result = parseInternal(args, schema, context, {
|
|
358
|
+
noUnknown: true,
|
|
359
|
+
});
|
|
360
|
+
if (result.hasErrors)
|
|
361
|
+
return result;
|
|
362
|
+
return ResultHandler.ok({
|
|
363
|
+
values: result.data.values,
|
|
364
|
+
positionals: result.data.positionals,
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
return { parse, parseFinal };
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Создаёт начальный экземпляр `StagedArgs` для разбора аргументов командной строки.
|
|
371
|
+
*
|
|
372
|
+
* @param args - Массив строк, представляющих аргументы (например, `process.argv.slice(2)`).
|
|
373
|
+
* @returns Объект для поэтапного разбора аргументов.
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* const staged = createStagedArgs(process.argv.slice(2));
|
|
377
|
+
* const { values, stagedArgs } = staged.parse(globalSchema);
|
|
378
|
+
* const { values: cmdValues } = stagedArgs.parseFinal(commandSchema);
|
|
379
|
+
*
|
|
380
|
+
* @since 0.4.0
|
|
381
|
+
*
|
|
382
|
+
**/
|
|
383
|
+
function createStagedArgs(args, options = {}) {
|
|
384
|
+
const { suggest } = options;
|
|
385
|
+
return createStage(args, { suggest });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { SchemaError, createStagedArgs };
|
package/package.json
CHANGED
|
@@ -1,10 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirta/staged-args",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"description": "⚡ Staged CLI arguments parser with TypeScript support",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"license": "Unlicense",
|
|
5
6
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
"cli",
|
|
8
|
+
"arguments",
|
|
9
|
+
"parser",
|
|
10
|
+
"staged",
|
|
11
|
+
"command-line",
|
|
12
|
+
"typescript",
|
|
13
|
+
"mirta"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"imports": {
|
|
23
|
+
"#src/*": "./src/*.js"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/wb-mirta/core/tree/latest/packages/mirta-staged-args#readme",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/wb-mirta/core.git",
|
|
37
|
+
"directory": "packages/mirta-staged-args"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/wb-mirta/core/issues"
|
|
41
|
+
},
|
|
42
|
+
"funding": {
|
|
43
|
+
"type": "individual",
|
|
44
|
+
"url": "https://boosty.to/wihome/donate"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=24.12.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build:mono": "rollup -c node:@mirta/rollup/config-package"
|
|
51
|
+
}
|
|
52
|
+
}
|