npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gherkin-lint-plus

v1.0.2

Published

Gherkin linter (TypeScript, @cucumber/gherkin)

Readme

gherkin-lint-plus

A Gherkin linter for .feature files, implemented in TypeScript and built on @cucumber/gherkin. It aims to be a modern, maintainable take on the same idea as the original gherkin-lint CLI.

Requirements: Node.js >=20.

Install

npm install gherkin-lint-plus

From a git checkout (development):

cd gherkin-lint-plus
npm install
npm run build

npm run build runs the unit tests first, then compiles to dist/.

Usage

You need a config file in the current working directory unless you pass -c (see below).

npx gherkin-lint-plus [options] [files...]
  • No arguments: lints all **/*.feature under ., respecting ignore rules.
  • Paths: files, directories, or globs; directories are expanded to **/*.feature under them.

CLI options

| Option | Description | |--------|-------------| | -c, --config [path] | Config file (default: .gherkin-lintrc in cwd) | | -f, --format [format] | Output format; only stylish is supported today | | -i, --ignore <items> | Comma-separated ignore globs (overrides .gherkin-lintignore if present) | | -r, --rulesdir <dir> | Extra directory of custom rules (repeatable); see Custom rules |

Exit code 1 if any file has lint errors.

Configuration file

The default name is .gherkin-lintrc in your working directory. Contents are JSON with optional comments (JSONC). Use -c / --config if the file lives elsewhere.

Each key is a rule name. Allowed values:

| Form | Meaning | |------|---------| | "on" | Rule enabled with default options (or no extra options). | | "off" | Rule disabled (not allowed for mandatory rules). | | ["on", <options>] | Rule enabled; <options> is an object for most rules, or a string where documented (e.g. new-line-at-eof, no-dupe-scenario-names). | | ["off", <options>] | Rule disabled (same restriction for mandatory rules). |

Optional rules are off if omitted from the config.

Mandatory rules always run. You may omit them or set "on" / ["on", …]; you must not set them to "off" or ["off", …] (the CLI reports a configuration error).

Example .gherkin-lintrc

{
  "indentation": "on",
  "no-trailing-spaces": "on",
  "file-name": ["on", { "style": "kebab-case" }],
  "new-line-at-eof": ["on", "yes"]
}

Ignoring feature files

  1. Add .gherkin-lintignore in the working directory: one glob pattern per line.
  2. Or use -i / --ignore with comma-separated globs (this overrides the ignore file when provided).

