From 89b252dae6f1f5cd65504185adde64dfcb2317b1 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 14 Apr 2024 20:52:59 -0700 Subject: [PATCH] feat: @lexical/eslint-plugin - add a rules-of-lexical linter to help with $function rules --- .eslintrc.js | 11 +- .flowconfig | 1 + .npmrc | 1 + eslint-plugin/package.json | 2 +- package-lock.json | 1213 ++++++++++++++++- package.json | 2 + packages/lexical-devtools/tsconfig.json | 1 + .../LexicalEslintPlugin.js | 14 + packages/lexical-eslint-plugin/README.md | 5 + .../flow/LexicalEslintPlugin.js.flow | 11 + packages/lexical-eslint-plugin/package.json | 49 + .../__tests__/unit/rules-of-lexical.test.ts | 165 +++ packages/lexical-eslint-plugin/src/index.js | 32 + packages/lexical-eslint-plugin/src/index.ts | 16 + .../src/rules/rules-of-lexical.js | 353 +++++ .../src/util/getFunctionName.js | 41 + .../src/util/getParentAssignmentName.js | 60 + .../flow/LexicalContextMenuPlugin.js.flow | 11 + .../flow/LexicalEditorRefPlugin.js.flow | 11 + .../flow/LexicalNodeEventPlugin.js.flow | 11 + .../lexical-website/docs/maintainers-guide.md | 117 ++ .../docs/packages/lexical-eslint-plugin.md | 6 + scripts/npm/npm-init.js | 44 + scripts/update-flowconfig.js | 28 +- tsconfig.build.json | 3 + tsconfig.json | 8 +- 26 files changed, 2203 insertions(+), 13 deletions(-) create mode 100644 packages/lexical-eslint-plugin/LexicalEslintPlugin.js create mode 100644 packages/lexical-eslint-plugin/README.md create mode 100644 packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow create mode 100644 packages/lexical-eslint-plugin/package.json create mode 100644 packages/lexical-eslint-plugin/src/__tests__/unit/rules-of-lexical.test.ts create mode 100644 packages/lexical-eslint-plugin/src/index.js create mode 100644 packages/lexical-eslint-plugin/src/index.ts create mode 100644 packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js create mode 100644 packages/lexical-eslint-plugin/src/util/getFunctionName.js create mode 100644 packages/lexical-eslint-plugin/src/util/getParentAssignmentName.js create mode 100644 packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow create mode 100644 packages/lexical-react/flow/LexicalEditorRefPlugin.js.flow create mode 100644 packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow create mode 100644 packages/lexical-website/docs/packages/lexical-eslint-plugin.md create mode 100644 scripts/npm/npm-init.js diff --git a/.eslintrc.js b/.eslintrc.js index ab5e599fe7fd..b51af0c16df8 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/.npmrc b/.npmrc index 521a9f7c0773..b6c243d6f368 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ legacy-peer-deps=true +init-module=./scripts/npm/npm-init.js 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 1dc03ce094c6..9df29b09854a 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", @@ -68,6 +69,7 @@ "hermes-parser": "^0.20.1", "hermes-transform": "^0.20.1", "husky": "^7.0.1", + "init-package-json": "^6.0.2", "jest": "^29.4.0", "jest-environment-jsdom": "^29.4.0", "jsdom": "^24.0.0", @@ -97,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": { @@ -4064,6 +4066,96 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -4880,6 +4972,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 @@ -5063,6 +5159,222 @@ "node": ">= 8" } }, + "node_modules/@npmcli/git": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.6.tgz", + "integrity": "sha512-4x/182sKXmQkf0EtXxT26GEsaOATpD7WVtza5hrYivWZeo6QefC6xq9KAXrnjtFKBZ4rZwR7aX/zClYYXgtwLw==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.3.tgz", + "integrity": "sha512-cgsjCvld2wMqkUqvY+SZI+1ZJ7umGBYc9IAKfqJRKJCcs7hCQYxScUgdsyrRINk3VmdCYf9TXiLBHQ6ECTxhtg==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@npmcli/package-json/node_modules/normalize-package-data": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", + "dev": true, + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/test": { "version": "1.41.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", @@ -6402,9 +6714,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": "*" @@ -8680,6 +8992,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -11087,6 +11408,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -13229,6 +13556,63 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", @@ -14900,6 +15284,24 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/init-package-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-6.0.2.tgz", + "integrity": "sha512-ZQ9bxt6PkqIH6fPU69HPheOMoUqIqVqwZj0qlCBfoSCG4lplQhVM/qB3RS4f0RALK3WZZSrNQxNtCZgphuf3IA==", + "dev": true, + "dependencies": { + "@npmcli/package-json": "^5.0.0", + "npm-package-arg": "^11.0.0", + "promzard": "^1.0.0", + "read": "^3.0.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", @@ -15624,6 +16026,24 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/jest/-/jest-29.4.3.tgz", @@ -21087,6 +21507,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -21432,6 +21861,78 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", + "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -22368,6 +22869,31 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -23575,6 +24101,15 @@ "node": ">=6" } }, + "node_modules/proc-log": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.0.0.tgz", + "integrity": "sha512-v1lzmYxGDs2+OZnmYtYZK3DG8zogt+CbQ+o/iqqtTfpyCmGWulCTEQu5GIbivf7OjgIkH2Nr8SH8UxAGugZNbg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -23589,12 +24124,40 @@ "node": ">=0.4.0" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, "node_modules/promise-polyfill": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==", "dev": true }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/promise-toolbox": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/promise-toolbox/-/promise-toolbox-0.21.0.tgz", @@ -23619,6 +24182,18 @@ "node": ">= 6" } }, + "node_modules/promzard": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promzard/-/promzard-1.0.1.tgz", + "integrity": "sha512-ulDF77aULEHUoJkN5XZgRV5loHXBaqd9eorMvLNLvi2gXMuRAtwH6Gh4zsMHQY1kTt7tyv/YZwZW5C2gtj8F2A==", + "dev": true, + "dependencies": { + "read": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -24758,6 +25333,18 @@ "react": "17.0.2" } }, + "node_modules/read": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/read/-/read-3.0.1.tgz", + "integrity": "sha512-SLBrDU/Srs/9EoWhU5GdbAoxG1GzpQHo/6qiGItaoLJ1thmYpcNIM1qISEUvyHBzfGlWIyd6p2DNi1oV1VmAuw==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -26641,6 +27228,27 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -26747,6 +27355,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -28629,6 +29250,18 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -29616,6 +30249,57 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -31450,6 +32134,17 @@ "lexical": "0.14.3" } }, + "packages/lexical-eslint-plugin": { + "name": "@lexical/eslint-plugin", + "version": "0.14.3", + "license": "MIT", + "devDependencies": { + "@types/eslint": "^8.56.9" + }, + "peerDependencies": { + "eslint": ">=7.31.0" + } + }, "packages/lexical-file": { "name": "@lexical/file", "version": "0.14.3", @@ -34514,6 +35209,65 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -35309,6 +36063,12 @@ "lexical": "0.14.3" } }, + "@lexical/eslint-plugin": { + "version": "file:packages/lexical-eslint-plugin", + "requires": { + "@types/eslint": "^8.56.9" + } + }, "@lexical/file": { "version": "file:packages/lexical-file", "requires": { @@ -35569,6 +36329,165 @@ "fastq": "^1.6.0" } }, + "@npmcli/git": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.6.tgz", + "integrity": "sha512-4x/182sKXmQkf0EtXxT26GEsaOATpD7WVtza5hrYivWZeo6QefC6xq9KAXrnjtFKBZ4rZwR7aX/zClYYXgtwLw==", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "dependencies": { + "isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true + }, + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + }, + "which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "requires": { + "isexe": "^3.1.1" + } + } + } + }, + "@npmcli/package-json": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.3.tgz", + "integrity": "sha512-cgsjCvld2wMqkUqvY+SZI+1ZJ7umGBYc9IAKfqJRKJCcs7hCQYxScUgdsyrRINk3VmdCYf9TXiLBHQ6ECTxhtg==", + "dev": true, + "requires": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + } + }, + "hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "requires": { + "lru-cache": "^10.0.1" + } + }, + "json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true + }, + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + }, + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "normalize-package-data": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, + "requires": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + } + } + }, + "@npmcli/promise-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", + "dev": true, + "requires": { + "which": "^4.0.0" + }, + "dependencies": { + "isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true + }, + "which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "requires": { + "isexe": "^3.1.1" + } + } + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@playwright/test": { "version": "1.41.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", @@ -36392,9 +37311,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": "*" @@ -38152,6 +39071,15 @@ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, + "builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, "bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -39865,6 +40793,12 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, "errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -41343,6 +42277,44 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "fork-ts-checker-webpack-plugin": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", @@ -42547,6 +43519,21 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "init-package-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-6.0.2.tgz", + "integrity": "sha512-ZQ9bxt6PkqIH6fPU69HPheOMoUqIqVqwZj0qlCBfoSCG4lplQhVM/qB3RS4f0RALK3WZZSrNQxNtCZgphuf3IA==", + "dev": true, + "requires": { + "@npmcli/package-json": "^5.0.0", + "npm-package-arg": "^11.0.0", + "promzard": "^1.0.0", + "read": "^3.0.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + } + }, "inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", @@ -43037,6 +44024,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jest": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/jest/-/jest-29.4.3.tgz", @@ -46800,6 +47797,12 @@ } } }, + "mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true + }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -47076,6 +48079,62 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, + "npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true + }, + "npm-package-arg": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", + "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "dev": true, + "requires": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "requires": { + "lru-cache": "^10.0.1" + } + }, + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + } + } + }, + "npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "requires": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + } + }, "npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -47747,6 +48806,24 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + } + } + }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -48474,6 +49551,12 @@ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" }, + "proc-log": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.0.0.tgz", + "integrity": "sha512-v1lzmYxGDs2+OZnmYtYZK3DG8zogt+CbQ+o/iqqtTfpyCmGWulCTEQu5GIbivf7OjgIkH2Nr8SH8UxAGugZNbg==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -48485,12 +49568,36 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, "promise-polyfill": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==", "dev": true }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "dependencies": { + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + } + } + }, "promise-toolbox": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/promise-toolbox/-/promise-toolbox-0.21.0.tgz", @@ -48509,6 +49616,15 @@ "sisteransi": "^1.0.5" } }, + "promzard": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promzard/-/promzard-1.0.1.tgz", + "integrity": "sha512-ulDF77aULEHUoJkN5XZgRV5loHXBaqd9eorMvLNLvi2gXMuRAtwH6Gh4zsMHQY1kTt7tyv/YZwZW5C2gtj8F2A==", + "dev": true, + "requires": { + "read": "^3.0.1" + } + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -49338,6 +50454,15 @@ "scheduler": "^0.20.2" } }, + "read": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/read/-/read-3.0.1.tgz", + "integrity": "sha512-SLBrDU/Srs/9EoWhU5GdbAoxG1GzpQHo/6qiGItaoLJ1thmYpcNIM1qISEUvyHBzfGlWIyd6p2DNi1oV1VmAuw==", + "dev": true, + "requires": { + "mute-stream": "^1.0.0" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -50813,6 +51938,25 @@ } } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + } + } + }, "string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -50889,6 +52033,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -52221,6 +53374,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "requires": { + "builtins": "^5.0.0" + } + }, "value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -52960,6 +54122,43 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 7bc20010eb54..92f76e964199 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,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", @@ -158,6 +159,7 @@ "hermes-parser": "^0.20.1", "hermes-transform": "^0.20.1", "husky": "^7.0.1", + "init-package-json": "^6.0.2", "jest": "^29.4.0", "jest-environment-jsdom": "^29.4.0", "jsdom": "^24.0.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..d73cdf02d45b --- /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/index.js'); diff --git a/packages/lexical-eslint-plugin/README.md b/packages/lexical-eslint-plugin/README.md new file mode 100644 index 000000000000..16019db5a1fb --- /dev/null +++ b/packages/lexical-eslint-plugin/README.md @@ -0,0 +1,5 @@ +# `@lexical/eslint-plugin` + +[![See API Documentation](https://lexical.dev/img/see-api-documentation.svg)](https://lexical.dev/docs/api/modules/lexical_eslint_plugin) + +Lexical specific linting rules for ESLint diff --git a/packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow b/packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow new file mode 100644 index 000000000000..3b23ff98ce68 --- /dev/null +++ b/packages/lexical-eslint-plugin/flow/LexicalEslintPlugin.js.flow @@ -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 + */ diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json new file mode 100644 index 000000000000..21045c75c61a --- /dev/null +++ b/packages/lexical-eslint-plugin/package.json @@ -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.3", + "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" + }, + "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" + }, + "dependencies": {} +} diff --git a/packages/lexical-eslint-plugin/src/__tests__/unit/rules-of-lexical.test.ts b/packages/lexical-eslint-plugin/src/__tests__/unit/rules-of-lexical.test.ts new file mode 100644 index 000000000000..e3cba972fb1d --- /dev/null +++ b/packages/lexical-eslint-plugin/src/__tests__/unit/rules-of-lexical.test.ts @@ -0,0 +1,165 @@ +/** + * 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. + * + */ + +import {RuleTester} from 'eslint'; +import * as prettier from 'prettier'; + +import plugin from '../../index.js'; + +// The given string which may be prefixed or underscored later +const NAME = (name: string) => name; +// This name is always prefixed, never underscored, to create scope conflicts on naive rename +const NAME_PREFIXED = (name: string) => + name.replace(/^\$?/, '$').replace(/_$/, ''); +// Ensure an underscore if the given name is prefixed +const NAME_UNDERSCORE = (name: string) => + !/^\$/.test(name) || /_$/.test(name) ? name : name + '_'; +const REEXPORT = (name: string) => + /^\$/.test(name) + ? `\n/** @deprecated renamed to ${name} by @lexical/eslint-plugin rules-of-lexical */\nexport const ${name.replace( + /^\$/, + '', + )} = ${name};\n` + : ''; + +function fmt( + strings: TemplateStringsArray, + ...keys: ((name: string) => string)[] +) { + const rval = (name: string) => { + const result = [strings[0]]; + keys.forEach((key, i) => { + result.push(key(name), strings[i + 1]); + }); + return prettier.format(result.join(''), {parser: 'typescript'}); + }; + rval.keys = keys; + return rval; +} + +const ruleTester = new RuleTester({ + parserOptions: {ecmaVersion: 2018, sourceType: 'module'}, +}); + +describe('LexicalEslintPlugin', () => { + it('exports a plugin with meta and rules', () => { + expect(Object.keys(plugin).sort()).toEqual( + expect.arrayContaining(['meta', 'rules']), + ); + }); +}); +['rules-of-lexical'].forEach((ruleName) => { + const namedRules = [ + fmt`const ${NAME} = () => $getRoot();`, + fmt`const ${NAME} = () => { return $getRoot(); }`, + fmt`function ${NAME}() { return $getRoot(); }`, + fmt`export default function ${NAME}() { return $getRoot(); }`, + fmt` + function ${NAME}() { return $getRoot(); } + export default function caller(editor) { return editor.getState().read(() => ${NAME}()); }`, + fmt` + function render() { + const ${NAME} = () => { return $getRoot(); } + return editor.getState().read(() => ${NAME}()); + } + `, + fmt` + function render() { + const ${NAME} = useCallback(() => { return $getRoot(); }, []); + return editor.getState().read(() => ${NAME}()); + } + `, + fmt` + const ${NAME} = (node) => { + if ($isMarkNode(node)) { + $unwrapMarkNode(node); + return; + } + if ($isElementNode(node)) { + const children = node.getChildren(); + for (const child of children) { + ${NAME}(child); + } + } + }; + `, + fmt` + import {${NAME_PREFIXED}} from '../../nodes/KeywordNode'; + export default function KeywordsPlugin() { + const ${NAME_UNDERSCORE} = useCallback((textNode) => { + return ${NAME_PREFIXED}(textNode.getTextContent()); + }, []); + } + `, + fmt` + export function ${NAME}() { + $getRoot(); + }${REEXPORT} + `, + fmt` + export const ${NAME} = () => $getRoot();${REEXPORT} + `, + ]; + describe(ruleName, () => { + const rule = plugin.rules[ruleName]; + ruleTester.run(ruleName, rule, { + invalid: [ + ...namedRules.map((codegen, i): RuleTester.InvalidTestCase => { + const caller = `func${i}`; + const suggestName = + `$${caller}` + (codegen.keys.includes(NAME_UNDERSCORE) ? '_' : ''); + return { + code: codegen(caller), + errors: [ + { + messageId: 'rulesOfLexicalReport', + suggestions: [ + { + data: { + callee: '$getRoot', + caller, + suggestName, + }, + messageId: 'rulesOfLexicalSuggestion', + output: codegen(suggestName), + }, + ], + }, + ], + output: codegen(suggestName), + }; + }), + ], + valid: [ + // this is used in tests + fmt`async function testCase() { await update(() => { $getRoot() }) }`, + // accepted by .update + fmt`editor.update(() => $getRoot());`, + // Accepted by .read + fmt`editor.getEditorState().read(() => $getRoot());`, + // accepted by being in a class definition + fmt` + class Foo extends TextNode { + mutator() { + $getRoot(); + } + }`, + fmt` + export function $isSelectionCapturedInDecorator(node) { + return $isDecoratorNode($getNearestNodeFromDOMNode(node)); + } + `, + fmt`new Option({ onSelect: (_node) => $getRoot() })`, + fmt`new Option({ onSelect: function (_node) { $getRoot() } })`, + fmt`new Option({ onSelect: function onSelect(_node) { $getRoot() } })`, + fmt`new Option({ onSelect(_node) { $getRoot() } })`, + ...namedRules, + ].map((codegen, i) => codegen(`$func${i}`)), + }); + }); +}); diff --git a/packages/lexical-eslint-plugin/src/index.js b/packages/lexical-eslint-plugin/src/index.js new file mode 100644 index 000000000000..be9159e08480 --- /dev/null +++ b/packages/lexical-eslint-plugin/src/index.js @@ -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; diff --git a/packages/lexical-eslint-plugin/src/index.ts b/packages/lexical-eslint-plugin/src/index.ts new file mode 100644 index 000000000000..da3b834c8522 --- /dev/null +++ b/packages/lexical-eslint-plugin/src/index.ts @@ -0,0 +1,16 @@ +/** + * 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. + * + */ + +/** + * For bootstrapping reasons, this module is written in CJS JavaScript so no + * compilation is necessary + */ + +import * as plugin from './index.js'; + +export default plugin; diff --git a/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js b/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js new file mode 100644 index 000000000000..95c0ac9ab8c8 --- /dev/null +++ b/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js @@ -0,0 +1,353 @@ +/** + * 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 getFunctionName = require('../util/getFunctionName.js'); +const getParentAssignmentName = require('../util/getParentAssignmentName.js'); + +/** + * @typedef {import('@types/eslint').Rule.NodeParentExtension} NodeParentExtension + * @typedef {import('estree').CallExpression & NodeParentExtension} CallExpression + * @typedef {import('estree').Identifier & NodeParentExtension} Identifier + * @typedef {import('@types/eslint').Rule.Node} Node + * @typedef {import('@types/eslint').Rule.RuleModule} RuleModule + * @typedef {import('@types/eslint').Rule.ReportFixer} ReportFixer + * @typedef {import('@types/eslint').SourceCode} SourceCode + * @typedef {import('@types/eslint').Scope.Variable} Variable + */ + +/** + * Find the variable associated with the given Identifier + * + * @param {SourceCode} sourceCode + * @param {Identifier} identifier + */ +function getIdentifierVariable(sourceCode, identifier) { + const scopeManager = sourceCode.scopeManager; + for (let node = identifier; node; node = node.parent) { + const variable = scopeManager + .getDeclaredVariables(node) + .find((v) => v.identifiers.includes(identifier)); + if (variable) { + return variable; + } + const scope = scopeManager.acquire(node); + if (scope) { + return ( + scope.set.get(identifier.name) || + (scope.upper ? scope.upper.set.get(identifier.name) : undefined) + ); + } + } + return undefined; +} + +/** + * Catch all identifiers that begin with "$" followed by an lowercase Latin + * character + * + * @param {string} name + */ +function isDollarFunctionName(name) { + // Can be in the format of $name or INTERNAL_$name + return /(^|_)\$[a-z]/.test(name); +} + +const ALLOWED_DOLLAR_FUNCTIONS = { + $createRootNode: true, +}; + +/** + * It's usually safe to call $isNode functions + * + * @param {string} name + * @returns + */ +function isAllowedDollarFunction(name) { + return ( + /^(INTERNAL_)?\$is[A-Z]/.test(name) || name in ALLOWED_DOLLAR_FUNCTIONS + ); +} + +// These are other function names that could have editor state context +const ALLOWED_IMPLICIT_EDITOR_STATE = { + after: true, + convert: true, + forChild: true, +}; + +// These are internal or exported so we probably have to keep them allowed for some time +const ALLOWED_DEPRECATED_IMPLICIT_EDITOR_STATE = { + beginUpdate: true, + commitPendingUpdates: true, + createChildrenArray: true, + flushRootMutations: true, + getNodeFromDOM: true, + getNodeFromDOMNode: true, + isSelectionCapturedInDecoratorInput: true, + parseEditorState: true, +}; + +/** + * Some function names should have implicit access to editor state, because they are likely + * part of a conversion or they were historically used and exported so will require deprecation + * + * @param {string} name + */ +function isImplicitDollarFunctionName(name) { + return ( + name in ALLOWED_IMPLICIT_EDITOR_STATE || + name in ALLOWED_DEPRECATED_IMPLICIT_EDITOR_STATE || + /NodeTransform$/.test(name) || + /^convert.*(Element|Node)/.test(name) || + /^(internal|INTERNAL_)/.test(name) + ); +} + +// read, update, registerCommand, and registerNodeTransform should definitely be here +const ALLOWED_CALL_IDENTIFIERS = { + read: true, + registerCommand: true, + registerNodeTransform: true, + update: true, +}; + +/** @param {Node | undefined} node */ +function isAllowedCallIdentifier(node) { + // These is the allow list of other functions that may call $functions + return ( + node && node.type === 'Identifier' && node.name in ALLOWED_CALL_IDENTIFIERS + ); +} + +/** @param {Node | undefined} node */ +function isHookFunctionIdentifier(node) { + return node && node.type === 'Identifier' && /^use[A-Z]/.test(node.name); +} + +/** + * @param {CallExpression} node + */ +function isCheckedDollarFunctionCall(node) { + const id = getFunctionNameIdentifier(node.callee); + return ( + id && isDollarFunctionName(id.name) && !isAllowedDollarFunction(id.name) + ); +} + +/** + * @param {Node | undefined} node + * @returns {Identifier | undefined} + */ +function getFunctionNameIdentifier(node) { + if (!node) { + return; + } else if (node.type === 'Identifier') { + return node; + } else if (node.type === 'MemberExpression' && !node.computed) { + return getFunctionNameIdentifier(node.property); + } +} + +/** @param {CallExpression} node */ +function isAllowedCall(node) { + // We allow read(), update(), (expr).read() and (expr).update() here + return isAllowedCallIdentifier(getFunctionNameIdentifier(node.callee)); +} + +/** @param {Node} node */ +function getLexicalFunctionName(node) { + const name = getFunctionName(node); + if (name) { + return name; + } + if ( + node.parent.type === 'CallExpression' && + node.parent.arguments[0] === node + ) { + const parentName = getFunctionNameIdentifier(node.parent.callee); + if (isHookFunctionIdentifier(parentName)) { + return getParentAssignmentName(node.parent); + } + } +} + +/** + * @param {Identifier} nameIdentifier + * @param {Variable | undefined} variable + */ +function getSuggestName(nameIdentifier, variable) { + const suggestName = '$' + nameIdentifier.name; + // Add an underscore if this would shadow an existing name + if (variable) { + for (let scope = variable.scope.upper; scope; scope = scope.upper) { + if (scope.set.has(suggestName)) { + return suggestName + '_'; + } + } + } + return suggestName; +} + +/** + * @param {Identifier} nameIdentifier + * @param {Variable | undefined} variable + */ +function getExportDeclaration(nameIdentifier, variable) { + if (variable && variable.defs.length === 1) { + const [{node}] = variable.defs; + if (node.parent.type === 'ExportNamedDeclaration') { + // export function foo(); + return node.parent; + } else if ( + node.parent.type === 'VariableDeclaration' && + node.parent.parent.type === 'ExportNamedDeclaration' + ) { + // export const foo = () => {}; + return node.parent.parent; + } + } +} + +/** @param {Record<'caller'|'suggestName'>} data */ +function renameExportText({caller, suggestName}) { + return `\n/** @deprecated renamed to ${suggestName} by @lexical/eslint-plugin rules-of-lexical */\nexport const ${caller} = ${suggestName};`; +} + +/** @type {RuleModule} */ +module.exports = { + create(context) { + // Deprecated in 8.x but we are still on 7.x + const sourceCode = context.getSourceCode(); + /** @type {Set} */ + const ignoreSet = new Set(); + /** @type {Set} */ + const reportedSet = new Set(); + /** @type {{ name?: Identifier, node }[]} funStack */ + const funStack = []; + const shouldIgnore = () => { + if (ignoreSet.size > 0) { + return true; + } + // Ignore property assignments + const lastFunction = funStack[funStack.length - 1]; + return lastFunction && lastFunction.node.parent.type === 'Property'; + }; + const pushIgnoredNode = (node) => ignoreSet.add(node); + const popIgnoredNode = (node) => ignoreSet.delete(node); + const pushFunction = (node) => { + const name = getFunctionNameIdentifier(getLexicalFunctionName(node)); + funStack.push({name, node}); + if ( + name && + (isDollarFunctionName(name.name) || + isImplicitDollarFunctionName(name.name)) + ) { + pushIgnoredNode(node); + } + }; + const popFunction = (node) => { + funStack.pop(); + popIgnoredNode(node); + }; + const getParentLexicalFunctionNameIdentifier = (_node) => { + const pair = funStack[funStack.length - 1]; + return pair ? pair.name : undefined; + }; + // Find all $function calls that are not inside a class or inside a $function + return { + ArrowFunctionExpression: pushFunction, + 'ArrowFunctionExpression:exit': popFunction, + CallExpression: (node) => { + if (isAllowedCall(node)) { + pushIgnoredNode(node); + } + if (shouldIgnore() || !isCheckedDollarFunctionCall(node)) { + return; + } + const nameIdentifier = getParentLexicalFunctionNameIdentifier(node); + if (!nameIdentifier || reportedSet.has(nameIdentifier)) { + return; + } + reportedSet.add(nameIdentifier); + const variable = getIdentifierVariable(sourceCode, nameIdentifier); + const suggestName = getSuggestName(nameIdentifier, variable); + const exportDeclaration = getExportDeclaration( + nameIdentifier, + variable, + ); + const data = { + callee: sourceCode.getText(node.callee), + caller: sourceCode.getText(nameIdentifier), + suggestName, + }; + /** @type {ReportFixer} */ + const fix = (fixer) => { + /** @type {Set} */ + const replaced = new Set(); + /** @type {Rule.Fix[]} */ + const fixes = []; + const renameIdentifier = (identifier) => { + if (!replaced.has(identifier)) { + replaced.add(identifier); + fixes.push(fixer.replaceText(identifier, suggestName)); + } + }; + renameIdentifier(nameIdentifier); + if (exportDeclaration) { + fixes.push( + fixer.insertTextAfter(exportDeclaration, renameExportText(data)), + ); + } + if (variable) { + for (const ref of variable.references) { + renameIdentifier(ref.identifier); + } + } + return fixes; + }; + context.report({ + data, + fix, + messageId: 'rulesOfLexicalReport', + node: nameIdentifier, + suggest: [ + { + data, + fix, + messageId: 'rulesOfLexicalSuggestion', + }, + ], + }); + }, + 'CallExpression:exit': popIgnoredNode, + ClassBody: pushIgnoredNode, + 'ClassBody:exit': popIgnoredNode, + FunctionDeclaration: pushFunction, + 'FunctionDeclaration:exit': popFunction, + FunctionExpression: pushFunction, + 'FunctionExpression:exit': popFunction, + }; + }, + meta: { + docs: { + description: 'enforces the Rules of Lexical', + recommended: true, + url: 'https://lexical.dev/docs/intro#reading-and-updating-editor-state', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + rulesOfLexicalReport: + '{{ callee }} called from {{ caller }}, without $ prefix or read/update context', + rulesOfLexicalSuggestion: 'Rename {{ caller }} to {{ suggestName }}', + }, + schema: [{additionalProperties: false, properties: {}, type: 'object'}], + type: 'suggestion', + }, +}; diff --git a/packages/lexical-eslint-plugin/src/util/getFunctionName.js b/packages/lexical-eslint-plugin/src/util/getFunctionName.js new file mode 100644 index 000000000000..c64c0a3d55df --- /dev/null +++ b/packages/lexical-eslint-plugin/src/util/getFunctionName.js @@ -0,0 +1,41 @@ +/** + * 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 getParentAssignmentName = require('./getParentAssignmentName'); + +/** + * Gets the static name of a function AST node. For function declarations it is + * easy. For anonymous function expressions it is much harder. If you search for + * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places + * where JS gives anonymous function expressions names. We roughly detect the + * same AST nodes with some exceptions to better fit our use case. + * + * @param {import('@types/eslint').Rule.Node} node + */ +module.exports = function getFunctionName(node) { + // This code was refactored from react-eslint-plugin + if ( + node.type === 'FunctionDeclaration' || + (node.type === 'FunctionExpression' && node.id) + ) { + // function useHook() {} + // const whatever = function useHook() {}; + // + // Function declaration or function expression names win over any + // assignment statements or other renames. + return node.id; + } else if ( + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ) { + return getParentAssignmentName(node); + } else { + return undefined; + } +}; diff --git a/packages/lexical-eslint-plugin/src/util/getParentAssignmentName.js b/packages/lexical-eslint-plugin/src/util/getParentAssignmentName.js new file mode 100644 index 000000000000..5c1902e14a73 --- /dev/null +++ b/packages/lexical-eslint-plugin/src/util/getParentAssignmentName.js @@ -0,0 +1,60 @@ +/** + * 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 +'use strict'; + +/** + * Gets the static name of an AST node, used to determine the name of an + * anonymous function declaration. This was extracted so it could also + * be used in the context of useCallback or useMemo, e.g. + * `const fun = useCallback(() => {}, [])` where the name is not the direct + * parent of the anonymous function. + * + * @param {import('@types/eslint').Rule.Node} node + */ +module.exports = function getParentAssignmentName(node) { + if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) { + // const useHook = () => {}; + return node.parent.id; + } else if ( + node.parent.type === 'AssignmentExpression' && + node.parent.right === node && + node.parent.operator === '=' + ) { + // useHook = () => {}; + return node.parent.left; + } else if ( + node.parent.type === 'Property' && + node.parent.value === node && + !node.parent.computed + ) { + // {useHook: () => {}} + // {useHook() {}} + return node.parent.key; + + // NOTE: We could also support `ClassProperty` and `MethodDefinition` + // here to be pedantic. However, hooks in a class are an anti-pattern. So + // we don't allow it to error early. + // + // class {useHook = () => {}} + // class {useHook() {}} + } else if ( + node.parent.type === 'AssignmentPattern' && + node.parent.right === node && + !node.parent.computed + ) { + // const {useHook = () => {}} = {}; + // ({useHook = () => {}} = {}); + // + // Kinda clowny, but we'd said we'd follow spec convention for + // `IsAnonymousFunctionDefinition()` usage. + return node.parent.left; + } else { + return undefined; + } +}; diff --git a/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow b/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow new file mode 100644 index 000000000000..27b66459534b --- /dev/null +++ b/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow @@ -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. + * + */ + +/** + * LexicalContextMenuPlugin + */ diff --git a/packages/lexical-react/flow/LexicalEditorRefPlugin.js.flow b/packages/lexical-react/flow/LexicalEditorRefPlugin.js.flow new file mode 100644 index 000000000000..88050067c04a --- /dev/null +++ b/packages/lexical-react/flow/LexicalEditorRefPlugin.js.flow @@ -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. + * + */ + +/** + * LexicalEditorRefPlugin + */ diff --git a/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow b/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow new file mode 100644 index 000000000000..a7fed0f44a06 --- /dev/null +++ b/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow @@ -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. + * + */ + +/** + * LexicalNodeEventPlugin + */ diff --git a/packages/lexical-website/docs/maintainers-guide.md b/packages/lexical-website/docs/maintainers-guide.md index cab6a21310ba..ca3221a66b93 100644 --- a/packages/lexical-website/docs/maintainers-guide.md +++ b/packages/lexical-website/docs/maintainers-guide.md @@ -60,6 +60,123 @@ TypeScript file, not a subdirectory containing an index.ts file. The [update-packages](#npm-run-update-packages) script will ensure that the exports match the files on disk. +## Creating a new package + +The first step in creating a new package is to create the workspace, there +is a [npm-init](https://docs.npmjs.com/cli/v10/commands/npm-init) template +that will fill in some of the defaults for you based on conventions. + +The example we will use is the steps that were used to create the +`lexical-eslint-plugin`, which will be published to npm as +`@lexical/eslint-plugin`. + +### Create the workspace + +``` +npm init -w packages/lexical-eslint-plugin +``` + +This only automates the first step, creating a single file: + +
+ +`packages/lexical-eslint-plugin/package.json` + + +```json +{ + "name": "@lexical/eslint-plugin", + "description": "", + "keywords": [ + "lexical", + "editor" + ], + "version": "0.14.3", + "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" +} +``` +
+ +Some next steps for this package.json before moving on: + +- Update the description +- Add appropriate keywords + +### Create the initial source file + +``` +mkdir -p packages/lexical-eslint-plugin/src +code packages/lexical-eslint-plugin/src/index.ts +``` + +Here are some minimal examples of those files that you might start out with. +I've elided the license header, the eslint header/header fixer will help you +with that! + +
+ +`packages/lexical-eslint-plugin/src/index.ts` + + +```typescript +import {name, version} from '../package.json'; + +const plugin = { + meta: {name, version}, + rules: {}, +}; + +export default plugin; +``` +
+ +### Run update-packages to generate boilerplate docs & config + +``` +npm run update-packages +``` + +This will set up the tsconfig, flow, etc. configuration to recognize your +new module. It will also create an initial README.md using only the +description from the package.json. + + +### Create an initial unit test + +``` +mkdir -p packages/lexical-eslint-plugin/src/__tests__/unit +code packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts +``` + + +
+ +`packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts` + + +```typescript +import plugin from '@lexical/eslint-plugin'; + +describe('LexicalEslintPlugin', () => { + it('exports a plugin with meta and rules', () => { + expect(Object.keys(plugin).sort()).toMatchObject(['meta', 'rules']); + }); +}); +``` +
+ + ## Scripts for development ### npm run update-packages diff --git a/packages/lexical-website/docs/packages/lexical-eslint-plugin.md b/packages/lexical-website/docs/packages/lexical-eslint-plugin.md new file mode 100644 index 000000000000..b5e3088d4ab4 --- /dev/null +++ b/packages/lexical-website/docs/packages/lexical-eslint-plugin.md @@ -0,0 +1,6 @@ +--- +title: '' +sidebar_label: '@lexical/eslint-plugin' +--- + +{@import ../../../lexical-eslint-plugin/README.md} diff --git a/scripts/npm/npm-init.js b/scripts/npm/npm-init.js new file mode 100644 index 000000000000..44ab8d4b9fef --- /dev/null +++ b/scripts/npm/npm-init.js @@ -0,0 +1,44 @@ +/** + * 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 +// npm-init can not use strict mode because PromZard is very strange +// and does not simply require this module +/* eslint-disable strict */ +/* eslint-disable sort-keys-fix/sort-keys-fix */ + +const path = require('node:path'); +const npmToWwwName = require('../www/npmToWwwName'); +const argv = require('minimist')(process.argv.slice(2)); +const {PackageMetadata} = require('../shared/PackageMetadata'); + +const lexicalPkg = new PackageMetadata('packages/lexical/package.json'); + +// npm doesn't give us a way to discover the -w argument so +const workspace = argv.w || argv.workspace; +if ( + !Array.isArray(argv._) || + argv._.join(' ') !== 'init' || + typeof workspace !== 'string' || + !/^packages\/[^/]+$/.test(workspace) +) { + throw new Error( + 'Expecting to be called as npm init -w packages/PACKAGE_NAME', + ); +} +const pkgDirName = path.basename(workspace); + +module.exports = { + name: pkgDirName.replace(/^lexical-/, '@lexical/'), + description: '', + keywords: ['lexical', 'editor'], + version: lexicalPkg.packageJson.version, + license: lexicalPkg.packageJson.license, + repository: {...lexicalPkg.packageJson.repository, directory: workspace}, + main: `${npmToWwwName(pkgDirName)}.js`, + types: 'index.d.ts', +}; diff --git a/scripts/update-flowconfig.js b/scripts/update-flowconfig.js index 50bdbca46e88..9d2e9b872f9f 100644 --- a/scripts/update-flowconfig.js +++ b/scripts/update-flowconfig.js @@ -13,9 +13,25 @@ const path = require('node:path'); const {packagesManager} = require('./shared/packagesManager'); const npmToWwwName = require('./www/npmToWwwName'); +const headerTemplate = fs.readFileSync( + path.resolve(__dirname, 'www', 'headerTemplate.js'), + 'utf8', +); + const BLOCK_REGEX = /^([\s\S]+?\n;; \[generated-start update-flowconfig\]\n)([\s\S]*?)(;; \[generated-end update-flowconfig\][\s\S]+)$/; +function flowTemplate(wwwName) { + return ( + headerTemplate.replace(/^( *\/)$/, '* @flow strict\n$1') + + ` +/** + * ${wwwName} + */` + + '\n' + ); +} + /** * @param {string} configContents * @param {string} generatedCode @@ -60,7 +76,17 @@ function updateFlowconfig(flowconfigPath = './.flowconfig') { } } else { for (const name of pkg.getExportedNpmModuleNames()) { - emit(name, resolveRelative('flow', `${npmToWwwName(name)}.js.flow`)); + const wwwName = npmToWwwName(name); + const flowFile = `${wwwName}.js.flow`; + const resolvedFlowFile = resolveRelative('flow', flowFile); + emit(name, resolveRelative('flow', flowFile)); + const flowFilePath = pkg.resolve('flow', flowFile); + if (!fs.existsSync(flowFilePath)) { + console.log( + `Creating boilerplate ${resolvedFlowFile.replace(/^[^/]+\//, '')}`, + ); + fs.writeFileSync(flowFilePath, flowTemplate(wwwName)); + } } } } diff --git a/tsconfig.build.json b/tsconfig.build.json index dc17b674c796..6b35fbf19ed5 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -13,6 +13,9 @@ "./packages/lexical-devtools-core/src/index.ts" ], "@lexical/dragon": ["./packages/lexical-dragon/src/index.ts"], + "@lexical/eslint-plugin": [ + "./packages/lexical-eslint-plugin/src/index.ts" + ], "@lexical/file": ["./packages/lexical-file/src/index.ts"], "@lexical/hashtag": ["./packages/lexical-hashtag/src/index.ts"], "@lexical/headless": ["./packages/lexical-headless/src/index.ts"], diff --git a/tsconfig.json b/tsconfig.json index ea4625439029..56ca99ffaccb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,9 @@ "./packages/lexical-devtools-core/src/index.ts" ], "@lexical/dragon": ["./packages/lexical-dragon/src/index.ts"], + "@lexical/eslint-plugin": [ + "./packages/lexical-eslint-plugin/src/index.ts" + ], "@lexical/file": ["./packages/lexical-file/src/index.ts"], "@lexical/hashtag": ["./packages/lexical-hashtag/src/index.ts"], "@lexical/headless": ["./packages/lexical-headless/src/index.ts"], @@ -179,6 +182,7 @@ "@lexical/code/src": ["./packages/lexical-code/src"], "@lexical/devtools-core/src": ["./packages/lexical-devtools-core/src"], "@lexical/dragon/src": ["./packages/lexical-dragon/src"], + "@lexical/eslint-plugin/src": ["./packages/lexical-eslint-plugin/src"], "@lexical/file/src": ["./packages/lexical-file/src"], "@lexical/hashtag/src": ["./packages/lexical-hashtag/src"], "@lexical/headless/src": ["./packages/lexical-headless/src"], @@ -213,6 +217,6 @@ "**/node_modules/**", "./packages/lexical-devtools/**" ], - "typedocOptions": {"logLevel": "Verbose"}, - "ts-node": {"require": ["tsconfig-paths/register"], "transpileOnly": true} + "typedocOptions": { "logLevel": "Verbose" }, + "ts-node": { "require": ["tsconfig-paths/register"], "transpileOnly": true } }