Skip to content

Commit

Permalink
feat: @lexical/eslint-plugin - add a rules-of-lexical linter to help …
Browse files Browse the repository at this point in the history
…with $function rules
  • Loading branch information
etrepum committed Apr 24, 2024
1 parent 836fff6 commit e48cad5
Show file tree
Hide file tree
Showing 19 changed files with 926 additions and 10 deletions.
11 changes: 9 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
'fbjs',
'plugin:react-hooks/recommended',
'plugin:lexical/all',
'plugin:@lexical/all',
'prettier',
],

Expand Down Expand Up @@ -79,8 +80,13 @@ module.exports = {
},
},
{
// These aren't compiled, but they're written in module JS
files: ['packages/lexical-playground/esm/*.mjs'],
files: [
// These aren't compiled, but they're written in module JS
'packages/lexical-playground/esm/*.mjs',
// These are written in module JS for bootstrapping reasons, so we
// can use the plugin without compiling it first
'packages/lexical-eslint-plugin/**/*.mjs',
],
parserOptions: {
sourceType: 'module',
},
Expand Down Expand Up @@ -119,6 +125,7 @@ module.exports = {
'react',
'no-only-tests',
'lexical',
'@lexical',
],

// Stop ESLint from looking for a configuration file in parent folders
Expand Down
1 change: 1 addition & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module.name_mapper='^@lexical/clipboard$' -> '<PROJECT_ROOT>/packages/lexical-cl
module.name_mapper='^@lexical/code$' -> '<PROJECT_ROOT>/packages/lexical-code/flow/LexicalCode.js.flow'
module.name_mapper='^@lexical/devtools-core$' -> '<PROJECT_ROOT>/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow'
module.name_mapper='^@lexical/dragon$' -> '<PROJECT_ROOT>/packages/lexical-dragon/flow/LexicalDragon.js.flow'
module.name_mapper='^@lexical/eslint-plugin$' -> '<PROJECT_ROOT>/packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow'
module.name_mapper='^@lexical/file$' -> '<PROJECT_ROOT>/packages/lexical-file/flow/LexicalFile.js.flow'
module.name_mapper='^@lexical/hashtag$' -> '<PROJECT_ROOT>/packages/lexical-hashtag/flow/LexicalHashtag.js.flow'
module.name_mapper='^@lexical/headless$' -> '<PROJECT_ROOT>/packages/lexical-headless/flow/LexicalHeadless.js.flow'
Expand Down
2 changes: 1 addition & 1 deletion eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"description": "ESLint plugin for lexical",
"main": "src/index.js",
"peerDependencies": {
"eslint": ">=4.19.1"
"eslint": "^7.31.0 || ^8.0.0"
}
}
36 changes: 29 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"@babel/preset-flow": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.16.7",
"@lexical/eslint-plugin": "file:./packages/lexical-eslint-plugin",
"@playwright/test": "^1.41.2",
"@rollup/plugin-alias": "^3.1.4",
"@rollup/plugin-babel": "^5.3.0",
Expand Down
1 change: 1 addition & 0 deletions packages/lexical-devtools/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@lexical/code": ["../lexical-code/src/index.ts"],
"@lexical/devtools-core": ["../lexical-devtools-core/src/index.ts"],
"@lexical/dragon": ["../lexical-dragon/src/index.ts"],
"@lexical/eslint-plugin": ["../lexical-eslint-plugin/src/index.ts"],
"@lexical/file": ["../lexical-file/src/index.ts"],
"@lexical/hashtag": ["../lexical-hashtag/src/index.ts"],
"@lexical/headless": ["../lexical-headless/src/index.ts"],
Expand Down
14 changes: 14 additions & 0 deletions packages/lexical-eslint-plugin/LexicalEslintPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

'use strict';
/**
* This file is here for bootstrapping reasons so we can use it without
* building anything
*/
module.exports = require('./src/LexicalEslintPlugin.js');
138 changes: 138 additions & 0 deletions packages/lexical-eslint-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# `@lexical/eslint-plugin`

