π ESLint plugin for citty and all citty lovers
npm install -D eslint-plugin-cittySample eslint.config.mjs
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import citty from "eslint-plugin-citty";
export default [
{
plugins: {
citty: citty,
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...citty.configs.recommended,
{
files: ["**/*.{ts,tsx,cts,mts}"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./tsconfig.json",
},
globals: globals.node,
},
},
];| Rule | π¨ error | π§ fix | |
|---|---|---|---|
| alias-should-be-short | β | ||
| enum-must-have-options | β | ||
| group-command-args | β | ||
| must-have-run-or-sub-commands | β | ||
| no-alias-on-positional | β | β | |
| no-default-on-positional | β | β | |
| no-duplicated-version | β | ||
| no-empty-command-properties | β | β | |
| no-hidden-root-command | β | ||
| no-meaningless-value-hint | β | β | |
| recommend-meta | β | ||
| recommend-name-and-description-in-args | β | ||
| sort-define-command-properties | β | ||
| sort-sub-commands | β | β | |
| valid-version | β | ||
| value-hint-case | β |
Disallow multiple version definitions in citty commands within the same file.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
version: "1.0.0",
},
});
export const sub = defineCommand({
meta: {
version: "1.0.1", // Duplicate version definition in the same file
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
version: "1.0.0",
},
});
export const sub = defineCommand({
meta: {
description: "Sub command",
},
});Enforce version format in citty commands. Defaults to SemVer.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
version: "invalid-version",
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
version: "1.0.0",
},
});You can customize the version format using the pattern option.
Config
{
"rules": {
"citty/valid-version": ["error", { "pattern": "^\\d{4}\\.\\d{2}\\.\\d{2}$" }]
}
}Bad (Custom Pattern)
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
version: "1.0.0", // Does not match YYYY.MM.DD
},
});Good (Custom Pattern)
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
version: "2023.12.01",
},
});Disallow valueHint when it is meaningless (boolean type or enum specified).
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
verbose: {
type: Boolean,
valueHint: "level", // meaningless for boolean
},
format: {
type: String,
enum: ["json", "yaml"],
valueHint: "format", // meaningless for enum
},
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
name: {
type: String,
valueHint: "NAME",
},
verbose: {
type: Boolean,
},
},
});Enforce letter case for valueHint. Defaults to upper case (UPPER_CASE).
Options
case:upper(default),lower,camel,kebab,pascal,snake
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
name: {
type: String,
valueHint: "name", // Should be upper case
},
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
name: {
type: String,
valueHint: "NAME",
},
},
});{
"rules": {
"citty/value-hint-case": ["error", { "case": "camel" }]
}
}Good (Camel Case Config)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
userId: {
type: String,
valueHint: "userId",
},
},
});no-hidden-root-command
Disallow hidden property in root commands.
This rule considers export default defineCommand(...) or export const main = defineCommand(...) as root commands.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
hidden: true, // Root command cannot be hidden
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
name: "main",
},
});
export const sub = defineCommand({
meta: {
hidden: true, // Sub command can be hidden
},
});Disallow alias property on positional arguments.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
input: {
type: "positional",
alias: "i", // Positional arguments cannot have aliases
},
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
input: {
type: "positional",
},
verbose: {
type: "boolean",
alias: "v",
},
},
});Enforce options property when argument type is enum.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
choice: {
type: "enum",
// options missing
},
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
choice: {
type: "enum",
options: ["a", "b"],
},
},
});Enforce that a command has either run or subCommands defined.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
name: "main",
},
// missing run or subCommands
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
name: "main",
},
run() {
console.log("Hello");
},
});Recommend that a command has meta property defined.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
run() {
console.log("Hello");
},
// missing meta
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {
name: "main",
description: "My command",
},
run() {
console.log("Hello");
},
});Disallow empty implementations for run, setup, cleanup, and subCommands.
These properties should be removed if not used.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
run() {}, // Empty function
subCommands: {}, // Empty object
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
run() {
console.log("Running...");
},
// subCommands removed
});Disallow default property on positional arguments.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
input: {
type: "positional",
default: "default.txt", // Positional arguments cannot have default values
},
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
input: {
type: "positional",
},
verbose: {
type: "boolean",
default: false,
},
},
});Warn when an alias is not shorter than the argument name itself.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
force: {
alias: "force", // Same length as argument name
},
},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
force: {
alias: "f",
},
},
});Recommend that an arg has name and description properties defined.
YouYou can customize which properties to check using the check option. By default, both name and description are checked.
check:array(default:["name", "description"]) An array of strings indicating which properties to check for existence. Valid values are"name"and"description".
Config (Check only name)
{
"rules": {
"citty/recommend-name-and-description-in-args": ["warn", { "check": ["name"] }]
}
}Bad (Checking only name)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
foo: {
description: "Foo argument",
// missing name
},
},
});Good (Checking only name)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
foo: {
name: "foo",
description: "Foo argument",
},
},
});Config (Check only description)
{
"rules": {
"citty/recommend-name-and-description-in-args": ["warn", { "check": ["description"] }]
}
}Bad (Checking only description)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
foo: {
name: "foo",
// missing description
},
},
});Good (Checking only description)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
foo: {
name: "foo",
description: "Foo argument",
},
},
});Bad (Default behavior - missing both)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
force: {
// missing name and description
},
},
});Good (Default behavior - both present)
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
force: {
name: "force",
description: "Force the command",
},
},
});Enforce subCommands to be sorted alphabetically and prefer command references over inline definitions.
Bad
import { defineCommand } from "citty";
const b = defineCommand({});
const a = defineCommand({});
export const main = defineCommand({
subCommands: {
b, // Not sorted
a,
c: defineCommand({}), // Inline definition is not allowed
},
});Good
import { defineCommand } from "citty";
const a = defineCommand({});
const b = defineCommand({});
const c = defineCommand({});
export const main = defineCommand({
subCommands: {
a,
b,
c,
},
});Enforce a specific order for properties in defineCommand: meta, args, setup, run, cleanup, subCommands.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
run() {},
meta: {},
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
meta: {},
run() {},
});Enforce a specific grouping order for command arguments: positional, then required, then optional.
Bad
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
optional: {},
required: { required: true },
positional: { type: "positional" },
}
});Good
import { defineCommand } from "citty";
export const main = defineCommand({
args: {
positional: { type: "positional" },
required: { required: true },
optional: {},
}
});Made by Kanon. Publish under MIT License.