diff --git a/.eslintrc.js b/.eslintrc.js index 86307750e4be..18b0774783c9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { 'fbjs', 'plugin:react-hooks/recommended', 'plugin:lexical/all', + 'plugin:@lexical/all', 'prettier', ], @@ -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', }, @@ -119,6 +125,7 @@ module.exports = { 'react', 'no-only-tests', 'lexical', + '@lexical', ], // Stop ESLint from looking for a configuration file in parent folders diff --git a/.flowconfig b/.flowconfig index cb6c16f3d357..277424bdcd33 100644 --- a/.flowconfig +++ b/.flowconfig @@ -24,6 +24,7 @@ module.name_mapper='^@lexical/clipboard$' -> '/packages/lexical-cl module.name_mapper='^@lexical/code$' -> '/packages/lexical-code/flow/LexicalCode.js.flow' module.name_mapper='^@lexical/devtools-core$' -> '/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow' module.name_mapper='^@lexical/dragon$' -> '/packages/lexical-dragon/flow/LexicalDragon.js.flow' +module.name_mapper='^@lexical/eslint-plugin$' -> '/packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow' module.name_mapper='^@lexical/file$' -> '/packages/lexical-file/flow/LexicalFile.js.flow' module.name_mapper='^@lexical/hashtag$' -> '/packages/lexical-hashtag/flow/LexicalHashtag.js.flow' module.name_mapper='^@lexical/headless$' -> '/packages/lexical-headless/flow/LexicalHeadless.js.flow' diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json index c50e3ba12a0f..89b92535ebfd 100644 --- a/eslint-plugin/package.json +++ b/eslint-plugin/package.json @@ -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" } } diff --git a/package-lock.json b/package-lock.json index fc3dec158c94..d557a503a823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,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", @@ -98,7 +99,7 @@ "version": "1.0.0", "dev": true, "peerDependencies": { - "eslint": ">=4.19.1" + "eslint": "^7.31.0 || ^8.0.0" } }, "node_modules/@aklinker1/rollup-plugin-visualizer": { @@ -6181,6 +6182,10 @@ "resolved": "packages/lexical-dragon", "link": true }, + "node_modules/@lexical/eslint-plugin": { + "resolved": "packages/lexical-eslint-plugin", + "link": true + }, "node_modules/@lexical/file": { "resolved": "packages/lexical-file", "link": true @@ -7721,9 +7726,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", - "integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==", + "version": "8.56.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", + "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -33131,6 +33136,17 @@ "lexical": "0.14.5" } }, + "packages/lexical-eslint-plugin": { + "name": "@lexical/eslint-plugin", + "version": "0.14.5", + "license": "MIT", + "devDependencies": { + "@types/eslint": "^8.56.9" + }, + "peerDependencies": { + "eslint": ">=7.31.0 || ^8.0.0" + } + }, "packages/lexical-file": { "name": "@lexical/file", "version": "0.14.5", @@ -37984,6 +38000,12 @@ "lexical": "0.14.5" } }, + "@lexical/eslint-plugin": { + "version": "file:packages/lexical-eslint-plugin", + "requires": { + "@types/eslint": "^8.56.9" + } + }, "@lexical/file": { "version": "file:packages/lexical-file", "requires": { @@ -39082,9 +39104,9 @@ } }, "@types/eslint": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", - "integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==", + "version": "8.56.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", + "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==", "requires": { "@types/estree": "*", "@types/json-schema": "*" diff --git a/package.json b/package.json index 7d275ab31a03..d15d8cda27e1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/lexical-devtools/tsconfig.json b/packages/lexical-devtools/tsconfig.json index 350081311a66..8978b961e03a 100644 --- a/packages/lexical-devtools/tsconfig.json +++ b/packages/lexical-devtools/tsconfig.json @@ -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"], diff --git a/packages/lexical-eslint-plugin/LexicalEslintPlugin.js b/packages/lexical-eslint-plugin/LexicalEslintPlugin.js new file mode 100644 index 000000000000..0195b476c585 --- /dev/null +++ b/packages/lexical-eslint-plugin/LexicalEslintPlugin.js @@ -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'); diff --git a/packages/lexical-eslint-plugin/README.md b/packages/lexical-eslint-plugin/README.md new file mode 100644 index 000000000000..dd7fd163dac7 --- /dev/null +++ b/packages/lexical-eslint-plugin/README.md @@ -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 (