lang-tag
Version:
A professional solution for managing translations in modern JavaScript/TypeScript projects, especially those using component-based architectures. `lang-tag` simplifies internationalization by allowing you to define translation keys directly within the com
732 lines (727 loc) • 26.2 kB
JavaScript
;
const commander = require("commander");
const path = require("pathe");
const fs = require("fs");
const url = require("url");
const globby = require("globby");
const process$1 = require("node:process");
const JSON5 = require("json5");
const promises = require("fs/promises");
const path$1 = require("path");
const chokidar = require("chokidar");
const micromatch = require("micromatch");
var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
function _interopNamespaceDefault(e) {
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
if (e) {
for (const k in e) {
if (k !== "default") {
const d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: () => e[k]
});
}
}
}
n.default = e;
return Object.freeze(n);
}
const process__namespace = /* @__PURE__ */ _interopNamespaceDefault(process$1);
const CONFIG_FILE_NAME = ".lang-tag.config.js";
const EXPORTS_FILE_NAME = ".lang-tag.exports.json";
const ANSI_CODES = {
reset: "\x1B[0m",
red: "\x1B[31m",
green: "\x1B[32m",
blue: "\x1B[34m",
yellow: "\x1B[33m",
cyan: "\x1B[36m",
bgRed: "\x1B[41m",
bgGreen: "\x1B[42m",
bgBlue: "\x1B[44m",
bgYellow: "\x1B[43m",
black: "\x1B[30m",
white: "\x1B[37m"
};
function colorize(text, colorCode) {
return `${colorCode}${text}${ANSI_CODES.reset}`;
}
const miniChalk = {
red: (text) => colorize(text, ANSI_CODES.red),
green: (text) => colorize(text, ANSI_CODES.green),
blue: (text) => colorize(text, ANSI_CODES.blue),
yellow: (text) => colorize(text, ANSI_CODES.yellow),
cyan: (text) => colorize(text, ANSI_CODES.cyan),
bgRedWhite: (text) => colorize(text, `${ANSI_CODES.bgRed}${ANSI_CODES.white}`),
bgGreenBlack: (text) => colorize(text, `${ANSI_CODES.bgGreen}${ANSI_CODES.black}`),
bgBlueWhite: (text) => colorize(text, `${ANSI_CODES.bgBlue}${ANSI_CODES.white}`),
bgYellowBlack: (text) => colorize(text, `${ANSI_CODES.bgYellow}${ANSI_CODES.black}`)
};
function time() {
const now = /* @__PURE__ */ new Date();
return now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0") + " ";
}
function success(message) {
console.log(time() + miniChalk.green("Success: ") + message);
}
function info(message) {
console.log(time() + miniChalk.cyan("Info: ") + message);
}
function warning(message) {
console.warn(time() + miniChalk.yellow("Warning: ") + message);
}
function error(message) {
console.error(time() + miniChalk.bgRedWhite("Error: ") + message);
}
function messageNamespacesUpdated(config, namespaces) {
let n = namespaces.map((n2) => miniChalk.yellow('"') + miniChalk.cyan(n2 + ".json") + miniChalk.yellow('"')).join(miniChalk.yellow(", "));
success("Updated namespaces " + miniChalk.yellow(config.outputDir) + miniChalk.yellow("(") + n + miniChalk.yellow(")"));
}
function messageLangTagTranslationConfigRegenerated(filePath) {
info(`Lang tag configurations written for file "${filePath}"`);
}
function messageCollectTranslations() {
info("Collecting translations from source files...");
}
function messageWatchMode() {
info("Starting watch mode for translations...");
info("Watching for changes...");
info("Press Ctrl+C to stop watching");
}
function messageFoundTranslationKeys(totalKeys) {
info(`Found ${totalKeys} translation keys`);
}
function messageExistingConfiguration() {
success(`Configuration file already exists. Please remove the existing configuration file before creating a new default one`);
}
function messageInitializedConfiguration() {
success(`Configuration file created successfully`);
}
function messageNoChangesMade() {
info("No changes were made based on the current configuration and files.");
}
function messageNodeModulesNotFound() {
warning('"node_modules" directory not found.');
}
function messageWrittenExportsFile() {
success(`Written ${EXPORTS_FILE_NAME}`);
}
function messageImportedFile(fileName) {
success(`Imported node_modules file: "${fileName}"`);
}
function messageOriginalNamespaceNotFound(filePath) {
warning(`Original namespace file "${filePath}" not found. A new one will be created.`);
}
function messageErrorInFile(e, file, match) {
error(e + `
Tag: ${match.fullMatch}
File: ${file}
Index: ${match.index}`);
}
function messageErrorInFileWatcher(e) {
error(`Error in file watcher: ${String(e)}`);
}
function messageErrorReadingConfig(error2) {
warning(`Error reading configuration file: ${String(error2)}`);
}
function messageSkippingInvalidJson(invalid, match) {
warning(`Skipping invalid JSON "${invalid}" at match "${match.fullMatch}"`);
}
function messageErrorReadingDirectory(dir, e) {
error(`Error reading directory "${dir}": ${String(e)}`);
}
function messageImportLibraries() {
info("Importing translations from libraries...");
}
function messageLibrariesImportedSuccessfully() {
success("Successfully imported translations from libraries.");
}
const defaultConfig = {
tagName: "lang",
includes: ["src/**/*.{js,ts,jsx,tsx}"],
excludes: ["node_modules", "dist", "build"],
outputDir: "locales/en",
import: {
dir: "src/lang-libraries",
tagImportPath: 'import { lang } from "@/my-lang-tag-path"',
onImport: ({ importedRelativePath, fileGenerationData }, actions) => {
const exportIndex = (fileGenerationData.index || 0) + 1;
fileGenerationData.index = exportIndex;
actions.setFile(path.basename(importedRelativePath));
actions.setExportName(`translations${exportIndex}`);
}
},
isLibrary: false,
language: "en",
translationArgPosition: 1,
onConfigGeneration: (params) => void 0
};
async function readConfig(projectPath) {
const configPath = path.resolve(projectPath, CONFIG_FILE_NAME);
if (!fs.existsSync(configPath)) {
throw new Error(`No "${CONFIG_FILE_NAME}" detected`);
}
try {
const configModule = await import(url.pathToFileURL(configPath).href);
if (!configModule.default || Object.keys(configModule.default).length === 0) {
throw new Error(`Config found, but default export is undefined`);
}
const userConfig = configModule.default || {};
return {
...defaultConfig,
...userConfig,
import: {
...defaultConfig.import,
...userConfig.import
}
};
} catch (error2) {
messageErrorReadingConfig(error2);
throw error2;
}
}
function extractLangTagData(config, match, beforeError) {
const pos = config.translationArgPosition;
const tagTranslationsContent = pos === 1 ? match.content1 : match.content2;
if (!tagTranslationsContent) {
beforeError();
throw new Error("Translations not found");
}
const tagTranslations = JSON5.parse(tagTranslationsContent);
let tagConfigContent = pos === 1 ? match.content2 : match.content1;
if (tagConfigContent) {
tagConfigContent = JSON5.stringify(JSON5.parse(tagConfigContent), void 0, 4);
}
const tagConfig = tagConfigContent ? JSON5.parse(tagConfigContent) : {
path: "",
namespace: ""
};
if (!tagConfig.path) tagConfig.path = "";
if (!tagConfig.namespace) tagConfig.namespace = "";
return {
replaceTagConfigContent(newConfigString) {
const arg1 = pos === 1 ? tagTranslationsContent : newConfigString;
const arg2 = pos === 1 ? newConfigString : tagTranslationsContent;
const tagFunction = `${config.tagName}(${arg1}, ${arg2})`;
if (match.variableName) return ` ${match.variableName} = ${tagFunction}`;
return tagFunction;
},
tagTranslationsContent,
tagConfigContent,
tagTranslations,
tagConfig
};
}
function findLangTags(config, content) {
const tagName = config.tagName;
const optionalVariableAssignment = `(?:\\s*(\\w+)\\s*=\\s*)?`;
const matches = [];
let currentIndex = 0;
const startPattern = new RegExp(`${optionalVariableAssignment}${tagName}\\(\\s*\\{`, "g");
while (true) {
startPattern.lastIndex = currentIndex;
const startMatch = startPattern.exec(content);
if (!startMatch) break;
const matchStartIndex = startMatch.index;
const variableName = startMatch[1] || void 0;
let braceCount = 1;
let i = matchStartIndex + startMatch[0].length;
while (i < content.length && braceCount > 0) {
if (content[i] === "{") braceCount++;
if (content[i] === "}") braceCount--;
i++;
}
if (braceCount !== 0) {
currentIndex = matchStartIndex + 1;
continue;
}
let content1 = content.substring(matchStartIndex + startMatch[0].length - 1, i);
let content2;
while (i < content.length && (content[i] === " " || content[i] === "\n" || content[i] === " " || content[i] === ",")) {
i++;
}
if (i < content.length && content[i] === "{") {
braceCount = 1;
const secondParamStart = i;
i++;
while (i < content.length && braceCount > 0) {
if (content[i] === "{") braceCount++;
if (content[i] === "}") braceCount--;
i++;
}
if (braceCount === 0) {
content2 = content.substring(secondParamStart, i);
}
}
while (i < content.length && content[i] !== ")") {
i++;
}
if (i < content.length) {
i++;
}
const fullMatch = content.substring(matchStartIndex, i);
matches.push({
fullMatch,
variableName,
content1,
content2,
index: matchStartIndex
});
currentIndex = i;
}
return matches.filter((m) => {
try {
JSON5.parse(m.content1);
} catch (error2) {
messageSkippingInvalidJson(m.content1, m);
return false;
}
if (m.content2) {
try {
JSON5.parse(m.content2);
} catch (error2) {
messageSkippingInvalidJson(m.content2, m);
return false;
}
}
return true;
});
}
function replaceLangTags(content, replacements) {
let updatedContent = content;
let offset = 0;
replacements.forEach((replacement, match) => {
const startIdx = match.index + offset;
const endIdx = startIdx + match.fullMatch.length;
updatedContent = updatedContent.slice(0, startIdx) + replacement + updatedContent.slice(endIdx);
offset += replacement.length - match.fullMatch.length;
});
return updatedContent;
}
function deepMergeTranslations(target, source) {
if (typeof target !== "object") {
throw new Error("Target must be an object");
}
if (typeof source !== "object") {
throw new Error("Source must be an object");
}
let changed = false;
for (const key in source) {
if (!source.hasOwnProperty(key)) {
continue;
}
let targetValue = target[key];
const sourceValue = source[key];
if (typeof targetValue === "string" && typeof sourceValue === "object") {
throw new Error(`Trying to write object into target key "${key}" which is translation already`);
}
if (Array.isArray(sourceValue)) {
throw new Error(`Trying to write array into target key "${key}", we do not allow arrays inside translations`);
}
if (typeof sourceValue === "object") {
if (!targetValue) {
targetValue = {};
target[key] = targetValue;
}
if (deepMergeTranslations(targetValue, sourceValue)) {
changed = true;
}
} else {
if (target[key] !== sourceValue) {
changed = true;
}
target[key] = sourceValue;
}
}
return changed;
}
function gatherTranslationsToNamespaces(files, config) {
const namespaces = {};
const pos = config.translationArgPosition;
let totalKeys = 0;
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const matches = findLangTags(config, content);
totalKeys += matches.length;
for (let match of matches) {
const { tagTranslations, tagConfig } = extractLangTagData(config, match, () => {
messageErrorInFile(`Translations at ${pos} argument position are not defined`, file, match);
});
const namespaceTranslations = namespaces[tagConfig.namespace] || {};
if (!(tagConfig.namespace in namespaces)) {
namespaces[tagConfig.namespace] = namespaceTranslations;
}
try {
const translations = digToSection(tagConfig.path, namespaceTranslations);
deepMergeTranslations(translations, tagTranslations);
} catch (e) {
messageErrorInFile(e.message + `, path: "${tagConfig.path}"`, file, match);
throw e;
}
}
}
return { namespaces, totalKeys };
}
function digToSection(key, translations) {
if (!key) return translations;
const sp = key.split(".");
let currentValue = translations[sp[0]];
if (currentValue && typeof currentValue != "object") {
throw new Error(`Key "${sp[0]}" is not an object (found value: "${currentValue}")`);
}
if (!currentValue) {
currentValue = {};
translations[sp[0]] = currentValue;
}
sp.shift();
return digToSection(sp.join("."), currentValue);
}
async function ensureDirectoryExists(filePath) {
await promises.mkdir(filePath, { recursive: true });
}
async function writeJSON(filePath, data) {
await promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
}
async function readJSON(filePath) {
const content = await promises.readFile(filePath, "utf-8");
return JSON.parse(content);
}
async function saveNamespaces(config, namespaces) {
const changedNamespaces = [];
await ensureDirectoryExists(config.outputDir);
for (let namespace of Object.keys(namespaces)) {
if (!namespace) {
continue;
}
const filePath = path.resolve(
process$1.cwd(),
config.outputDir,
namespace + ".json"
);
let originalJSON = {};
try {
originalJSON = await readJSON(filePath);
} catch (e) {
messageOriginalNamespaceNotFound(filePath);
}
if (deepMergeTranslations(originalJSON, namespaces[namespace])) {
changedNamespaces.push(namespace);
await writeJSON(filePath, originalJSON);
}
}
return changedNamespaces;
}
async function saveAsLibrary(files, config) {
const cwd = process$1.cwd();
const packageJson = await readJSON(path$1.resolve(cwd, "package.json"));
if (!packageJson) {
throw new Error("package.json not found");
}
const pos = config.translationArgPosition;
const langTagFiles = {};
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
const matches = findLangTags(config, content);
if (!matches?.length) {
continue;
}
const relativePath = path$1.relative(cwd, file);
const fileObject = {
matches: []
};
langTagFiles[relativePath] = fileObject;
for (let match of matches) {
const { tagTranslationsContent, tagConfigContent } = extractLangTagData(config, match, () => {
messageErrorInFile(`Translations at ${pos} argument position are not defined`, file, match);
});
fileObject.matches.push({
translations: tagTranslationsContent,
config: tagConfigContent,
variableName: match.variableName
});
}
}
const data = {
language: config.language,
packageName: packageJson.name || "",
files: langTagFiles
};
await writeJSON(EXPORTS_FILE_NAME, data);
messageWrittenExportsFile();
}
async function collectTranslations() {
messageCollectTranslations();
const config = await readConfig(process__namespace.cwd());
const files = await globby.globby(config.includes, {
cwd: process__namespace.cwd(),
ignore: config.excludes,
absolute: true
});
if (config.isLibrary) {
await saveAsLibrary(files, config);
} else {
const { namespaces, totalKeys } = gatherTranslationsToNamespaces(files, config);
messageFoundTranslationKeys(totalKeys);
const changedNamespaces = await saveNamespaces(config, namespaces);
if (changedNamespaces.length > 0) {
messageNamespacesUpdated(config, changedNamespaces);
} else {
messageNoChangesMade();
}
}
}
async function checkAndRegenerateFileLangTags(config, file, path2) {
const content = fs.readFileSync(file, "utf-8");
let libraryImportsDir = config.import.dir;
if (!libraryImportsDir.endsWith(path$1.sep)) libraryImportsDir += path$1.sep;
const pos = config.translationArgPosition;
const matches = findLangTags(config, content);
const replacements = /* @__PURE__ */ new Map();
for (let match of matches) {
const { tagConfig, tagConfigContent, replaceTagConfigContent } = extractLangTagData(config, match, () => {
messageErrorInFile(`Translations at ${pos} argument position are not defined`, file, match);
});
const newConfig = config.onConfigGeneration({
config: tagConfig,
fullPath: file,
path: path2,
isImportedLibrary: path2.startsWith(libraryImportsDir)
});
if (!tagConfigContent || newConfig) {
const newConfigString = JSON5.stringify(newConfig, void 0, 4);
if (tagConfigContent !== newConfigString) {
replacements.set(match, replaceTagConfigContent(newConfigString));
}
}
}
if (replacements.size) {
const newContent = replaceLangTags(content, replacements);
await promises.writeFile(file, newContent, "utf-8");
return true;
}
return false;
}
async function regenerateTags() {
const config = await readConfig(process.cwd());
const files = await globby.globby(config.includes, {
cwd: process.cwd(),
ignore: config.excludes,
absolute: true
});
const charactersToSkip = process.cwd().length + 1;
let dirty = false;
for (const file of files) {
const path2 = file.substring(charactersToSkip);
const localDirty = await checkAndRegenerateFileLangTags(config, file, path2);
if (localDirty) {
messageLangTagTranslationConfigRegenerated(path2);
dirty = true;
}
}
if (!dirty) {
messageNoChangesMade();
}
}
function getBasePath(pattern) {
const globStartIndex = pattern.indexOf("*");
const braceStartIndex = pattern.indexOf("{");
let endIndex = -1;
if (globStartIndex !== -1 && braceStartIndex !== -1) {
endIndex = Math.min(globStartIndex, braceStartIndex);
} else if (globStartIndex !== -1) {
endIndex = globStartIndex;
} else if (braceStartIndex !== -1) {
endIndex = braceStartIndex;
}
if (endIndex === -1) {
const lastSlashIndex = pattern.lastIndexOf("/");
return lastSlashIndex !== -1 ? pattern.substring(0, lastSlashIndex) : ".";
}
const lastSeparatorIndex = pattern.lastIndexOf("/", endIndex);
return lastSeparatorIndex === -1 ? "." : pattern.substring(0, lastSeparatorIndex);
}
async function watchTranslations() {
const cwd = process.cwd();
const config = await readConfig(cwd);
await collectTranslations();
const baseDirsToWatch = [
...new Set(config.includes.map((pattern) => getBasePath(pattern)))
];
const finalDirsToWatch = baseDirsToWatch.map((dir) => dir === "." ? cwd : dir);
const ignored = [...config.excludes, "**/.git/**"];
console.log("Original patterns:", config.includes);
console.log("Watching base directories:", finalDirsToWatch);
console.log("Ignoring patterns:", ignored);
const watcher = chokidar.watch(finalDirsToWatch, {
// Watch base directories
cwd,
ignored,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 300,
pollInterval: 100
}
});
messageWatchMode();
watcher.on("change", async (filePath) => await handleFile(config, filePath)).on("add", async (filePath) => await handleFile(config, filePath)).on("error", (error2) => {
messageErrorInFileWatcher(error2);
});
}
async function handleFile(config, cwdRelativeFilePath, event) {
if (!micromatch.isMatch(cwdRelativeFilePath, config.includes)) {
return;
}
const cwd = process.cwd();
const absoluteFilePath = path$1.join(cwd, cwdRelativeFilePath);
const dirty = await checkAndRegenerateFileLangTags(config, absoluteFilePath, cwdRelativeFilePath);
if (dirty) {
messageLangTagTranslationConfigRegenerated(cwdRelativeFilePath);
}
const { namespaces } = gatherTranslationsToNamespaces([cwdRelativeFilePath], config);
const changedNamespaces = await saveNamespaces(config, namespaces);
if (changedNamespaces.length > 0) {
messageNamespacesUpdated(config, changedNamespaces);
}
}
const DEFAULT_INIT_CONFIG = `
/** @type {import('lang-tag/cli/config').LangTagConfig} */
const config = {
includes: ['src/**/*.{js,ts,jsx,tsx}'],
excludes: ['node_modules', 'dist', 'build', '**/*.test.ts'],
outputDir: 'public/locales/en',
onConfigGeneration: (params) => {
// We do not modify imported configurations
if (params.isImportedLibrary) return undefined;
//if (!params.config.path) {
// params.config.path = 'test';
// params.config.namespace = 'testNamespace';
//}
return undefined
}
};
module.exports = config;
`;
async function initConfig() {
if (fs.existsSync(CONFIG_FILE_NAME)) {
messageExistingConfiguration();
return;
}
try {
await promises.writeFile(CONFIG_FILE_NAME, DEFAULT_INIT_CONFIG, "utf-8");
messageInitializedConfiguration();
} catch (error2) {
console.error("Error creating configuration file:", error2);
}
}
function findExportJson(dir, depth = 0, maxDepth = 3) {
if (depth > maxDepth) return [];
let results = [];
try {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
results = results.concat(findExportJson(fullPath, depth + 1, maxDepth));
} else if (file === EXPORTS_FILE_NAME) {
results.push(fullPath);
}
}
} catch (error2) {
messageErrorReadingDirectory(dir, error2);
}
return results;
}
function getExportFiles() {
const nodeModulesPath = path.join(process__namespace.cwd(), "node_modules");
if (!fs.existsSync(nodeModulesPath)) {
messageNodeModulesNotFound();
return [];
}
return findExportJson(nodeModulesPath);
}
async function importLibraries(config) {
const files = getExportFiles();
await ensureDirectoryExists(config.import.dir);
const generationFiles = {};
for (const filePath of files) {
const exportData = await readJSON(filePath);
for (let langTagFilePath in exportData.files) {
const fileGenerationData = {};
const matches = exportData.files[langTagFilePath].matches;
for (let match of matches) {
let parsedTranslations = typeof match.translations === "string" ? JSON5.parse(match.translations) : match.translations;
let parsedConfig = typeof match.config === "string" ? JSON5.parse(match.config) : match.config === void 0 ? {} : match.config;
let file = langTagFilePath;
let exportName = match.variableName || "";
config.import.onImport({
packageName: exportData.packageName,
importedRelativePath: langTagFilePath,
originalExportName: match.variableName,
translations: parsedTranslations,
config: parsedConfig,
fileGenerationData
}, {
setFile: (f) => {
file = f;
},
setExportName: (name) => {
exportName = name;
},
setConfig: (newConfig) => {
parsedConfig = newConfig;
}
});
if (!file || !exportName) {
throw new Error(`[lang-tag] onImport did not set fileName or exportName for package: ${exportData.packageName}, file: '${file}' (original: '${langTagFilePath}'), exportName: '${exportName}' (original: ${match.variableName})`);
}
let exports2 = generationFiles[file];
if (!exports2) {
exports2 = {};
generationFiles[file] = exports2;
}
const param1 = config.translationArgPosition === 1 ? parsedTranslations : parsedConfig;
const param2 = config.translationArgPosition === 1 ? parsedConfig : parsedTranslations;
exports2[exportName] = `${config.tagName}(${JSON5.stringify(param1, void 0, 4)}, ${JSON5.stringify(param2, void 0, 4)})`;
}
}
}
for (let file of Object.keys(generationFiles)) {
const filePath = path.resolve(
process__namespace.cwd(),
config.import.dir,
file
);
const exports2 = Object.entries(generationFiles[file]).map(([name, tag]) => {
return `export const ${name} = ${tag};`;
}).join("\n\n");
const content = `${config.import.tagImportPath}
${exports2}`;
await ensureDirectoryExists(path.dirname(filePath));
await promises.writeFile(filePath, content, "utf-8");
messageImportedFile(file);
}
if (config.import.onImportFinish) config.import.onImportFinish();
}
async function importTranslations() {
messageImportLibraries();
const config = await readConfig(process__namespace.cwd());
await importLibraries(config);
messageLibrariesImportedSuccessfully();
}
function createCli() {
commander.program.name("lang-tag").description("CLI to manage language translations").version("0.1.0");
commander.program.command("collect").alias("c").description("Collect translations from source files").action(collectTranslations);
commander.program.command("import").alias("i").description("Import translations from libraries in node_modules").action(importTranslations);
commander.program.command("regenerate-tags").alias("rt").description("Regenerate configuration for language tags").action(regenerateTags);
commander.program.command("watch").alias("w").description("Watch for changes in source files and automatically collect translations").action(watchTranslations);
commander.program.command("init").description("Initialize project with default configuration").action(initConfig);
return commander.program;
}
if ((typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("cli/index.cjs", document.baseURI).href) === `file://${process.argv[1]}`) {
createCli().parse();
}
createCli().parse();