Skip to content
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
//////////////////////////////////////
"git.inputValidation": "warn",
"git.inputValidationSubjectLength": 50,
"git.inputValidationLength": 72
"git.inputValidationLength": 72,
"cSpell.words": [
"lintable"
]
}
24 changes: 21 additions & 3 deletions DEV.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
# Some developer guidelines

## Install as npm package locally to test

Also see [this answer](https://stackoverflow.com/a/28392481/9655481). Basically, inside another project where you want to test the plugin, you can install it as an npm package via a local path:

```bash
npm link /path/to/eslint-plugin-erb
```

Other useful commands:

```bash
npm ls --global
```

Finally to remove the link:

```bash
npm unlink eslint-plugin-erb
```

## Merge strategies

- Feature branches to `dev`: squash commit
- Continuous Release from `dev` to `main`: standard merge commit
- Hotfixes: branch off `main`, merge PR into `main` via squash commit, then merge back `main` to `dev` via standard merge commit.


## Create a new release (and publish to npm)

As this is only a small project, we haven't automated publishing to the NPM registry yet and instead rely on the following manual workflow.

- Make sure the tests pass locally: `npm test`
- Make another commit on the `dev` branch bumping the npm version in the `package.json`. For that, use:

```sh
npm run bump-version -- [<newversion> | major | minor | patch]
```

- ⚠ Copy the version specifier from `package.json` into the `index.js` meta information object.
- Once the `dev` branch is ready, open a PR (Pull request) called "Continuous Release <version.number>" and give it the "release" label. Merge this PR into `main`.
- Create a new release via the GitHub UI and assign a new tag alongside that.
- Fetch the tag locally (`git fetch`) and publish to npm via `npm run publish-final`. You probably have to login to npm first (`npm login`).
- Enjoy ✌ Check that the release is available [here on npm](https://www.npmjs.com/package/eslint-plugin-erb).


53 changes: 32 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

**Lint your JavaScript code inside ERB files (`.js.erb`).**
A zero-dependency plugin for [ESLint](https://eslint.org/).
<br>Also lints your **HTML code** in `.html.erb` if you want to.

![showcase-erb-lint-gif](https://github.com/Splines/eslint-plugin-erb/assets/37160523/623d6007-b4f5-41ce-be76-5bc0208ed636?raw=true)


> **Warning**
> v2.0.0 is breaking. We use the new ESLint flat config format. Use `erb:recommended-legacy` if you want to keep using the old `.eslintrc.js` format.

Expand All @@ -19,19 +19,15 @@ Install the plugin alongside [ESLint](https://eslint.org/docs/latest/use/getting
npm install --save-dev eslint eslint-plugin-erb
```


### Configure

Starting of v9 ESLint provides a [new flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new) (`eslint.config.js`). Also see the [configuration migration guide](https://eslint.org/docs/latest/use/configure/migration-guide). Use it as follows and it will automatically lint all your `.js.erb` files:
Starting of v9 ESLint provides a [new flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new) (`eslint.config.js`). Also see the [configuration migration guide](https://eslint.org/docs/latest/use/configure/migration-guide). Use it as follows and it will automatically lint all your **JavaScript code** in `.js.erb` files:

```js
// eslint.config.js
import erb from "eslint-plugin-erb";

export default [
// if you are using VSCode, don't forget to put
// "eslint.experimental.useFlatConfig": true
// in your settings.json
erb.configs.recommended,
{
linterOptions: {
Expand All @@ -46,7 +42,6 @@ export default [
// your other configuration options
}
];

```

<details>
Expand Down Expand Up @@ -120,25 +115,20 @@ export default [

</details>


<details>

<summary>Alternative way to configure the processor</summary>

With this variant you have a bit more control over what is going on, e.g. you could name your files `.js.special-erb` and still lint them (if they contain JS and ERB syntax).


```js
// eslint.config.js
import erb from "eslint-plugin-erb";

export default [
// if you are using VSCode, don't forget to put
// "eslint.experimental.useFlatConfig": true
// in your settings.json
{
files: ["**/*.js.erb"],
processor: erb.processors.erbProcessor,
processor: erb.processors.processorJs,
},
{
linterOptions: {
Expand All @@ -157,10 +147,6 @@ export default [

</details>





<details>
<summary>Legacy: you can still use the old `.eslintrc.js` format</summary>

Expand All @@ -173,7 +159,7 @@ module.exports = {
};
```

Or you can configure the processor manually (advanced):
Or you can configure the processor manually:

```js
// .eslintrc.js
Expand All @@ -182,23 +168,47 @@ module.exports = {
overrides: [
{
files: ["**/*.js.erb"],
processor: "erb/erbProcessor"
processor: "erb/processorJs"
}
]
};
```

</details>

If you also want to lint **HTML code** in `.html.erb` files, you can use our preprocessor in conjunction with the amazing [`html-eslint`](https://html-eslint.org/) plugin. Install `html-eslint`, then add the following to your ESLint config file (flat config format):

```js
// eslint.config.js
import erb from "eslint-plugin-erb";

export default [
// your other configurations...
{
processor: erb.processors["processorHtml"],
...html.configs["flat/recommended"],
files: ["**/*.html", "**/*.html.erb"],
rules: {
...html.configs["flat/recommended"].rules,
"@html-eslint/indent": ["error", 2],
// other rules...
},
}
];
```

Additionally, you might want to add the following option to the other objects (`{}`) in `export default []` (at the same level like the `files` key above), since other rules might be incompatible with HTML files:

```js
ignores: ["**/*.html**"],
```

## Editor Integrations

The [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for VSCode has built-in support for the ERB processor once you've configured it in your `.eslintrc.js` file as shown above.

If you're using VSCode, you may find this `settings.json` options useful:

```jsonc
{
"editor.formatOnSave": false, // it still autosaves with the options below
Expand All @@ -208,7 +218,6 @@ If you're using VSCode, you may find this `settings.json` options useful:
// https://eslint.style/guide/faq#how-to-auto-format-on-save
// https://github.com/microsoft/vscode-eslint#settings-options
"eslint.format.enable": true,
"eslint.experimental.useFlatConfig": true, // use the new flat config format
"[javascript]": {
"editor.formatOnSave": false, // to avoid formatting twice (ESLint + VSCode)
"editor.defaultFormatter": "dbaeumer.vscode-eslint" // use ESLint plugin
Expand All @@ -230,8 +239,8 @@ If you're using VSCode, you may find this `settings.json` options useful:
}
```


## Limitations

- Does not account for code indentation inside `if/else` ERB statements, e.g.
this snippet

Expand All @@ -240,7 +249,9 @@ this snippet
console.log("You are lucky 🍀");
<% end %>
```

will be autofixed to

```js
<% if you_feel_lucky %>
console.log("You are lucky 🍀");
Expand Down
27 changes: 20 additions & 7 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
// Load processor
const preprocess = require("./preprocess.js");
const { preprocessJs, preprocessHtml } = require("./preprocess.js");
const postprocess = require("./postprocess.js");
const processor = {
preprocess,

// Load processors
const processorJs = {
meta: {
name: "processJs",
},
preprocess: preprocessJs,
postprocess,
supportsAutofix: true,
};
const processorHtml = {
meta: {
name: "processHtml",
},
preprocess: preprocessHtml,
postprocess,
supportsAutofix: true,
};
Expand All @@ -16,21 +28,22 @@ const plugin = {
configs: {
"recommended": {
files: ["**/*.js.erb"],
processor: processor,
processor: processorJs,
},
// for the old non-flat config ESLint API
"recommended-legacy": {
plugins: ["erb"],
overrides: [
{
files: ["**/*.js.erb"],
processor: "erb/erbProcessor",
processor: "erb/processorJs",
},
],
},
},
processors: {
erbProcessor: processor,
processorJs: processorJs,
processorHtml: processorHtml,
},
};

Expand Down
46 changes: 33 additions & 13 deletions lib/preprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ const { indexToColumn } = require("./file_coordinates.js");
// how annoying is that kind of import in JS ?!
var OffsetMap = require("./offset_map.js").OffsetMap;

const erbRegex = /<%[\s\S]*?%>/g;
const DUMMY_STR = "/* eslint-disable */{}/* eslint-enable */";
const DUMMY_LEN = DUMMY_STR.length;
const ERB_REGEX = /<%[\s\S]*?%>/g;

/**
* Transforms the given text into lintable text. We do this by stripping out
Expand All @@ -19,17 +17,19 @@ const DUMMY_LEN = DUMMY_STR.length;
* location of messages in the postprocess step later.
* @param {string} text text of the file
* @param {string} filename filename of the file
* @returns {Array<{ filename: string, text: string }>} source code blocks to lint.
* @param {string} dummyString dummy string to replace ERB tags with
* (this is language-specific)
* @returns {Array<{ filename: string, text: string }>} source code blocks to lint
*/
function preprocess(text, filename) {
function preprocess(text, filename, dummyString) {
let lintableTextArr = text.split("");

let match;
let numAddLines = 0;
let numDiffChars = 0;
const offsetMap = new OffsetMap();

while ((match = erbRegex.exec(text)) !== null) {
while ((match = ERB_REGEX.exec(text)) !== null) {
// Match information
const startIndex = match.index;
const matchText = match[0];
Expand All @@ -42,16 +42,16 @@ function preprocess(text, filename) {

// Columns
const coordStartIndex = indexToColumn(text, startIndex);
const endColumnAfter = coordStartIndex.column + DUMMY_LEN;
const endColumnAfter = coordStartIndex.column + dummyString.length;
const coordEndIndex = indexToColumn(text, endIndex);
const endColumnBefore = coordEndIndex.column;
const numAddColumns = endColumnBefore - endColumnAfter;

replaceTextWithDummy(lintableTextArr, startIndex, matchLength - 1);
replaceTextWithDummy(lintableTextArr, startIndex, matchLength - 1, dummyString);

// Store in map
const lineAfter = coordEndIndex.line - numAddLines;
numDiffChars += DUMMY_LEN - matchLength;
numDiffChars += dummyString.length - matchLength;
const endIndexAfter = endIndex + numDiffChars;
offsetMap.addMapping(endIndexAfter, lineAfter, numAddLines, numAddColumns);
}
Expand All @@ -61,12 +61,32 @@ function preprocess(text, filename) {
return [lintableText];
}

// works in-place
function replaceTextWithDummy(lintableTextArr, startIndex, length) {
lintableTextArr[startIndex] = DUMMY_STR;
/**
* In-place replaces the text (as array) at the given index, for a given length,
* with a dummy string.
*
* Note that the dummy string is inserted at the given index as one big string.
* For the length of the match, subsequent characters are replaced with empty
* strings in the array.
*/
function replaceTextWithDummy(lintableTextArr, startIndex, length, dummyString) {
lintableTextArr[startIndex] = dummyString;
const replaceArgs = Array(length).join(".").split(".");
// -> results in ['', '', '', '', ...]
lintableTextArr.splice(startIndex + 1, length, ...replaceArgs);
}

module.exports = preprocess;
function preprocessJs(text, filename) {
const dummyString = "/* eslint-disable */{}/* eslint-enable */";
return preprocess(text, filename, dummyString);
}

function preprocessHtml(text, filename) {
const dummyString = "<!-- -->";
return preprocess(text, filename, dummyString);
}

module.exports = {
preprocessJs,
preprocessHtml,
};
2 changes: 1 addition & 1 deletion tests/postprocess.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require("fs");

const assert = require("chai").assert;
const pre = require("../lib/preprocess.js");
const { preprocessJs: pre } = require("../lib/preprocess.js");
const post = require("../lib/postprocess.js");
const cache = require("../lib/cache.js");

Expand Down
2 changes: 1 addition & 1 deletion tests/preprocess.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require("fs");

const assert = require("chai").assert;
const p = require("../lib/preprocess.js");
const { preprocessJs: p } = require("../lib/preprocess.js");
const cache = require("../lib/cache.js");

describe("preprocess", () => {
Expand Down
Loading