@ynode/sqrl-lint
v1.7.0
Published
Tag-aware linter, formatter, and Prettier plugin for Squirrelly (.sqrl) templates
Maintainers
Readme
@ynode/sqrl-lint
Copyright (c) 2026 Michael Welter [email protected]
A dedicated linter and formatter for Squirrelly (.sqrl) templates, built specifically for the @ynode Fastify
ecosystem. It combines a tag-aware formatter with Squirrelly engine validation and targeted semantic rules based on
real template failures. Surrounding HTML, CSS, and JavaScript remain untouched.
Features
- Strict Formatting: Enforces consistent spacing for helpers (
{{@,{{#), base brackets ({{,}}), raw outputs ({{*), whitespace controls, execution tags ({{!), and block closures ({{/). - Semantic Guardrails: Catches bare logical OR expressions, dead
elseifspellings, output assignments, unsafe JSON rendering, invalid native branches, malformed templates, and optional project policies. - Engine Validation: Compiles each finalized template with Squirrelly so malformed JavaScript, comments, and helper structure fail during linting instead of at request time.
- Actionable Diagnostics: Emits stable rule IDs with one-based line and column locations in text and JSON reports.
- Read-Only Checks: Fails CI pipelines with exit code
1for formatting or semantic violations. - Quality of Life: Automatically ignores
node_modulesby default and presents beautiful, colorized error logs and success reports. - Conservative Auto-Repair:
--fixapplies formatting and semantics-preserving repairs while leaving judgment calls as diagnostics. - Fast-Glob Powered: Built-in
fast-globprocessing natively supports arbitrary inclusion and exclusion targeting.
Installation
npm install -D @ynode/sqrl-lintSupported runtimes are Node.js 20 at 20.19 or newer, Node.js 22 at 22.12 or newer, or Node.js 23+, matching the CLI's yargs runtime dependency.
Usage
You can use the linter either manually via npx or wire it directly into your package.json scripts block.
Check Formatting (Read-Only)
npx sqrl-lint "src/**/*.sqrl"If any files need formatting or contain semantic errors, diagnostics are logged to stderr and the process exits with
a non-zero code (see Exit Codes).
Auto-Fix Formatting
npx sqrl-lint "src/**/*.sqrl" --fixApplies formatting plus safe semantic repairs. Non-fixable findings, such as an assignment in an output tag, remain
diagnostics and still produce exit code 1 after fix mode.
JSON Reporting
npx sqrl-lint "src/**/*.sqrl" --report jsonIn file mode, this emits a machine-readable JSON summary to stdout, suitable for CI/log parsers. In --stdin mode,
formatted template content uses stdout, so the JSON report is written to stderr instead. Both modes use the same
summary schema: mode and success, aggregate file/error counts, concurrency and duration, plus a results array with
per-file statuses and diagnostics. Each diagnostic includes ruleId, severity, message, index, line, column,
and fixable. Check-mode locations refer to the invocation's original input. After --fix writes safe repairs,
unresolved diagnostics are recalculated against the finalized file or stdout content.
Disable ANSI Colors
npx sqrl-lint "src/**/*.sqrl" --no-colorDisables ANSI color styling in text output.
Diffs (Check Mode)
npx sqrl-lint "src/**/*.sqrl" --diffUnified diffs are enabled by default for each file that needs formatting, making CI failures actionable. Use
--no-diff to suppress them.
Ignore Additional Files
npx sqrl-lint "src/**/*.sqrl" --ignore "src/vendor/**" --ignore "**/*.generated.sqrl"Adds one or more glob patterns to the built-in ignores. If no input files match after ignores are applied, the command
reports an operational error and exits with code 2.
Parallel Processing
npx sqrl-lint "src/**/*.sqrl" --fix --concurrency 4Processes files with bounded parallelism for faster runs on large repositories.
Stdin / Editor Integration
cat src/views/home.sqrl | npx sqrl-lint --stdin --fixReads template content from stdin and writes the formatted output to stdout, making it ideal for editor "format on save"
integrations, shell pipelines, and git hooks. Use --stdin-filepath <path> to control the filename shown in error
messages and diffs.
Quiet Mode
npx sqrl-lint "src/**/*.sqrl" --quietSuppresses reports and diagnostics, including JSON reports; only the exit code indicates the result. In --stdin mode,
the formatted template content remains on stdout because it is the command's data output.
Version
npx sqrl-lint --versionPrints the installed package version and exits.
Exit Codes
| Code | Meaning |
| ---- | --------------------------------------------------------------------- |
| 0 | All files are formatted and no semantic lint errors remain |
| 1 | One or more files need formatting or have semantic lint errors |
| 2 | Operational error (I/O failure, invalid arguments, permission denied) |
Formatting Rules
The linter enforces consistent spacing inside Squirrelly tag boundaries. Rules are applied in order; the first match wins.
| Tag Type | Before | After |
| ------------------- | --------------------- | ----------------------- |
| Helper / Macro open | {{@extends()}} | {{@ extends() }} |
| Native branch | {{#elif(user)}} | {{# elif(user) }} |
| Self-closing helper | {{@partial("x")/}} | {{@ partial("x") /}} |
| Execution | {{!it.ready=true;}} | {{! it.ready=true; }} |
| Block close | {{/if}} | {{/ if }} |
| Expression | {{name}} | {{ name }} |
| Raw output | {{*rawHtml}} | {{* rawHtml }} |
| Whitespace controls | {{-name-}} | {{- name -}} |
Content outside {{ ... }} boundaries (HTML, CSS, JS) is never modified.
Semantic Rules
The default rules favor low-noise failures and semantics-preserving fixes.
| Rule ID | Behavior | Purpose |
| ----------------------------------- | -------- | --------------------------------------------------------------------------------------------- |
| no-unparenthesized-logical-or | Fix | Protects top-level \|\| with parentheses or the configured nullish rewrite |
| valid-elif | Fix | Rewrites else if(...), elseif(...), and elf(...) to Squirrelly's recognized elif(...) |
| valid-squirrelly-syntax | Report | Reports unclosed tags and failures from Squirrelly.compile() |
| valid-native-branch | Report | Rejects orphaned, duplicate, missing-condition, or out-of-order native branches |
| valid-filter | Report | Rejects empty or malformed filter segments |
| valid-async-syntax | Report | Requires async compilation when a helper, block, or filter uses the async modifier |
| no-ambiguous-leading-prefix | Mixed | Disambiguates leading regex output and rejects whitespace-obscured execution prefixes |
| no-output-assignment | Report | Prevents assignment results from leaking into rendered HTML |
| no-unsafe-raw-json | Report | Rejects raw JSON.stringify(...) and configured unsafe serializer output |
| known-filter | Opt-in | Reports filter names absent from the project's configured registry |
| no-implicit-null-output | Opt-in | Adds ?? "" to a bare optional-chain output expression |
| no-execute-tag / no-safe-filter | Opt-in | Restricts execution or safe filters on sensitive template surfaces |
For example, this is not valid JavaScript from Squirrelly's parser's point of view:
{{ it.name || "Unknown" }}Squirrelly sees each top-level | as a filter separator. The safe automatic repair preserves the author's JavaScript
semantics:
{{ (it.name || "Unknown") }}That semantics-preserving repair is the default. Projects that intentionally want nullish fallback semantics can set
logicalOrFix to "nullish":
{{ it.name ?? "Unknown" }}Nullish mode rewrites each exposed JavaScript logical-OR operator, while distinguishing it from regex text, comments,
and ||=. It also adds the parentheses JavaScript requires when ?? is combined with &&. Already-parenthesized
logical OR remains untouched because it is valid, intentional JavaScript. Opting in changes behavior for "", 0, and
false: those values no longer use the fallback.
Assignments are reported without an automatic rewrite because rendering the assigned value could theoretically be intentional:
{{ it.page = "dashboard" }}For a side effect that should not render, use Squirrelly's JavaScript execution prefix:
{{! it.page = "dashboard"; }}Prettier Integration
The package ships a Prettier plugin so you can format .sqrl files alongside the rest of your codebase. Install
Prettier alongside this package:
npm install -D prettier @ynode/sqrl-lintThen add the plugin to your Prettier configuration:
{
"plugins": ["@ynode/sqrl-lint/prettier"]
}When the project uses nullish fixes, configure Prettier consistently so it does not apply the default parenthesizing repair before the CLI runs:
{
"plugins": ["@ynode/sqrl-lint/prettier"],
"sqrlLogicalOrFix": "nullish"
}Once configured, prettier --write "**/*.sqrl" applies the CLI's tag spacing and default safe repairs. Prettier does
not report non-fixable diagnostics or load .sqrl-lintrc.json; keep the CLI in CI as the semantic enforcement gate.
Programmatic API
import { lintContent } from "@ynode/sqrl-lint";
const result = lintContent('{{ it.name || "Unknown" }}', {
logicalOrFix: "nullish",
});
console.log(result.content);
// {{ it.name ?? "Unknown" }}
console.log(result.diagnostics);lintContent(source, options) returns { changed, content, diagnostics }. Library callers pass LintOptions directly;
the exported DEFAULT_LINT_OPTIONS documents the defaults. The JSON config file and --config flag belong to the CLI,
while the Prettier plugin has its own sqrlLogicalOrFix option and otherwise uses linter defaults. It does not load
.sqrl-lintrc.json.
Lint Configuration
For the CLI, place an optional .sqrl-lintrc.json in the working directory, or pass an explicit file with --config:
npx sqrl-lint "src/**/*.sqrl" --config config/sqrl-lint.json{
"compile": true,
"async": false,
"logicalOrFix": "nullish",
"knownFilters": ["date", "dateInput", "fixed", "json", "scriptJson", "timeInput"],
"unsafeRawFilters": ["json"],
"noImplicitNullOutput": false,
"forbidExecute": false,
"forbidSafe": false
}knownFiltersenables a complete registry check. Squirrelly's built-inefilter and non-asyncsaferaw-output marker are always accepted; anasync safecallable must be explicitly registered.logicalOrFixaccepts"parenthesize"(the semantics-preserving default) or"nullish"to replace exposed||fallbacks with??. Set Prettier'ssqrlLogicalOrFixto the same strategy when using the plugin.unsafeRawFiltersidentifies serializers that must not be emitted through{{* ... }}or a chain containingsafe.noImplicitNullOutputsafely adds an empty-string fallback to simple optional-chain output expressions.forbidExecuteandforbidSafesupport restricted surfaces such as templates compiled into browser JavaScript. Use a separate explicit config when only a targeted glob needs these policies.compiledefaults totrue; disable it only for projects that intentionally use nonstandard syntax the installed Squirrelly engine cannot compile.asyncenables Squirrelly's async-template compilation mode for templates that legitimately containawait.
Configuration is strict: misspelled keys, invalid types, empty filter names, and duplicates are operational errors with
exit code 2.
Package Script Integration
Because this is a standard ecosystem plugin, you can easily wire it into your @ynode lint:guardrails group alongside
CSS and HTML linting:
"scripts": {
"lint:sqrl:format": "sqrl-lint \"src/**/*.sqrl\"",
"lint:sqrl:format:fix": "sqrl-lint \"src/**/*.sqrl\" --fix",
"lint:guardrails": "npm run lint:css && npm run lint:sqrl:format"
}Known Limitations
Literal Opening Double-Braces and Custom Delimiters
The tag-aware scanner treats every {{ sequence as the start of a Squirrelly tag and does not support custom tag
delimiters. If a template needs to emit a literal opening double-brace or embed foreign Vue.js or Handlebars syntax,
move the content to a partial that the linter does not process or exclude it with --ignore. Disabling engine
compilation does not disable the tag scanner. Leading regex expressions are parenthesized when that is unambiguous;
delimiter-containing or irreducibly ambiguous regex/block-close sequences are reported and left unchanged.
