eslint-plugin-ejs-templates
v0.3.0
Published
ESLint plugin for EJS (Embedded JavaScript) templates — processor and rules
Maintainers
Readme
eslint-plugin-ejs-templates
An ESLint plugin for EJS (Embedded JavaScript) templates.
EJS files are parsed by tree-sitter-embedded-template via web-tree-sitter, which provides accurate position information for all lint messages and autofixes.
Features
- EJS processor – extracts each EJS tag into its own virtual JS block so standard ESLint rules can inspect the embedded JavaScript
- Autofix support – all plugin rules are fixable; run
eslint --fixto automatically apply fixes ejs-templates/prefer-raw– flags<%= … %>and suggests<%- … %>ejs-templates/prefer-slurping-codeonly– flags<% … %>code tags that can be safely converted to<%_ … _%>ejs-templates/experimental-prefer-slurp-multiline– converts multiline<% … %>to<%_ … _%>ejs-templates/prefer-single-line-tags– collapses multiline EJS tags to single-line tagsejs-templates/format– normalizes spacing inside tags and multiline closing delimiter layoutejs-templates/slurp-newline– ensures<%_ … _%>tags are on their own lineejs-templates/indent– enforces brace-depth–based indentation on standalone<%_ … _%>tags
Installation
npm install --save-dev eslint eslint-plugin-ejs-templatesUsage
Add the plugin to your ESLint flat config (eslint.config.js):
import { defineConfig } from 'eslint/config';
import templates from 'eslint-plugin-ejs-templates';
import eslint from '@eslint/js';
export default defineConfig([
// Standard JS rules:
eslint.configs.recommended,
// Apply the EJS processor to all *.ejs files with no rules (opt-in below):
...templates.configs.base,
{
files: ['**/*.ejs'],
rules: {
// Enable EJS-specific rules (apply in this recommended order):
'ejs-templates/experimental-prefer-slurp-multiline': 'error',
'ejs-templates/prefer-slurping-codeonly': 'error',
'ejs-templates/prefer-single-line-tags': 'error',
'ejs-templates/slurp-newline': 'error',
'ejs-templates/indent': 'error',
'ejs-templates/prefer-raw': 'error',
'ejs-templates/format': 'error',
},
},
]);Or use configs.all to enable every rule in one step:
import { defineConfig } from 'eslint/config';
import templates from 'eslint-plugin-ejs-templates';
export default defineConfig([...templates.configs.all]);Then run ESLint as usual:
npx eslint "**/*.ejs"
# or auto-fix violations:
npx eslint --fix "**/*.ejs"Note on incompatible rules
The EJS processor lints each tag as a separate virtual JavaScript block.
no-undefdiagnostics are suppressed internally for*.ejsvirtual blocks, so you do not need to disableno-undefin your ESLint config.
Note on ESLint directives in EJS comments
You can use supported ESLint directive comments inside EJS comments:
<%# eslint-disable no-var %><%# eslint-enable no-var %><%# eslint-disable-next-line no-var %>Example:
<%# eslint-disable-next-line no-var %> <% var value = 1; %>Regular EJS comments that are not ESLint directives continue to be ignored.
Rules
Apply rules in the following order for best results:
experimental-prefer-slurp-multiline— convert multiline<% %>to<%_ %>firstprefer-slurping-codeonly— convert single-line<% %>to<%_ %>prefer-single-line-tags— collapse remaining multiline tagsslurp-newline— ensure slurp tags are on their own lineindent— enforce brace-depth indentationprefer-raw— prefer<%-over<%=format— apply final whitespace/layout normalization
ejs-templates/prefer-raw
Prefers <%- (raw / unescaped output) over <%= (HTML-escaped output).
<%= is meant for HTML output; when the value is not expected to be rendered
as HTML, prefer raw output with <%-.
| | |
| ----------- | -------------------------------------------- |
| Fixable | Yes — eslint --fix converts <%= to <%- |
<!-- ✗ violation -->
<%= value %>
<!-- ✓ fixed -->
<%- value %>ejs-templates/prefer-slurping-codeonly
Prefers <%_ … _%> (whitespace-slurping) over <% … %> for single-line code
tags that are logic-only (no direct output), whose content has balanced braces,
and does not open or close a brace block.
Use this for code-only control logic; blocks that generate output should keep their output-specific delimiters.
| | |
| ----------- | ------------------------------------------------------ |
| Fixable | Yes — eslint --fix converts <% … %> to <%_ … _%> |
<!-- ✗ violation -->
<% const cssClass = active ? 'active' : ''; %>
<!-- ✓ fixed -->
<%_ const cssClass = active ? 'active' : ''; _%>Tags that open or close brace depth are left unchanged:
<% if (condition) { %> ← not flagged (opens a block)
<% } %> ← not flagged (closes a block)ejs-templates/experimental-prefer-slurp-multiline
Converts multiline <% … %> tags to <%_ … _%>. Apply this rule before
prefer-single-line-tags so that multiline <% %> tags get their delimiters changed
before being collapsed.
| | |
| ----------- | ------------------------------------------------------------ |
| Fixable | Yes — eslint --fix changes <% to <%_ and %> to _%> |
<!-- ✗ violation -->
<%
if (condition) {
%>
<!-- ✓ fixed -->
<%_
if (condition) {
_%>ejs-templates/prefer-single-line-tags
Flags EJS tags whose content spans multiple lines. The autofix splits the content
into separate single-line tags — one tag per statement boundary (;, }, {).
Lines starting with . are joined to the preceding line (chained method calls).
Keeping tags single-line avoids visual confusion between template output text and EJS control flow, making template intent easier to scan.
| | |
| ----------- | -------------------------------------- |
| Fixable | Yes — eslint --fix collapses the tag |
Options:
{ mode: 'always' }(default) — always split by statement boundaries (;,{,}){ mode: 'braces' }— when braces are present, keep the content between{and}in a single tag without collapsing its inner lines; multiline tags without braces are left unchanged
// eslint.config.js
{
files: ['**/*.ejs'],
rules: {
'ejs-templates/prefer-single-line-tags': ['error', { mode: 'braces' }],
},
}<!-- ✗ violation: single phrase split across lines -->
<%_
if (generateSpringAuditor) {
_%>
<!-- ✓ fixed -->
<%_ if (generateSpringAuditor) { _%><!-- ✗ violation: multiple statements -->
<%_
const x = 1;
const y = 2;
_%>
<!-- ✓ fixed: one tag per statement -->
<%_ const x = 1; _%>
<%_ const y = 2; _%><!-- ✗ violation: block with body and close -->
<%_
if (x) {
doWork();
}
_%>
<!-- ✓ fixed: one tag per boundary -->
<%_ if (x) { _%>
<%_ doWork(); _%>
<%_ } _%>ejs-templates/slurp-newline
Ensures <%_ … _%> whitespace-slurping tags are on their own line. An inline
slurp tag will not eat the preceding whitespace as intended. Apply this rule
after prefer-slurping-* and before indent.
Because slurp tags remove the newline/whitespace before them, placing each tag on its own line (then indenting it) makes the template easier to read and reason about.
| | |
| ----------- | ----------------------------------------------------- |
| Fixable | Yes — eslint --fix inserts a newline before the tag |
<!-- ✗ violation: slurp tag is inline after other content -->
some text<%_ doWork(); _%>
<!-- ✓ fixed -->
some text
<%_ doWork(); _%>ejs-templates/format
Applies final formatting normalization to EJS tags:
- ensures a single space around trimmed content (
<% foo %>) - controls multiline closing delimiter style
| | |
| ----------- | ----------------------------------------------------- |
| Fixable | Yes — eslint --fix normalizes tag whitespace/layout |
Options:
{ multilineClose: 'new-line' }(default) — for originally multiline tags, move close delimiter to a new line aligned with opening tag indentation{ multilineClose: 'same-line' }— keep close delimiter on the same line as content after formatting
// eslint.config.js
{
files: ['**/*.ejs'],
rules: {
'ejs-templates/format': ['error', { multilineClose: 'new-line' }],
},
}<!-- input -->
<%_
doWork(); _%>
<!-- with multilineClose: 'new-line' (default) -->
<%_ doWork();
_%>
<!-- with multilineClose: 'same-line' -->
<%_ doWork(); _%>ejs-templates/indent
Enforces brace-depth–based indentation (two spaces per level) on standalone
<%_ … _%> tags.
Consistent indentation improves readability of nested template logic.
| | |
| ----------- | --------------------------------------------------- |
| Fixable | Yes — eslint --fix adjusts the leading whitespace |
<!-- ✗ violation: wrong indentation -->
<%_ if (show) { _%>
<%_ doWork(); _%>
<%_ } _%>
<!-- ✓ fixed -->
<%_ if (show) { _%>
<%_ doWork(); _%>
<%_ } _%>Supported EJS Delimiters
| Delimiter | Meaning |
| --------- | -------------------------------------- |
| <% | Code (no output) |
| <%= | Output (HTML-escaped) |
| <%- | Output (raw / unescaped) |
| <%_ | Code, trims preceding whitespace |
| <%# | Comment (no output) |
| %> | Standard closing delimiter |
| -%> | Closing delimiter, trims trailing \n |
| _%> | Closing delimiter, trims whitespace |