This ESLint plugin enforces the [Lexical $function convention](https://lexical.dev/docs/intro#reading-and-updating-editor-state).

## Installation

Assuming you already have ESLint installed, run:

```sh
npm install @lexical/eslint-plugin --save-dev
```

Then extend the recommended eslint config:

```js
{
"extends": [
// ...
"plugin:@lexical/recommended"
]
}
```

### Custom Configuration

If you want more fine-grained configuration, you can instead add a snippet like this to your ESLint configuration file:

```js
{
"plugins": [
// ...
"@lexical"
],
"rules": {
// ...
"@lexical/rules-of-lexical": "error"
}
}
```


## Valid and Invalid Examples

### Valid Examples

\$functions may be called by other \$functions

```js
function $namedCorrectly() {
return $getRoot();
}
```

\$functions may be called in `editor.update` or `editorState.read`

```js
function validUsesEditorOrState(editor) {
editor.update(() => $getRoot());
editor.getLatestState().read(() => $getRoot());
}
```

\$functions may be called from class methods

```js
class CustomNode extends ElementNode {
appendText(string) {
this.appendChild($createTextNode(string));
}
}
```

### Invalid Examples

#### Rename autofix

```js
function invalidFunction() {
return $getRoot();
}
function $callsInvalidFunction() {
return invalidFunction();
}
```

*Autofix:* The function is renamed with a $ prefix. Any references to this
name in this module are also always renamed.

```js
function $invalidFunction() {
return $getRoot();
}
function $callsInvalidFunction() {
return $invalidFunction();
}
```

#### Rename & deprecate autofix

```js
export function exportedInvalidFunction() {
return $getRoot();
}
```

*Autofix:* The exported function is renamed with a $ prefix. The previous name
is also exported and marked deprecated, because automatic renaming of
references to that name is limited to the module's scope.

```js
export function $exportedInvalidFunction() {
return $getRoot();
}
/** @deprecated renamed to $exportedInvalidFunction by @lexical/eslint-plugin rules-of-lexical */
export const exportedInvalidFunction = $exportedInvalidFunction;
```

#### Rename scope conflict

```js
import {$getRoot} from 'lexical';
function InvalidComponent({editor}) {
const [editor] = useLexicalComposerContext();
const getRoot = useCallback(() => $getRoot(), []);
return (<button onClick={() => editor.update(() => getRoot())} />);
}
```

*Autofix:* The function is renamed with a $ prefix and _ suffix since the suggested name was already in scope.

```js
import {$getRoot} from 'lexical';
function InvalidComponent({editor}) {
const [editor] = useLexicalComposerContext();
const $getRoot_ = useCallback(() => $getRoot(), []);
return (<button onClick={() => editor.update(() => $getRoot_())} />);
}
```
11 changes: 11 additions & 0 deletions packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

/**
* LexicalEslintPlugin
*/
49 changes: 49 additions & 0 deletions packages/lexical-eslint-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@lexical/eslint-plugin",
"description": "Lexical specific linting rules for ESLint",
"keywords": [
"eslint",
"eslint-plugin",
"eslintplugin",
"lexical",
"editor"
],
"version": "0.14.5",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/lexical.git",
"directory": "packages/lexical-eslint-plugin"
},
"main": "LexicalEslintPlugin.js",
"types": "index.d.ts",
"bugs": {
"url": "https://github.com/facebook/lexical/issues"
},
"homepage": "https://github.com/facebook/lexical#readme",
"sideEffects": false,
"peerDependencies": {
"eslint": ">=7.31.0 || ^8.0.0"
},
"exports": {
".": {
"import": {
"types": "./index.d.ts",
"development": "./LexicalEslintPlugin.dev.mjs",
"production": "./LexicalEslintPlugin.prod.mjs",
"node": "./LexicalEslintPlugin.node.mjs",
"default": "./LexicalEslintPlugin.mjs"
},
"require": {
"types": "./index.d.ts",
"development": "./LexicalEslintPlugin.dev.js",
"production": "./LexicalEslintPlugin.prod.js",
"default": "./LexicalEslintPlugin.js"
}
}
},
"devDependencies": {
"@types/eslint": "^8.56.9"
},
"module": "LexicalEslintPlugin.mjs"
}
32 changes: 32 additions & 0 deletions packages/lexical-eslint-plugin/src/LexicalEslintPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

// @ts-check

const {name, version} = require('../package.json');
const rulesOfLexical = require('./rules/rules-of-lexical.js');

const all = {
plugins: ['@lexical'],
rules: {
'@lexical/rules-of-lexical': 'warn',
},
};

const plugin = {
configs: {
all,
recommended: all,
},
meta: {name, version},
rules: {
'rules-of-lexical': rulesOfLexical,
},
};

module.exports = plugin;
Loading

0 comments on commit e48cad5

Please sign in to comment.