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

@maxonfjvipon/xslint

v0.6.0

Published

XSL Linter

Readme

xslint

Lint your XSL/XSLT stylesheets — catch malformed XML, invalid XPath, and stylistic defects before they ship.

DevOps By Rultor.com

npm grunt codecov PDD status Hits-of-Code License

xslint is a CLI linter for XSL stylesheets. It first checks that every stylesheet is well-formed and every XPath expression compiles, then runs its checks for stylistic, semantic, and logical problems — each reported with its exact line and column, in your terminal or in CI.

Quick start

Run it on your stylesheets — no install needed:

npx @maxonfjvipon/[email protected] path/to/stylesheets

Given a stylesheet like this:

<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="urn:my">
  <xsl:function name="my:upper">
    <xsl:param name="text"/>
    <xsl:sequence select="upper-case($text)"/>
  </xsl:function>
  <xsl:template match="//book">
    <xsl:variable name="x" select="title"/>
    <xsl:value-of select="$x"/>
  </xsl:template>
</xsl:stylesheet>

xslint points at each problem with its exact position and how to fix it:

[WARNING] sheet.xsl(3:3) A stylesheet function is never called in any expression across the corpus. Remove it or call it. (unused-function)

A run reports the recommended preset unless told otherwise: what a processor refuses, and the dead code whose report is almost never wrong. Ask for the whole catalog, style checks and unused variables included, with --preset all:

[WARNING] sheet.xsl(2:1) The stylesheet element has no @id attribute. Declare it to specify the unique identifier explicitly. (missing-id-in-stylesheet)
[WARNING] sheet.xsl(3:3) A stylesheet function is never called in any expression across the corpus. Remove it or call it. (unused-function)
[WARNING] sheet.xsl(7:24) A pattern alternative opens with //, which at most demands a document-node root. Drop it, and set an explicit priority if the template must rank as before. (starts-with-double-slash)
[WARNING] sheet.xsl(8:5) A variable, parameter, function, or template has a single-character name. Use a descriptive name that reveals intent. (short-names)

In CI, use the GitHub Action to get inline annotations on your pull requests:

- uses: actions/checkout@v6
- uses: xslint/[email protected]

Or run it on commit with pre-commit — add to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/xslint/xslint
    rev: 0.4.0
    hooks:
      - id: xslint

Browse the full check catalog.

Proven on real code

Pointed at core stylesheets from the three most widely-used XSLT projects — DocBook-XSL (1.0), TEI (2.0), and DITA-OT (1.0/2.0) — and run with --preset all, xslint surfaced 11,161 findings across 45 different checks in 867 stylesheets, with no false positives from its validators: 3,279 pieces of literal text outside xsl:text, 639 xsl:choose blocks with no xsl:otherwise, and 583 template and function parameters nothing reads. Real stylistic and logical findings in code that has shipped for decades. The recommended preset a run reports by default draws 245 of them.

Every figure above is read off the reports committed under test/resources/corpora/, which a nightly job re-lints at the pinned commits and diffs line for line — so a number here is one the tree still draws, and the build fails while it is not.

Installation

To install xslint globally, install npm first, then run:

npm install -g @maxonfjvipon/[email protected]
xslint --version

Build

To build xslint from source, clone this repository:

git clone [email protected]:xslint/xslint.git
cd xslint

Next, run these commands to install xslint system-wide:

npm install
npm install -g .

Use Node 22, the release CI runs on. Babel 8, which the mutation tester pulls in, declares itself for 22.18 and 24.11 upward only, so on any other release npm install prints an EBADENGINE warning per Babel package and then installs regardless.

Verify that xslint is installed correctly:

$ xslint --version
0.0.0

Usage

You can check all files in current directory:

xslint

To check specified files - provide them as arguments:

xslint path/to/your/file1.xsl path/to/your/file2.xslt

Either spelling of the name is read, .xsl and .xslt. A directory is walked for both and everything else in it is stepped over, while a file named on the command line under any other suffix earns a warning rather than being counted as clean. A .git or a node_modules is never opened, wherever in the tree it stands, and neither is a directory the project's own .gitignore files name — a tree the project does not track is not its source. A file git tracks is read whatever a line says of it, the way git itself reads one, and a path given on the command line is read whatever those files say about it.

A run reports the checks of one preset. recommended, the default, holds what a processor refuses and the dead code whose report is almost never wrong; all holds every check in the catalog, style checks and unused variables included. The check catalog marks the preset each check belongs to:

xslint --preset all

You can suppress some checks by using --suppress option:

