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 26, 2024
1 parent 988a25a commit d2ba7da
Show file tree
Hide file tree
Showing 19 changed files with 1,081 additions and 10 deletions.
31 changes: 29 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
const restrictedGlobals = require('confusing-browser-globals');

const OFF = 0;
const WARN = 1;
const ERROR = 2;

module.exports = {
Expand Down Expand Up @@ -64,6 +65,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@lexical/all',
],
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
Expand All @@ -72,15 +74,39 @@ module.exports = {
},
plugins: ['react', '@typescript-eslint', 'header'],
rules: {
'@lexical/rules-of-lexical': [
WARN,
/** @type import('./packages/lexical-eslint-plugin/src').RulesOfLexicalOptions */ ({
isImplicitDollarFunction: [
'beginUpdate',
'commitPendingUpdates',
'createBinding',
'createChildrenArray',
'flushRootMutations',
'getNodeFromDOM',
'getNodeFromDOMNode',
'getOrInitCollabNodeFromSharedType',
'isSelectionCapturedInDecoratorInput',
'mergePrevious',
'parseEditorState',
'removeNode',
'syncLocalCursorPosition',
'trimTextContentFromAnchor',
],
isSafeDollarFunction: '$getRoot',
}),
],
'@typescript-eslint/ban-ts-comment': OFF,
'@typescript-eslint/no-this-alias': OFF,
'@typescript-eslint/no-unused-vars': [ERROR, {args: 'none'}],
'header/header': [2, 'scripts/www/headerTemplate.js'],
},
},
{
// 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',
],
parserOptions: {
sourceType: 'module',
},
Expand Down Expand Up @@ -119,6 +145,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"
}
Loading

0 comments on commit d2ba7da

Please sign in to comment.