CLI-утилиты в Node.js почти всегда опираются на входные аргументы командной строки, которые приходят в виде строк и требуют строгой проверки перед использованием. Основная проблема заключается в отсутствии типизации: числа, булевы значения, списки и флаги передаются как строки, что делает валидацию обязательным этапом.
Библиотека Joi позволяет формализовать структуру CLI-аргументов через декларативные схемы и выполнять их проверку с преобразованием типов, значениями по умолчанию и строгими ограничениями.
Аргументы командной строки доступны через:
process.argvТипичный пример:
node app.js --port=3000 --debug=true --files=a.txt,b.txt
После парсинга получается объект:
{
port: "3000",
debug: "true",
files: "a.txt,b.txt"
}
Основная задача Joi — привести этот объект к строгой структуре:
{
port: 3000,
debug: true,
files: ["a.txt", "b.txt"]
}
Joi позволяет описывать схему входных данных через цепочки методов:
import Joi from 'joi';
const schema = Joi.object({
port: Joi.number().integer().min(1).max(65535).default(3000),
debug: Joi.boolean().default(false),
files: Joi.array().items(Joi.string()).default([])
});
После парсинга CLI-аргументов они передаются в Joi:
const args = {
port: process.env.PORT,
debug: process.argv.includes('--debug'),
files: process.argv
.find(arg => arg.startsWith('--files='))
?.split('=')[1]
?.split(',')
};
const result = schema.validate(args, {
convert: true,
abortEarly: false
});
Включает автоматическое преобразование типов
"3000" → 3000"true" → trueИспользуется по умолчанию и критически важен для CLI.
Контролирует поведение при ошибках:
true — остановка на первой ошибкеfalse — сбор всех ошибок{
abortEarly: false
}
CLI часто содержит лишние параметры.
{
allowUnknown: true,
stripUnknown: true
}
allowUnknown — разрешает неизвестные поляstripUnknown — удаляет их из результатаCLI почти всегда передаёт всё как строки:
debug: Joi.boolean()
Вход:
--debug=true
Результат:
true
port: Joi.number().integer()
Вход:
--port=8080
Результат:
8080
CLI:
--files=a.js,b.js,c.js
Схема:
files: Joi.array().items(Joi.string())
Подготовка:
const files = rawFiles?.split(',');
Joi позволяет задавать строгую обязательность:
input: Joi.string().required()
Если параметр отсутствует:
Error: "input" is required
CLI часто имеет взаимосвязанные аргументы:
const schema = Joi.object({
mode: Joi.string().valid('dev', 'prod'),
debug: Joi.boolean()
}).when(Joi.object({ mode: 'dev' }).unknown(), {
then: Joi.object({
debug: Joi.boolean().default(true)
})
});
CLI требует понятных сообщений:
const schema = Joi.object({
port: Joi.number().required().messages({
'number.base': 'Порт должен быть числом',
'any.required': 'Не указан порт запуска'
})
});
Joi работает с уже структурированными данными, поэтому требуется парсинг CLI:
function parseArgs(argv) {
const args = {};
argv.forEach(arg => {
if (arg.startsWith('--')) {
const [key, value] = arg.replace('--', '').split('=');
args[key] = value ?? true;
}
});
return args;
}
import Joi from 'joi';
const schema = Joi.object({
port: Joi.number().integer().min(1).max(65535).default(3000),
host: Joi.string().default('localhost'),
debug: Joi.boolean().default(false)
});
function parseArgs(argv) {
const args = {};
for (const arg of argv) {
if (arg.startsWith('--')) {
const [key, value] = arg.slice(2).split('=');
args[key] = value ?? true;
}
}
return args;
}
const rawArgs = parseArgs(process.argv.slice(2));
const { value, error } = schema.validate(rawArgs, {
convert: true,
abortEarly: false,
stripUnknown: true
});
if (error) {
console.error(error.details.map(d => d.message).join('\n'));
process.exit(1);
}
console.log('Конфигурация:', value);
CLI может содержать вложенные структуры:
--db.host=localhost --db.port=5432
Парсинг:
function deepParse(argv) {
const result = {};
argv.forEach(arg => {
if (!arg.startsWith('--')) return;
const pathValue = arg.slice(2);
const [path, value] = pathValue.split('=');
const keys = path.split('.');
let current = result;
keys.forEach((key, index) => {
if (index === keys.length - 1) {
current[key] = value ?? true;
} else {
current[key] = current[key] || {};
current = current[key];
}
});
});
return result;
}
Схема Joi:
const schema = Joi.object({
db: Joi.object({
host: Joi.string().required(),
port: Joi.number().integer().required()
})
});
CLI часто содержит вспомогательные флаги:
--verbose --trace --experimental
Чтобы не загрязнять конфигурацию:
stripUnknown: true
Результат будет содержать только описанные в схеме поля.
CLI часто дополняется env:
const config = {
port: process.env.PORT,
debug: process.env.DEBUG === 'true'
};
Схема Joi:
const schema = Joi.object({
port: Joi.number().default(3000),
debug: Joi.boolean().default(false)
});
CLI может переопределять env:
const finalConfig = schema.validate({
...config,
...rawArgs
}).value;
Флаги вида:
--verbose
--silent
Обрабатываются как boolean:
verbose: Joi.boolean().default(false)
Парсер:
args[key] = value === undefined ? true : value;
mode: Joi.string().valid('development', 'production', 'test')
CLI:
--mode=production
При ошибке:
"mode" must be one of [development, production, test]
const schema = Joi.object({
file: Joi.string().custom((value, helpers) => {
if (!value.endsWith('.json')) {
return helpers.error('any.invalid');
}
return value;
})
});
CLI:
--tag=js --tag=node --tag=backend
Парсер:
if (!args.tag) args.tag = [];
args.tag = Array.isArray(args.tag) ? args.tag : [args.tag];
Схема:
tags: Joi.array().items(Joi.string())