If there is no ignore file and you did not pass -i, node_modules/** is ignored by default.

Built-in rules summary

| Rule name | Category | In .gherkin-lintrc? | Enable example | |-----------|----------|------------------------|----------------| | no-tags-on-backgrounds | Parse | No — fix the .feature file | — | | one-feature-per-file | Parse | No | — | | up-to-one-background-per-file | Parse | No | — | | no-multiline-steps | Parse | No | — | | unexpected-error | Parse | No (other parse failures) | — | | no-empty-file | Mandatory | Yes (cannot turn off) | Omit, or "no-empty-file": "on" | | no-files-without-scenarios | Mandatory | Yes (cannot turn off) | Omit, or "on" | | no-unnamed-features | Mandatory | Yes (cannot turn off) | Omit, or "on" | | no-unnamed-scenarios | Mandatory | Yes (cannot turn off) | Omit, or "on" | | allowed-tags | Optional | Yes | "allowed-tags": "on" + options (see below) | | file-name | Optional | Yes | "file-name": ["on", { "style": "PascalCase" }] | | indentation | Optional | Yes | "indentation": "on" or ["on", { … }] | | keywords-in-logical-order | Optional | Yes | "keywords-in-logical-order": "on" | | max-scenarios-per-file | Optional | Yes | "max-scenarios-per-file": ["on", { … }] | | name-length | Optional | Yes | "name-length": ["on", { … }] | | new-line-at-eof | Optional | Yes | "new-line-at-eof": ["on", "yes"] | | no-background-only-scenario | Optional | Yes | "no-background-only-scenario": "on" | | no-dupe-feature-names | Optional | Yes | "no-dupe-feature-names": "on" | | no-dupe-scenario-names | Optional | Yes | "on" or ["on", "in-feature"] | | no-duplicate-tags | Optional | Yes | "no-duplicate-tags": "on" | | no-empty-background | Optional | Yes | "no-empty-background": "on" | | no-examples-in-scenarios | Optional | Yes | "no-examples-in-scenarios": "on" | | no-homogenous-tags | Optional | Yes | "no-homogenous-tags": "on" | | no-multiple-empty-lines | Optional | Yes | "no-multiple-empty-lines": "on" | | no-partially-commented-tag-lines | Optional | Yes | "no-partially-commented-tag-lines": "on" | | no-restricted-patterns | Optional | Yes | ["on", { … }] | | no-restricted-tags | Optional | Yes | ["on", { "tags": [], "patterns": [] }] | | no-scenario-outlines-without-examples | Optional | Yes | "no-scenario-outlines-without-examples": "on" | | no-superfluous-tags | Optional | Yes | "no-superfluous-tags": "on" | | no-trailing-spaces | Optional | Yes | "no-trailing-spaces": "on" | | no-unused-variables | Optional | Yes | "no-unused-variables": "on" | | one-space-between-tags | Optional | Yes | "one-space-between-tags": "on" | | only-one-when | Optional | Yes | "only-one-when": "on" | | required-tags | Optional | Yes | ["on", { … }] | | scenario-size | Optional | Yes | ["on", { "steps-length": { … } }] | | use-and | Optional | Yes | "use-and": "on" |

Parse rules are emitted when the Gherkin parser rejects the file. They are not toggled in config; correct the source. Mandatory rules always run and cannot be disabled.

Backgrounds: you may have one optional Background under Feature, and one optional Background inside each Rule:. up-to-one-background-per-file fires when a second Background appears in the same scope (duplicate at feature level or duplicate inside one rule).


Rule reference

Parse-time rules (not configurable)

These appear as rule IDs on parse failures. Fix the .feature text; do not add them to .gherkin-lintrc.

| Rule | What it checks | |------|----------------| | no-tags-on-backgrounds | Tags are not allowed on Background. | | one-feature-per-file | Only one Feature: per file. | | up-to-one-background-per-file | At most one Background in each feature or rule block. | | no-multiline-steps | Step text must not continue as a random continuation line (use And, Doc Strings, or tables). | | unexpected-error | Other parser errors, surfaced with the raw message. |


Mandatory rules

Always evaluated. Use "on" or omit the key; never "off".

no-empty-file

What: Whitespace-only or missing feature content is not allowed.

Example:

{
  "no-empty-file": "on"
}

no-files-without-scenarios

What: The file must contain at least one scenario (including under Rule:).

Example:

{
  "no-files-without-scenarios": "on"
}

no-unnamed-features

What: Feature: must have a name.

Example:

{
  "no-unnamed-features": "on"
}

no-unnamed-scenarios

What: Scenario: / Scenario Outline: must have a name.

Example:

{
  "no-unnamed-scenarios": "on"
}

Optional rules

Turn on with "rule-name": "on" or "off" to disable. Use ["on", …] when the rule needs options (below).

allowed-tags

What: Only listed tags and patterns are allowed on tag-bearing nodes.

Enable / off:

{
  "allowed-tags": [
    "on",
    { "tags": ["@watch", "@wip"], "patterns": ["^@todo$"] }
  ]
}
{ "allowed-tags": "off" }

file-name

What: Basename of *.feature must match a naming style (default PascalCase).

Styles: PascalCase, Title Case, camelCase, kebab-case, snake_case

{
  "file-name": ["on", { "style": "kebab-case" }]
}

indentation

What: Checks column positions for keywords, tags, steps, and example tables. Defaults: feature-level keywords at 0; steps and table rows at 2 spaces. Override per keyword type; localized Gherkin keywords are supported.

{
  "indentation": [
    "on",
    {
      "Feature": 0,
      "Background": 0,
      "Rule": 0,
      "Scenario": 0,
      "Step": 2,
      "Examples": 0,
      "example": 2,
      "given": 2,
      "when": 2,
      "then": 2,
      "and": 2,
      "but": 2,
      "feature tag": 0,
      "scenario tag": 0
    }
  ]
}

keywords-in-logical-order

What: Given / When / Then order should follow a sensible flow.

{ "keywords-in-logical-order": "on" }

max-scenarios-per-file

What: Caps how many “scenarios” count per file (plain scenarios count as 1; outlines can count each example row).

Options: maxScenarios (default 10), countOutlineExamples (default true)

{
  "max-scenarios-per-file": [
    "on",
    { "maxScenarios": 10, "countOutlineExamples": true }
  ]
}

name-length

What: Max length for Feature, Rule, Scenario, and step text (default 70 each).

{
  "name-length": [
    "on",
    { "Feature": 70, "Rule": 70, "Scenario": 70, "Step": 70 }
  ]
}

new-line-at-eof

What: Require or forbid a newline at end of file. Must use a string option: "yes" or "no".

{ "new-line-at-eof": ["on", "yes"] }
{ "new-line-at-eof": ["on", "no"] }

no-background-only-scenario

What: If there is only one scenario, a Background is not allowed (feature level or inside a Rule).

{ "no-background-only-scenario": "on" }

no-dupe-feature-names

What: The same feature name must not appear in more than one linted file (cross-file state).

{ "no-dupe-feature-names": "on" }

no-dupe-scenario-names

What: Duplicate scenario names. String option: "anywhere" (default, cross-file) or "in-feature" (per feature only).

{ "no-dupe-scenario-names": "on" }
{ "no-dupe-scenario-names": ["on", "in-feature"] }

no-duplicate-tags

What: No duplicate tags on the same feature, rule, or scenario.

{ "no-duplicate-tags": "on" }

no-empty-background

What: Background must contain at least one step.

{ "no-empty-background": "on" }

no-examples-in-scenarios

What: Examples belongs on scenario outlines, not plain scenarios.

{ "no-examples-in-scenarios": "on" }

no-homogenous-tags

What: If every scenario shares the same tag(s), prefer moving those tags to the feature (or avoid redundant tagging).

{ "no-homogenous-tags": "on" }

no-multiple-empty-lines

What: No runs of multiple blank lines.

{ "no-multiple-empty-lines": "on" }

no-partially-commented-tag-lines

What: Tag lines must not be partially commented in a misleading way.

{ "no-partially-commented-tag-lines": "on" }

no-restricted-patterns

What: Ban regex patterns (case-insensitive) in names, descriptions, and step text. Keys: Global, Feature, Rule, Background, Scenario, ScenarioOutline. Match step text only (not the Given/When keyword).

{
  "no-restricted-patterns": [
    "on",
    {
      "Global": ["^globally restricted"],
      "Feature": ["poor description"],
      "Background": [],
      "Scenario": [],
      "Rule": [],
      "ScenarioOutline": []
    }
  ]
}

no-restricted-tags

What: Disallow specific @tags and patterns.

{
  "no-restricted-tags": [
    "on",
    { "tags": ["@watch"], "patterns": ["^@todo$"] }
  ]
}

no-scenario-outlines-without-examples

What: Every scenario outline must have Examples.

{ "no-scenario-outlines-without-examples": "on" }

no-superfluous-tags

What: Tags repeated on both feature and scenarios inside that feature are flagged.

{ "no-superfluous-tags": "on" }

no-trailing-spaces

What: No trailing spaces on lines.

{ "no-trailing-spaces": "on" }

no-unused-variables

What: Scenario outline placeholders must match Examples headers (no unused columns, no missing columns).

{ "no-unused-variables": "on" }

one-space-between-tags

What: Multiple tags on one line must be separated by a single space.

{ "one-space-between-tags": "on" }

only-one-when

What: At most one When step per scenario.

{ "only-one-when": "on" }

required-tags

What: Scenarios must match tag regexes. Options: tags (regex strings), ignoreUntagged (default true — untagged scenarios skipped).

{
  "required-tags": [
    "on",
    { "tags": ["^@issue:[1-9]\\d*$"], "ignoreUntagged": false }
  ]
}

scenario-size

What: Max steps per Background (feature level), Scenario (feature-level scenarios/outlines), and everything under a Rule (uses the Rule limit).

{
  "scenario-size": [
    "on",
    {
      "steps-length": {
        "Rule": 15,
        "Background": 15,
        "Scenario": 15
      }
    }
  ]
}

use-and

What: Repeated same step keyword (Given, When, …) should use And / But instead.

{ "use-and": "on" }

Custom rules

Use -r / --rulesdir one or more times. Each path must be a directory (absolute or relative to cwd). The linter loads every *.ts file in that directory (not subfolders). Files are compiled at runtime with jiti; JavaScript rule files are not supported.

Each module must default-export a rule object:

import type { Feature } from '@cucumber/messages';

const name = 'my-rule';

export default {
  name,
  run(
    feature: Feature | undefined,
    file: { relativePath: string; lines: string[] },
    config: unknown
  ) {
    return [{ message: '…', rule: name, line: 1 }];
  },
  availableConfigs: [] as const,
};
  • name — must match the key in .gherkin-lintrc.
  • run — returns { message, rule, line }[], or undefined / [] if OK. config is the second part of ["on", …] when present, else {}.
  • availableConfigs[] if only "on"/"off"; otherwise a string array (allowed string options) or an object whose keys are allowed option names.

If two files define the same name, the one loaded last wins.

Add @cucumber/messages (and optionally typescript) to your project for typing. See built-in rules under src/rules/ and the rulesdir fixture.

Development

| Script | Purpose | |--------|---------| | npm test | Run Vitest once | | npm run test:watch | Vitest watch mode | | npm run test:coverage | Tests + coverage (HTML & LCOV under coverage/) | | npm run build | Tests, then tsc | | npm start | Run node dist/main.js (after build) |

Limitations (current)

  • Output: only the stylish formatter; json / xunit are not implemented.

License

ISC