xslint --suppress=confusing-variable-and-node

You can skip several checks at once if they contain a certain substring:

xslint --suppress=unused

If you want to suppress many checks, use --suppress as many times as you need:

xslint --suppress=oversized-template --suppress=short-names

To ask one question of a whole tree, run only the checks you name with --only. It matches by substring the way --suppress does, it may be given as many times as you need, and it reaches any check in the catalog whatever the preset:

xslint --only=short-names --only=unused

The two combine, and a suppression always wins: a check both of them name stays quiet, so this runs every unused-* check but unused-variable:

xslint --only=unused --suppress=unused-variable

Configuration

Project-wide settings live in a .xslint.yml file, discovered by walking up from the current directory (or passed with --config <path>). Command-line flags override the file, and the file overrides the built-in defaults.

# .xslint.yml
preset: all              # default for --preset: recommended or all
rules:
  short-names: off       # turn one check off
  "unused-*": error      # or a family, by glob
exclude:
  - "test/**"                           # globs to skip, relative to this file
only:
  - "unused"                            # default for --only
max-warnings: 10                        # default for --max-warnings
log-level: info                         # default for --log-level
quiet: false                            # default for --quiet
  • preset names the preset a run starts from, recommended unless it says all. Passing --preset replaces it.
  • rules maps a check name — or a glob such as unused-* — to off, warning, or error. off disables the check (like --suppress); warning and error re-grade its severity. A check named exactly also runs where the preset leaves it out, which is how one style check joins a recommended run; a glob re-grades only the checks already in the run, so "*": warning or "unused-*": error adds none.
  • exclude lists globs, relative to the config file's own directory, whose matching files are not linted. A pattern covering everything under a directory — dir/** — also stops the walk descending it, so an exclusion costs nothing rather than the walk it then throws away. A wildcard here reads a name opening with a dot like any other, so dir/** covers a dir/.hidden/sheet.xsl as much as the rest of what stands under dir.
  • only lists the substrings --only would take, narrowing every run to the checks they name. Passing --only replaces this list rather than adding to it, and a check rules turns off stays off whichever of the two chose it.
  • max-warnings, log-level, and quiet set the defaults for the matching command-line flags.

Unknown top-level keys, rule names that match no check, and values of the wrong type (a non-numeric max-warnings, a non-list exclude or only, a non-boolean quiet, a non-string log-level or preset) are reported and ignored, so typos do not pass silently. A preset that does not exist fails the run instead, before a file is read, since a run over no checks would read as a clean report. An exclude glob is named the same way when a run walks a directory and the glob excludes nothing anywhere under it.

Inline suppression

Silence a rule in one place with an XML-comment directive. Rule names are optional and space-separated; with none, every rule at that location is suppressed.

<!-- xslint-disable-next-line short-names -->
<xsl:variable name="x" select="1"/>

<!-- xslint-disable-file not-using-schema-types -->
  • xslint-disable-next-line [rules] — the line after the comment.
  • xslint-disable-line [rules] — the comment's own line.
  • xslint-disable-file [rules] — the whole file (put it near the top).

A directive that suppresses nothing is reported as unused, so stale ones can be found and removed. A run narrowed by its preset, --only or --suppress judges only the directives whose rules it ran, since a rule it skipped reports nothing for a directive to cover.

An expression written across several lines is one value, and a directive that reaches any line of it silences every defect in it. Nothing inside a start tag can carry a comment of its own, so a directive above the element is the only way to reach a wrapped @test or @select at all — the cost is that it cannot pick out one defect in such a value and leave its neighbours reported.

Output

Defects are written to stdout; progress and diagnostic logs go to stderr, so xslint path/to/dir > report.txt captures only the findings. Pass --quiet to drop the informational log lines:

xslint --quiet

Output is colored only when it goes to an interactive terminal, so a redirected or piped run stays plain text; setting the conventional NO_COLOR environment variable turns coloring off everywhere.

Machine-readable output

--format selects the output. text (the default) is the human format above; json and sarif print a single document to stdout — logs stay on stderr, so the document is clean to pipe or redirect; github prints GitHub Actions workflow commands:

xslint --format json path/to/dir     # a flat array of defects
xslint --format sarif path/to/dir    # a SARIF 2.1.0 log
xslint --format github path/to/dir   # ::warning/::error annotations for CI

Each entry of the json array names the check as rule and carries its severity, its message, and the file, line and column the defect stands at. A fixable one carries a fix beside them, holding that span's own line and column, the value it replaces, the replacement it would write, and whether it is a suggestion. Defects come out ordered by file, then line, then column, then rule — so two runs over one tree emit the same document, and a report committed today diffs against one taken tomorrow.

