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

lintp-cli

v0.8.1

Published

File system linter with a custom DSL for directory structure validation

Readme

lintp - File System Linter with DSL

CI crates.io npm license: MIT

A powerful file system linter that validates directory structures and file naming conventions using a custom Domain-Specific Language (DSL) defined in YAML configuration files.

lintp demo: failing files are flagged with the exact rule condition that failed, then pass after renaming

Why lintp?

File-naming linters already exist — ls-lint is a good one, and if your conventions are purely "this extension uses this case style", it may be all you need. lintp is for the conventions a name-only check can't express, where a rule depends on what exists around a file:

lintp:
  custom-matchers:
    pascal-case: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
  config:
    # every component is PascalCase and has a test file sitting next to it
    .tsx:
      rule: 'pascal-case && in("${$BASENAME}.test.tsx", siblings("*.test.tsx"))'
    # longest suffix wins: test files match .test.tsx, not the component rule
    .test.tsx: 'matches($BASENAME, /^[A-Z][a-zA-Z0-9]*\.test$/)'

Rules are expressions in a small DSL with functions like siblings(), children(), find(), and exists(), so a rule can look at a file's context — not just its name.

Installation

npm ships a prebuilt binary for macOS, Linux, and Windows (x64/arm64) via optionalDependencies. On Windows ARM64, the x64 binary runs through Windows' built-in emulation layer. If no prebuilt package matches your platform, the launcher falls back to a checksum-verified binary from the GitHub release (the checksum guards download integrity, not tamper-proof supply-chain verification). The npm package is named lintp-cli (npm reserves the bare name) — the installed command is lintp.

# run without installing
npx lintp-cli

# or install globally — the command is `lintp`
npm install -g lintp-cli

From crates.io

cargo install lintp            # compiles with your Rust toolchain (1.85+)
cargo install lintp --locked   # exact tested dependency versions

From source

The project uses asdf to pin Node.js and Rust versions.

git clone https://github.com/narehart/lintp.git
cd lintp
asdf install          # required Node.js + Rust versions
cargo build --release
./target/release/lintp --help

Quick Start

Create lintp.yml in your project root. Define reusable patterns under custom-matchers, then assign a rule to each file type under config.

lintp:
  # define reusable patterns
  custom-matchers:
    kebab-case: "matches($BASENAME, /^[a-z0-9]+(?:-[a-z0-9]+)*$/)"
    PascalCase: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
    js-file: '$EXT == "js"'
    ts-file: '$EXT == "ts"'

  # rules per file type
  config:
    .js: "kebab-case && js-file"
    .ts: "PascalCase && ts-file"
    .dir: "kebab-case || PascalCase"

  ignore:
    - node_modules
    - .git
    - dist

Run it. Every file and directory is checked against the longest-matching suffix rule; failures name the exact condition that failed.

$ lintp
✓ ./lintp.yml
✓ ./tests
✓ ./tests/user-tests.js
✓ ./src
✗ ./src/badFile.js - .js - Does not match rule: kebab-case && js-file (failed: kebab-case)
✓ ./src/UserManager.ts
✓ ./src/utils.js
Some files or directories do not match the configured rules.

Rules combine variables ($NAME, $EXT…), operators (&&, ||, !, ==…), functions (matches, contains, startsWith…) and collections (siblings, children, find). The complete language lives in the dsl-reference; reusable recipes in common-patterns.

Built-in Variables

Every DSL expression has access to the file being checked:

$NAME      # full filename incl. extension   "index.test.js"
$BASENAME  # filename without extension      "index.test"
$EXT       # extension without the dot       "js"
$PATH      # full file path                  "./src/index.test.js"
$PARENT    # parent directory path           "./src"
$item      # current item inside any(), all(), map(), filter()
lintp:
  custom-matchers:
    js-file: '$EXT == "js"'
    in-src: 'contains($PATH, "/src/")'
    has-js-sibling: 'any(siblings("*"), endsWith($item, ".js"))'

Configuration

Rule keys are suffix patterns, not just extensions: a file matches every key its path ends with, and the longest matching suffix wins. Button.test.tsx matches both .tsx and .test.tsx, and the .test.tsx rule applies. .* applies only when no other key matches; .dir targets directories.

A key can group several suffixes with brace alternation — each expansion gets the same rule and message:

lintp:
  custom-matchers:
    camelCase: "matches($BASENAME, /^[a-z][a-zA-Z0-9]*$/)"
  config:
    ".{png,jpg,jpeg,gif,webp,svg}":
      rule: "camelCase"
      message: "image files are camelCase"

Suffix matching has one subtlety with dotfiles: a file literally named .rules also matches a .rules: key, but as a dotfile its $EXT is "" and its $BASENAME is the full dotted name. Write $EXT == "rules" when a rule should apply only to real .rules extensions — or use the behavior deliberately: .gitignore: is a valid key for targeting that exact file.

Directory-scoped rules