Inside a GitHub Action, --format github makes each defect an inline annotation on the pull-request diff with no upload step — the lowest-friction way to see findings on a review.

SARIF feeds GitHub code scanning, so xslint findings appear as annotations on pull requests:

- run: xslint --format sarif . > xslint.sarif || true
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: xslint.sarif

The || true keeps a findings exit code from failing the step before the upload; the alerts still surface in code scanning. Run it from the repository root so the reported paths stay repo-relative — a file outside the working directory is named by its absolute path instead.

Fixing

--fix applies the corrections that are deterministic and leave the stylesheet meaning what it meant. --fix-suggestions applies those and the ones that change behavior, remove code, or are one of several reasonable corrections, which a plain --fix never touches. A correction needing real judgment — a fresh name, a more specific path — stays report-only, and a run without either flag reports how many defects each option would fix.

xslint --fix path/to/dir
xslint --fix-suggestions path/to/dir

Only the exact span that was flagged is rewritten — the rest of the file is left byte-for-byte intact — and a fix is skipped rather than applied when the source no longer matches what it expects. Where two corrections cover the same piece of an expression — the redundant whitespace in d[position() = 1] sits inside the predicate that becomes d[1] — only the wider one is applied, and the other is announced as skipped. Run --fix again to take care of whatever the first run left.

Which corrections a check offers, in which of those two tiers, and why the construct is worth correcting at all, is on that check's own page in the check catalog.

An expression xslint cannot parse draws one defect, from the validator, and the checks that read expressions say nothing further about it: a stylesheet whose real fault is a missing bracket comes back with that fault and not with a page of advice about text no processor accepts. A rule that matches the attribute structurally rather than reading the expression can still report there, and is never offered a correction. Fix the syntax and the rest of the feedback appears on the next run.

Where the expression sits no longer decides whether you are told why. A bare one — a select, a test, and the rest listed under invalid-xpath-expression — a pattern such as match, and each expression the braces of an attribute value template, a text value template or a shadow attribute enclose are all parsed, and whichever of them is broken is reported as malformed. The report points at the character the parser stopped on rather than at the attribute holding it, so a fault buried in a long expression, or in the second of two {...} on one line, names its own column.

Pass --fix-dry-run to see what would remain after fixing, without writing any file:

xslint --fix-dry-run path/to/dir

Exit code

xslint exits non-zero when any error-severity defect is found. Warnings do not fail the run by default; to make them count, cap the allowed number with --max-warnings:

xslint --max-warnings=0    # any warning fails the run
xslint --max-warnings=10   # more than ten warnings fails the run

Checks

The full list of checks with descriptions and examples is available at xslint.github.io/xslint.

xslint runs in two stages. Validators first establish that the input is valid; linters then run over the stylesheets that pass, catching stylistic, semantic, and logical problems. A stylesheet that does not parse is reported once and skipped, so one broken file never hides the feedback on the rest.

Validators:

  • XML well-formedness — a stylesheet that is not well-formed XML, or that spells a namespace prefix it never declares, is reported at the line and column the parser stopped on and excluded from linting.
  • XPath syntax — every bare XPath expression (in select, test, use, value, group-by, group-adjacent, and the XSLT 3.0 key, initial-value, xpath, context-item, with-params, namespace-context, for-each-item, for-each-source and use-when) is parsed, on an XSLT element or — spelled xsl:use-when, the only spelling a simplified stylesheet has — on a literal result element; the ones the processor cannot parse are reported.

Linters:

  • Per-file checks evaluate one stylesheet at a time (most checks).
  • Cross-file checks reason across all the stylesheets you lint together. For example, a named template defined in one file but invoked from another (via xsl:import/xsl:include) is not reported as unused, an xsl:import/xsl:include cycle across files is flagged (circular-import), and the same module imported twice in one stylesheet is flagged (redundant-import). Lint the whole project at once so these checks can see every caller and every imported module.
  • Formatting checks are written in code rather than as a declarative selector — their YAML tunes only severity and message. Most read the parse tree of one expression; the rest read the document or the import graph. The kind holds 26 checks today, and the check catalog teaches every one of them.