A top-level key that is a glob pattern holds its own suffix→rule map, applied only to matching paths — and it overrides the global rule for the same suffix there. Globs match the path relative to the linted directory, and * crosses /, so src/ui/* covers the whole subtree. When several scopes match the same path, the most specific (longest pattern) wins: src/ui/* beats src/* for files under src/ui/. Braces expand in scope keys too: "api/{auth,billing}/*" is two scopes sharing one rule map.

lintp:
  config:
    .ts: "false" # default-deny: a .ts file must live in a listed location
    "src/ecs/systems/*":
      .ts:
        rule: 'matches($BASENAME, /^[a-z][a-zA-Z0-9]*System$/) && exists("${$BASENAME}.test.ts")'
        message: "systems are camelCase, end in System, and need a sibling test"
    "src/hooks/*":
      .ts:
        rule: "matches($BASENAME, /^use[A-Z][a-zA-Z0-9]*$/)"
        message: "hooks are named useX"

Prefer this over one global rule that chains ($PARENT == "./src/x" && ...) || ... branches: each location gets its own message, and failures point at the one rule that applies instead of printing the whole chain.

lintp:
  custom-matchers: # reusable pattern definitions
    kebab-case: "matches($BASENAME, /^[a-z0-9]+(?:-[a-z0-9]+)*$/)"
    pascal-case: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
  config: # suffix pattern → rule
    .test.js: 'matches($BASENAME, /^[a-z0-9-]+\.test$/)'
    .js: "kebab-case"
    .dir: "kebab-case || pascal-case"
    .*: '!contains($NAME, " ")'
  ignore: # glob patterns to skip
    - node_modules
    - "build/**"
    - "*.tmp"

Custom failure messages

Any rule can be a map with a message that replaces the raw expression in failure output — point teammates at your conventions doc instead of a regex.

lintp:
  custom-matchers:
    component-file: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
  config:
    .tsx:
      rule: "component-file"
      message: "Components must be PascalCase (see CONTRIBUTING.md)"
✗ ./src/badName.tsx - .tsx - Components must be PascalCase (see CONTRIBUTING.md)

DSL at a Glance

The built-in functions (full documentation with examples in the DSL Reference):

| Function | Purpose | | -------------------------- | ------------------------------------------------- | | matches(s, /re/ or glob) | Test against a regex or glob pattern | | contains(s, sub) | String contains a substring | | startsWith(s, prefix) | String starts with prefix | | endsWith(s, suffix) | String ends with suffix | | without(s, suffix) | Remove a suffix if present | | count(s or list) | Length of a string (characters) or list | | in(item, list) | List membership | | any(list, expr) | True if any item satisfies expr (binds $item) | | all(list, expr) | True if all items satisfy expr (binds $item) | | map(list, expr) | Transform each item | | filter(list, expr) | Keep items satisfying expr | | siblings(glob) | Files in the same directory | | children(glob) | Files inside this directory | | find(path, glob) | Files matching glob under a path | | exists(glob[, min, max]) | Files matching glob exist (optionally bounded) |

CLI Reference

lintp                          # lint cwd with ./lintp.yml
lintp /path/to/project         # lint a specific directory
lintp --config custom.yml      # custom config file
lintp --verbose                # show every file checked

Exit code 0 when everything passes, 1 on any violation or configuration error — a one-line CI gate. When a rule is a chain of && conditions, the failing condition(s) are listed in the (failed: …) suffix so you don't have to bisect composed rules by hand.

Symlinks: lintp does not follow symlinks. A symlinked directory's name IS checked against .dir rules, but its contents are not traversed.

Best Practices

  • Start simple. One kebab-case matcher and two config keys; add complexity as conventions solidify.
  • Name matchers descriptively. react-component, not rule1.
  • Compose. Build base patterns (kebab-case, js-file), then combine them: "kebab-case && js-file".
  • Test your rules. Touch a good file and a bad file, run lintp --verbose, verify both outcomes.
  • Ignore aggressively. node_modules, build output, generated files — specific names beat broad wildcards for speed.
  • Keep checks local. siblings()/children() are cheap; find(".", "**/*") re-scans the project for every file.

Troubleshooting

✗ No config file found
  → create lintp.yml or pass --config path/to/config.yml

✗ Failed to parse config file: mapping values are not allowed in this context at line 3
  → quote DSL expressions:  rule: '$NAME == "test"'

✗ Failed to parse rule: kebab-case && js file
  → DSL syntax error; the matcher name is missing its hyphen

✗ Unknown matcher 'keba-case' referenced by rule '.js'
  → define the matcher under custom-matchers first (checked at startup)

✗ Circular reference detected: rule-a
  → matchers may not reference each other in a cycle

Debugging a rule: run with --verbose, read the (failed: …) suffix first, and test expressions in isolation with a minimal single-rule config.

Additional Resources

  • Docs site — this content plus the full reference, rendered
  • DSL Reference — complete language reference with all operators, functions, and variables
  • Common Patterns — reusable patterns for naming conventions and validation rules
  • Examples — real-world configuration examples for different project types

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass: cargo test
  5. Submit a pull request

See CONTRIBUTING.md for commit conventions, the release process, and the docs-site workflow.

License

MIT License - see LICENSE file for details.