Every check that reads an expression reads it from an XPath or pattern attribute of an XSLT element (select, test, match, …) or from an attribute value template — <div class="{count(item) = 0}"/> is checked, and fixed, inside the braces. An attribute of your own output vocabulary that happens to share a name with an XSLT one, as in <widget test="count(item) = 0"/>, holds text destined for the result tree, so it is never read as XPath and never rewritten. And each of them is handed the expressions the validator kept, so a malformed one is reported once rather than nagged about its spacing, its axes and its count(...) calls on top of that.

Programmatic use

xslint is embeddable — editors, build tools, and the language server import it instead of shelling out. lint takes in-memory sources and returns the defects, touching no files and never exiting:

const {lint, fixed} = require('@maxonfjvipon/xslint')

const sources = [{file: 'sheet.xsl', content: '<xsl:stylesheet .../>'}]
const defects = lint(sources, {suppress: ['short-names']})
// each defect: {name, severity, message, file, line, pos, fix?}

// apply the fixable ones without writing to disk:
const {contents} = fixed(sources, defects)

lint(sources, {suppress, overrides, only, preset}) runs the validators and linters of a preset, recommended unless named, over the {file, content} sources, honors inline xslint-disable directives, runs every check overrides names beside the preset, and hands the defects back in the order the reports print them — file, line, column, rule; fixed(sources, defects, suggestions) returns the rewritten content per file.

settingsOf(dir, flags) reads the .xslint.yml nearest to dir the way the command line does, flags taking config, preset, only and suppress over it, and answers options to hand straight to lint, plus excluded(path), whether exclude: keeps the stylesheet at that absolute path out, file, the absolute path of the configuration it read (undefined when none), base, the directory its globs resolve against, and problems, one sentence per unknown key, mistyped value or rule naming no check. It prints nothing, and throws rather than answering a problem where the preset names no check list or the file is not YAML at all, as the command line fails on both:

const {lint, settingsOf} = require('@maxonfjvipon/xslint')

const settings = settingsOf('/path/to/project')
const defects = lint(
  sources.filter((source) => !settings.excluded(source.file)), settings,
)

stylesheetsOf(paths, settings) answers {stylesheets, problems}: the absolute paths of the stylesheets a run over paths reads, found the way the command line finds them — both suffixes, what .gitignore and exclude: keep out — and one sentence per warning it prints on the way, logging nothing above the debug level itself. A relative path resolves against the working directory of the process, not against settings.base, so pass absolute ones. sourceOf(file, content) answers the source lint takes for that content, reading the parameter entities it declares and the hrefs it writes that no file stands behind relative to file, so a buffer nobody saved lints as the file would:

const fs = require('fs')
const {lint, settingsOf, stylesheetsOf, sourceOf} = require('@maxonfjvipon/xslint')

const settings = settingsOf('/path/to/project')
const {stylesheets} = stylesheetsOf(['/path/to/project'], settings)
const defects = lint(
  stylesheets.map((file) => sourceOf(file, fs.readFileSync(file, 'utf-8'))),
  settings,
)

Editors

xslint runs inside your editor through the xslint-lsp language server, with the same diagnostics and quick-fixes as the CLI:

  • VS Code, Cursor, VSCodium, Windsurf, Gitpod — install the extension from Open VSX, or the .vsix attached to each release.
  • IntelliJ IDEA, WebStorm, PyCharm, and other JetBrains IDEs — install the xslint-jetbrains plugin.

How to Contribute

Fork repository, make changes, then send us a pull request. We will review your changes and apply them to the master branch shortly, provided they don't violate our quality standards. To avoid frustration, before sending us your pull request please make sure all your tests pass:

npm test

Most of those seconds go to the *.deep.test.js files, which run the command-line tool in a child process. While you are still working, run the rest of the suite on its own — it holds most of the tests and starts no process:

npm run fast

A test you add belongs on the side it costs: name it *.deep.test.js when it runs xslint or xcop in a child process — which it does by requiring test/helpers.js — and plain *.test.js when it stays in this one. test/conformance.test.js checks that both ways round, so a misnamed file turns the build red rather than quietly slowing the fast half down.

New linter rules live in src/resources/checks/xpath (per-file) or src/resources/checks/corpus (cross-file), each with a matching test pack in test/resources. The validators in src/resources/checks/validation and the formatting checks in src/resources/checks/format are fixed in code; their YAML only tunes severity and message. That YAML is where a check is written, but a run reads src/resources/checks.json, so rebuild and commit it whenever you touch one:

npx grunt checks

Forgetting is not a silent mistake — the test suite re-renders the file from the YAML and fails on any difference. Regenerate the documentation site with npx grunt docs.

You will need npm and node installed

See CONTRIBUTING.md for the full workflow and CHANGELOG.md for release notes.