diff --git a/.pkgs/configs/package.json b/.pkgs/configs/package.json
index 359e3d883f..4208372ce0 100644
--- a/.pkgs/configs/package.json
+++ b/.pkgs/configs/package.json
@@ -24,8 +24,8 @@
"eslint-plugin-de-morgan": "^1.2.1",
"eslint-plugin-function": "^0.0.21",
"eslint-plugin-jsdoc": "^50.7.1",
- "eslint-plugin-perfectionist": "^4.13.0",
- "eslint-plugin-regexp": "^2.7.0",
+ "eslint-plugin-perfectionist": "^4.14.0",
+ "eslint-plugin-regexp": "^2.8.0",
"eslint-plugin-unicorn": "^59.0.1",
"typescript-eslint": "^8.33.1"
}
diff --git a/.pkgs/eslint-plugin-local/package.json b/.pkgs/eslint-plugin-local/package.json
index 0c5236f54c..4928c8bdd1 100644
--- a/.pkgs/eslint-plugin-local/package.json
+++ b/.pkgs/eslint-plugin-local/package.json
@@ -34,8 +34,8 @@
"@typescript-eslint/utils": "^8.33.1",
"eslint-plugin-de-morgan": "^1.2.1",
"eslint-plugin-jsdoc": "^50.7.1",
- "eslint-plugin-perfectionist": "^4.13.0",
- "eslint-plugin-regexp": "^2.7.0",
+ "eslint-plugin-perfectionist": "^4.14.0",
+ "eslint-plugin-regexp": "^2.8.0",
"eslint-plugin-unicorn": "^59.0.1",
"string-ts": "^2.2.1",
"ts-pattern": "^5.7.1"
diff --git a/apps/website/package.json b/apps/website/package.json
index 3cd1b81c54..8a698ae2d0 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -20,7 +20,7 @@
"fumadocs-twoslash": "3.1.3",
"fumadocs-typescript": "4.0.5",
"fumadocs-ui": "15.5.0",
- "lucide-react": "^0.511.0",
+ "lucide-react": "^0.512.0",
"next": "^15.3.3",
"next-view-transitions": "^0.3.4",
"react": "^19.1.0",
@@ -52,7 +52,7 @@
"eslint": "^9.28.0",
"eslint-plugin-de-morgan": "^1.2.1",
"eslint-plugin-import-x": "^4.15.0",
- "eslint-plugin-perfectionist": "^4.13.0",
+ "eslint-plugin-perfectionist": "^4.14.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-unicorn": "^59.0.1",
diff --git a/examples/vite-react-dom-js-with-babel-eslint-parser-app/package.json b/examples/vite-react-dom-js-with-babel-eslint-parser-app/package.json
index ea1c5684c6..536ee87350 100644
--- a/examples/vite-react-dom-js-with-babel-eslint-parser-app/package.json
+++ b/examples/vite-react-dom-js-with-babel-eslint-parser-app/package.json
@@ -16,7 +16,7 @@
},
"devDependencies": {
"@babel/core": "^7.27.4",
- "@babel/eslint-parser": "^7.27.1",
+ "@babel/eslint-parser": "^7.27.5",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@eslint/config-inspector": "^1.0.2",
diff --git a/package.json b/package.json
index a864c3b865..388a4b7e48 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
"typedoc-plugin-mdn-links": "^5.0.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.1",
- "vitest": "^3.2.0"
+ "vitest": "^3.2.1"
},
"packageManager": "pnpm@10.11.1",
"engines": {
@@ -112,7 +112,7 @@
"@types/react-dom": "^19.1.5",
"cross-spawn": "^7.0.6",
"esbuild": "^0.25.5",
- "lucide-react": "^0.511.0",
+ "lucide-react": "^0.512.0",
"next": "^15.3.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts
index 06c19260f2..ed0a909947 100644
--- a/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts
@@ -1,4 +1,10 @@
import { name, version } from "../package.json";
+
+import noUnnecessaryUseCallback from "./rules-removed/no-unnecessary-use-callback";
+import noUnnecessaryUseMemo from "./rules-removed/no-unnecessary-use-memo";
+import noUnnecessaryUsePrefix from "./rules-removed/no-unnecessary-use-prefix";
+import preferUseStateLazyInitialization from "./rules-removed/prefer-use-state-lazy-initialization";
+
import noDirectSetStateInUseEffect from "./rules/no-direct-set-state-in-use-effect";
import noDirectSetStateInUseLayoutEffect from "./rules/no-direct-set-state-in-use-layout-effect";
@@ -10,5 +16,26 @@ export const plugin = {
rules: {
"no-direct-set-state-in-use-effect": noDirectSetStateInUseEffect,
"no-direct-set-state-in-use-layout-effect": noDirectSetStateInUseLayoutEffect,
+
+ /**
+ * @deprecated Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.
+ * @see https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback
+ */
+ "no-unnecessary-use-callback": noUnnecessaryUseCallback,
+ /**
+ * @deprecated Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.
+ * @see https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo
+ */
+ "no-unnecessary-use-memo": noUnnecessaryUseMemo,
+ /**
+ * @deprecated Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.
+ * @see https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix
+ */
+ "no-unnecessary-use-prefix": noUnnecessaryUsePrefix,
+ /**
+ * @deprecated Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.
+ * @see https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization
+ */
+ "prefer-use-state-lazy-initialization": preferUseStateLazyInitialization,
},
} as const;
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.md b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.md
new file mode 100644
index 0000000000..5dc4e40d25
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.md
@@ -0,0 +1,67 @@
+---
+title: no-unnecessary-use-callback
+---
+
+**Full Name in `eslint-plugin-react-hooks-extra`**
+
+```sh copy
+react-hooks-extra/no-unnecessary-use-callback
+```
+
+**Full Name in `@eslint-react/eslint-plugin`**
+
+```sh copy
+@eslint-react/hooks-extra/no-unnecessary-use-callback
+```
+
+**Features**
+
+`🧪`
+
+## Description
+
+Disallow unnecessary usage of `useCallback`.
+
+React Hooks `useCallback` has empty dependencies array like what's in the examples, are unnecessary. The hook can be removed and it's value can be created in the component body or hoisted to the outer scope of the component.
+
+## Examples
+
+### Failing
+
+```tsx
+import React, { useCallback } from "react";
+
+function MyComponent() {
+ const onClick = useCallback(() => {
+ console.log("clicked");
+ }, []);
+
+ return ;
+}
+```
+
+### Passing
+
+```tsx
+import React from "react";
+
+function onClick() {
+ console.log("clicked");
+}
+
+function MyComponent() {
+ return ;
+}
+```
+
+## Implementation
+
+- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-callback.ts)
+- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-callback.spec.ts)
+
+---
+
+## See Also
+
+- [`no-unnecessary-use-memo`](./no-unnecessary-use-memo)\
+ Disallows unnecessary usage of `useMemo`.
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.spec.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.spec.ts
new file mode 100644
index 0000000000..3174561fc9
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.spec.ts
@@ -0,0 +1,531 @@
+// TODO: Add more tests
+import tsx from "dedent";
+
+import { allValid, ruleTester } from "../../../../../test";
+import rule, { RULE_NAME } from "./no-unnecessary-use-callback";
+
+ruleTester.run(RULE_NAME, rule, {
+ invalid: [
+ {
+ code: tsx`
+ import { useState, useCallback } from "react";
+
+ function MyComponent() {
+ const a = 1;
+ const handleSnapshot = useCallback(() => Number(1), []);
+
+ return null;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useState, useCallback } from "react";
+
+ function MyComponent() {
+ const a = 1;
+ const handleSnapshot = useCallback(() => new String("1"), []);
+
+ return null;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useCallback } from "react";
+
+ const Comp = () => {
+ const onClick = useCallback(() => {
+ console.log("clicked");
+ }, []);
+
+ return ;
+ };
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useCallback } from "react";
+
+ const deps = [];
+ const Comp = () => {
+ const onClick = useCallback(() => {
+ console.log("clicked");
+ }, deps);
+
+ return ;
+ };
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useCallback } from "react";
+
+ const Comp = () => {
+ const deps = [];
+ const onClick = useCallback(() => {
+ console.log("clicked");
+ }, deps);
+
+ return ;
+ };
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useCallback } from "react";
+
+ const Comp = () => {
+ const style = useCallback((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), []);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ const { useCallback } = require("react");
+
+ const Comp = () => {
+ const style = useCallback((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), []);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import React from "react";
+
+ const Comp = () => {
+ const style = React.useCallback((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), []);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import React from "roact";
+
+ function App({ items }) {
+ const memoizedValue = React.useCallback(() => [0, 1, 2].sort(), []);
+
+ return
{count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "roact",
+ },
+ },
+ },
+ {
+ code: tsx`
+ import Roact from "roact";
+
+ function App({ items }) {
+ const memoizedValue = Roact.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "roact",
+ },
+ },
+ },
+ {
+ code: tsx`
+ import { useCallback } from "roact";
+
+ function App({ items }) {
+ const memoizedValue = useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "roact",
+ },
+ },
+ },
+ {
+ code: tsx`
+ import React from "@pika/react";
+
+ function App({ items }) {
+ const memoizedValue = React.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "@pika/react",
+ },
+ },
+ },
+ {
+ code: tsx`
+ import Pika from "@pika/react";
+
+ function App({ items }) {
+ const memoizedValue = Pika.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "@pika/react",
+ },
+ },
+ },
+ {
+ code: tsx`
+ import { useCallback } from "@pika/react";
+
+ function App({ items }) {
+ const memoizedValue = useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "@pika/react",
+ },
+ },
+ },
+ {
+ code: tsx`
+ const React = require("roact");
+
+ function App({ items }) {
+ const memoizedValue = React.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "roact",
+ },
+ },
+ },
+ {
+ code: tsx`
+ const Roact = require("roact");
+
+ function App({ items }) {
+ const memoizedValue = Roact.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "roact",
+ },
+ },
+ },
+ {
+ code: tsx`
+ const { useCallback } = require("roact");
+
+ function App({ items }) {
+ const memoizedValue = useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "roact",
+ },
+ },
+ },
+ {
+ code: tsx`
+ const React = require("@pika/react");
+
+ function App({ items }) {
+ const memoizedValue = React.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "@pika/react",
+ },
+ },
+ },
+ {
+ code: tsx`
+ const Pika = require("@pika/react");
+
+ function App({ items }) {
+ const memoizedValue = Pika.useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "@pika/react",
+ },
+ },
+ },
+ {
+ code: tsx`
+ const { useCallback } = require("@pika/react");
+
+ function App({ items }) {
+ const memoizedValue = useCallback(() => [0, 1, 2].sort(), []);
+
+ return {count}
;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ importSource: "@pika/react",
+ },
+ },
+ },
+ {
+ code: tsx`
+ import React from "react";
+
+ const Comp = () => {
+ const style = useCustomCallback((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), []);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseCallback",
+ },
+ ],
+ settings: {
+ "react-x": {
+ additionalHooks: {
+ useCallback: ["useCustomCallback"],
+ },
+ },
+ },
+ },
+ ],
+ valid: [
+ ...allValid,
+ tsx`
+ import { useState } from "react";
+
+ const Comp = () => {
+ const [state, setState] = useState(false);
+
+ return ;
+ };
+ `,
+ tsx`
+ const useData = (key) => {
+ return useSWR(key);
+ }
+ `,
+ tsx`
+ function useData(key) {
+ return useSWR(key);
+ }
+ `,
+ tsx`
+ function useData(key) {
+ const data = useSWR(key);
+ return data;
+ }
+ `,
+ tsx`
+ const useData = (key) => useSWR(key);
+ `,
+ tsx`
+ const onClick = () => {
+ console.log("clicked");
+ };
+
+ const Comp = () => {
+ return ;
+ };
+ `,
+ tsx`
+ import { useCallback } from "react";
+
+ const Comp = ({ theme }) => {
+ const style = useCallback(() => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), [theme.fontFamilyMonospace]);
+ return
+ }
+ `,
+ tsx`
+ import { useState, useCallback } from "react";
+
+ function MyComponent() {
+ const [showSnapshot, setShowSnapshot] = useState(false);
+ const handleSnapshot = useCallback(() => setShowSnapshot(true), []);
+
+ return null;
+ }
+ `,
+ tsx`
+ import { useCallback } from "react";
+
+ const Comp = () => {
+ const [width, setWidth] = useState(undefined)
+ const [open, setOpen] = useState(false)
+ const [title, setTitle] = useState(undefined)
+
+ const refItem = useCallback(() => {
+ return {
+ setWidth,
+ setWrap: setOpen,
+ setWrapperName: setTitle,
+ }
+ }, [])
+ };
+ `,
+ tsx`
+ import { useCallback } from "react";
+ const deps = []
+ const Comp = () => {
+ const [width, setWidth] = useState(undefined)
+ const [open, setOpen] = useState(false)
+ const [title, setTitle] = useState(undefined)
+ const cb = () => {
+ return {
+ setWidth,
+ setWrap: setOpen,
+ setWrapperName: setTitle,
+ }
+ }
+ const refItem = useCallback(cb, deps)
+ };
+ `,
+ ],
+});
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.ts
new file mode 100644
index 0000000000..269888e24f
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-callback.ts
@@ -0,0 +1,136 @@
+import type { RuleContext, RuleFeature } from "@eslint-react/kit";
+import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
+import type { CamelCase } from "string-ts";
+import * as AST from "@eslint-react/ast";
+import * as ER from "@eslint-react/core";
+import { identity } from "@eslint-react/eff";
+import { getSettingsFromContext } from "@eslint-react/shared";
+import * as VAR from "@eslint-react/var";
+import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
+import { match } from "ts-pattern";
+
+import { createRule } from "../utils";
+
+export const RULE_NAME = "no-unnecessary-use-callback";
+
+export const RULE_FEATURES = [
+ "EXP",
+] as const satisfies RuleFeature[];
+
+export type MessageID = CamelCase;
+
+export default createRule<[], MessageID>({
+ meta: {
+ type: "problem",
+ deprecated: {
+ deprecatedSince: "2.0.0",
+ replacedBy: [
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "eslint-plugin-react-x",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x",
+ },
+ rule: {
+ name: "no-unnecessary-use-callback",
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback",
+ },
+ },
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "@eslint-react/eslint-plugin",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin",
+ },
+ rule: {
+ name: "no-unnecessary-use-callback",
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback",
+ },
+ },
+ ],
+ },
+ docs: {
+ description: "Disallow unnecessary usage of `useCallback`.",
+ [Symbol.for("rule_features")]: RULE_FEATURES,
+ },
+ messages: {
+ noUnnecessaryUseCallback:
+ "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
+ },
+ schema: [],
+ },
+ name: RULE_NAME,
+ create,
+ defaultOptions: [],
+});
+
+export function create(context: RuleContext): RuleListener {
+ if (!context.sourceCode.text.includes("use")) return {};
+ const alias = getSettingsFromContext(context).additionalHooks.useCallback ?? [];
+ const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", alias);
+ return {
+ CallExpression(node) {
+ if (!ER.isReactHookCall(node)) {
+ return;
+ }
+ const initialScope = context.sourceCode.getScope(node);
+ if (!isUseCallbackCall(node)) {
+ return;
+ }
+ const scope = context.sourceCode.getScope(node);
+ const component = scope.block;
+ if (!AST.isFunction(component)) {
+ return;
+ }
+ const [arg0, arg1] = node.arguments;
+ if (arg0 == null || arg1 == null) {
+ return;
+ }
+
+ const hasEmptyDeps = match(arg1)
+ .with({ type: T.ArrayExpression }, (n) => n.elements.length === 0)
+ .with({ type: T.Identifier }, (n) => {
+ const variable = VAR.findVariable(n.name, initialScope);
+ const variableNode = VAR.getVariableInitNode(variable, 0);
+ if (variableNode?.type !== T.ArrayExpression) {
+ return false;
+ }
+ return variableNode.elements.length === 0;
+ })
+ .otherwise(() => false);
+
+ if (!hasEmptyDeps) {
+ return;
+ }
+ const arg0Node = match(arg0)
+ .with({ type: T.ArrowFunctionExpression }, (n) => {
+ if (n.body.type === T.ArrowFunctionExpression) {
+ return n.body;
+ }
+ return n;
+ })
+ .with({ type: T.FunctionExpression }, identity)
+ .with({ type: T.Identifier }, (n) => {
+ const variable = VAR.findVariable(n.name, initialScope);
+ const variableNode = VAR.getVariableInitNode(variable, 0);
+ if (variableNode?.type !== T.ArrowFunctionExpression && variableNode?.type !== T.FunctionExpression) {
+ return null;
+ }
+ return variableNode;
+ })
+ .otherwise(() => null);
+ if (arg0Node == null) return;
+
+ const arg0NodeScope = context.sourceCode.getScope(arg0Node);
+ const arg0NodeReferences = VAR.getChidScopes(arg0NodeScope).flatMap((x) => x.references);
+ const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
+
+ if (!isReferencedToComponentScope) {
+ context.report({
+ messageId: "noUnnecessaryUseCallback",
+ node,
+ });
+ }
+ },
+ };
+}
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.md b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.md
new file mode 100644
index 0000000000..4265b0f9b5
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.md
@@ -0,0 +1,75 @@
+---
+title: no-unnecessary-use-memo
+---
+
+**Full Name in `eslint-plugin-react-hooks-extra`**
+
+```sh copy
+react-hooks-extra/no-unnecessary-use-memo
+```
+
+**Full Name in `@eslint-react/eslint-plugin`**
+
+```sh copy
+@eslint-react/hooks-extra/no-unnecessary-use-memo
+```
+
+**Features**
+
+`🧪`
+
+## Description
+
+Disallow unnecessary usage of `useMemo`.
+
+React Hooks `useMemo` has empty dependencies array like what's in the examples, are unnecessary. The hook can be removed and it's value can be calculated in the component body or hoisted to the outer scope of the component.
+
+## Examples
+
+### Failing
+
+```tsx
+import React, { useMemo } from "react";
+import { Button, MantineTheme } from "@mantine/core";
+
+function MyComponent() {
+ const style = useMemo(
+ (theme: MantineTheme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace,
+ },
+ }),
+ [],
+ );
+ return ;
+}
+```
+
+### Passing
+
+```tsx
+import React from "react";
+import { Button, MantineTheme } from "@mantine/core";
+
+const style = (theme: MantineTheme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace,
+ },
+});
+
+function MyComponent() {
+ return ;
+}
+```
+
+## Implementation
+
+- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-memo.ts)
+- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-memo.spec.ts)
+
+---
+
+## See Also
+
+- [`no-unnecessary-use-callback`](./no-unnecessary-use-callback)\
+ Disallows unnecessary usage of `useCallback`.
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.spec.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.spec.ts
new file mode 100644
index 0000000000..723bc1067c
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.spec.ts
@@ -0,0 +1,244 @@
+import tsx from "dedent";
+
+import { allValid, ruleTester } from "../../../../../test";
+import rule, { RULE_NAME } from "./no-unnecessary-use-memo";
+
+ruleTester.run(RULE_NAME, rule, {
+ invalid: [
+ {
+ code: tsx`
+ import { useMemo } from "react";
+
+ const Comp = () => {
+ const style = useMemo((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), []);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseMemo",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useMemo } from "react";
+
+ const deps = [];
+ const Comp = () => {
+ const style = useMemo((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), deps);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseMemo",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useMemo } from "react";
+
+ const Comp = () => {
+ const deps = [];
+ const style = useMemo((theme) => ({
+ input: {
+ fontFamily: theme.fontFamilyMonospace
+ }
+ }), deps);
+ return
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseMemo",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const handleSnapshot = useMemo(() => () => console.log(true), []);
+
+ return null;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseMemo",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const handleSnapshot = useMemo(() => () => () => console.log(true), []);
+
+ return null;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUseMemo",
+ },
+ ],
+ },
+ ],
+ valid: [
+ ...allValid,
+ tsx`
+ import { useState } from "react";
+
+ const Comp = () => {
+ const [state, setState] = useState(false);
+
+ return ;
+ };
+ `,
+ tsx`
+ const useData = (key) => {
+ return useSWR(key);
+ }
+ `,
+ tsx`
+ function useData(key) {
+ return useSWR(key);
+ }
+ `,
+ tsx`
+ function useData(key) {
+ const data = useSWR(key);
+ return data;
+ }
+ `,
+ tsx`
+ const useData = (key) => useSWR(key);
+ `,
+ tsx`
+ const onClick = () => {
+ console.log("clicked");
+ };
+
+ const Comp = () => {
+ return ;
+ };
+ `,
+ tsx`
+ import { useMemo } from "react";
+
+ function App({ items }) {
+ const memoizedValue = useMemo(() => [...items].sort(), [items]);
+ return {count}
;
+ }
+ `,
+ tsx`
+ import { useMemo } from "react";
+
+ const Comp = () => {
+ const [width, setWidth] = useState(undefined)
+ const [open, setOpen] = useState(false)
+ const [title, setTitle] = useState(undefined)
+
+ const refItem = useMemo(() => {
+ return {
+ setWidth,
+ setWrap: setOpen,
+ setWrapperName: setTitle,
+ }
+ }, [])
+ };
+ `,
+ tsx`
+ import { useMemo } from "react";
+ const deps = []
+ const Comp = () => {
+ const [width, setWidth] = useState(undefined)
+ const [open, setOpen] = useState(false)
+ const [title, setTitle] = useState(undefined)
+ const cb = () => {
+ return {
+ setWidth,
+ setWrap: setOpen,
+ setWrapperName: setTitle,
+ }
+ }
+ const refItem = useMemo(cb, deps)
+ };
+ `,
+ tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const [showSnapshot, setShowSnapshot] = useState(false);
+ const handleSnapshot = useMemo(() => {
+ return () => setShowSnapshot(true)
+ }, []);
+
+ return null;
+ }
+ `,
+ tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const [showSnapshot, setShowSnapshot] = useState(false);
+ const handleSnapshot = useMemo(() => () => setShowSnapshot(true), []);
+
+ return null;
+ }
+ `,
+ tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const [showSnapshot, setShowSnapshot] = useState(false);
+ const handleSnapshot = useMemo(() => () => () => setShowSnapshot(true), []);
+
+ return null;
+ }
+ `,
+ tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const a = 1;
+ const handleSnapshot = useMemo(() => () => () => console.log(a), []);
+
+ return null;
+ }
+ `,
+ tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const a = 1;
+ const handleSnapshot = useMemo(() => Date.now(), []);
+
+ return null;
+ }
+ `,
+ tsx`
+ import { useState, useMemo } from "react";
+
+ function MyComponent() {
+ const a = 1;
+ const handleSnapshot = useMemo(() => new Date(), []);
+
+ return null;
+ }
+ `,
+ ],
+});
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.ts
new file mode 100644
index 0000000000..89f7fe6cc5
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-memo.ts
@@ -0,0 +1,141 @@
+import type { RuleContext, RuleFeature } from "@eslint-react/kit";
+import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
+import type { CamelCase } from "string-ts";
+import * as AST from "@eslint-react/ast";
+import * as ER from "@eslint-react/core";
+import { identity } from "@eslint-react/eff";
+import { getSettingsFromContext } from "@eslint-react/shared";
+import * as VAR from "@eslint-react/var";
+import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
+import { match } from "ts-pattern";
+
+import { createRule } from "../utils";
+
+export const RULE_NAME = "no-unnecessary-use-memo";
+
+export const RULE_FEATURES = [
+ "EXP",
+] as const satisfies RuleFeature[];
+
+export type MessageID = CamelCase;
+
+export default createRule<[], MessageID>({
+ meta: {
+ type: "problem",
+ deprecated: {
+ deprecatedSince: "2.0.0",
+ replacedBy: [
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "eslint-plugin-react-x",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x",
+ },
+ rule: {
+ name: "no-unnecessary-use-memo",
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo",
+ },
+ },
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "@eslint-react/eslint-plugin",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin",
+ },
+ rule: {
+ name: "no-unnecessary-use-memo",
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo",
+ },
+ },
+ ],
+ },
+ docs: {
+ description: "Disallow unnecessary usage of `useMemo`.",
+ [Symbol.for("rule_features")]: RULE_FEATURES,
+ },
+ messages: {
+ noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
+ },
+ schema: [],
+ },
+ name: RULE_NAME,
+ create,
+ defaultOptions: [],
+});
+
+export function create(context: RuleContext): RuleListener {
+ if (!context.sourceCode.text.includes("use")) return {};
+ const alias = getSettingsFromContext(context).additionalHooks.useMemo ?? [];
+ const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", alias);
+ return {
+ CallExpression(node) {
+ if (!ER.isReactHookCall(node)) {
+ return;
+ }
+ const initialScope = context.sourceCode.getScope(node);
+ if (!isUseMemoCall(node)) {
+ return;
+ }
+ const scope = context.sourceCode.getScope(node);
+ const component = scope.block;
+ if (!AST.isFunction(component)) {
+ return;
+ }
+ const [arg0, arg1] = node.arguments;
+ if (arg0 == null || arg1 == null) {
+ return;
+ }
+ const hasCallInArg0 = AST.isFunction(arg0)
+ && [...AST.getNestedCallExpressions(arg0.body), ...AST.getNestedNewExpressions(arg0.body)].length > 0;
+
+ if (hasCallInArg0) {
+ return;
+ }
+
+ const hasEmptyDeps = match(arg1)
+ .with({ type: T.ArrayExpression }, (n) => n.elements.length === 0)
+ .with({ type: T.Identifier }, (n) => {
+ const variable = VAR.findVariable(n.name, initialScope);
+ const variableNode = VAR.getVariableInitNode(variable, 0);
+ if (variableNode?.type !== T.ArrayExpression) {
+ return false;
+ }
+ return variableNode.elements.length === 0;
+ })
+ .otherwise(() => false);
+
+ if (!hasEmptyDeps) {
+ return;
+ }
+ const arg0Node = match(arg0)
+ .with({ type: T.ArrowFunctionExpression }, (n) => {
+ if (n.body.type === T.ArrowFunctionExpression) {
+ return n.body;
+ }
+ return n;
+ })
+ .with({ type: T.FunctionExpression }, identity)
+ .with({ type: T.Identifier }, (n) => {
+ const variable = VAR.findVariable(n.name, initialScope);
+ const variableNode = VAR.getVariableInitNode(variable, 0);
+ if (variableNode?.type !== T.ArrowFunctionExpression && variableNode?.type !== T.FunctionExpression) {
+ return null;
+ }
+ return variableNode;
+ })
+ .otherwise(() => null);
+ if (arg0Node == null) return;
+
+ const arg0NodeScope = context.sourceCode.getScope(arg0Node);
+ const arg0NodeReferences = VAR.getChidScopes(arg0NodeScope).flatMap((x) => x.references);
+ const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
+
+ if (!isReferencedToComponentScope) {
+ context.report({
+ messageId: "noUnnecessaryUseMemo",
+ node,
+ });
+ }
+ },
+ };
+}
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.md b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.md
new file mode 100644
index 0000000000..0c033b8304
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.md
@@ -0,0 +1,136 @@
+---
+title: no-unnecessary-use-prefix
+---
+
+**Full Name in `eslint-plugin-react-hooks-extra`**
+
+```sh copy
+react-hooks-extra/no-unnecessary-use-prefix
+```
+
+**Full Name in `@eslint-react/eslint-plugin`**
+
+```sh copy
+@eslint-react/hooks-extra/no-unnecessary-use-prefix
+```
+
+**Features**
+
+`🧪`
+
+**Presets**
+
+- `recommended`
+- `recommended-typescript`
+- `recommended-type-checked`
+
+## Description
+
+Enforces that a function with the 'use' prefix should use at least one Hook inside of it.
+
+If your function doesn’t call any Hooks, avoid the `use` prefix. Instead, write it as a regular function without the `use` prefix. For example, `useSorted` below doesn’t call Hooks, so call it `getSorted` instead:
+
+```tsx
+// 🔴 Avoid: A Hook that doesn't use Hooks
+function useSorted(items) {
+ return items.slice().sort();
+}
+```
+
+```tsx
+// ✅ Good: A regular function that doesn't use Hooks
+function getSorted(items) {
+ return items.slice().sort();
+}
+```
+
+This ensures that your code can call this regular function anywhere, including conditions:
+
+```tsx
+function List({ items, shouldSort }) {
+ let displayedItems = items;
+ if (shouldSort) {
+ // ✅ It's ok to call getSorted() conditionally because it's not a Hook
+ displayedItems = getSorted(items);
+ }
+ // ...
+}
+```
+
+You should give `use` prefix to a function (and thus make it a Hook) if it uses at least one Hook inside of it:
+
+```tsx
+// ✅ Good: A Hook that uses other Hooks
+function useAuth() {
+ return useContext(Auth);
+}
+```
+
+Technically, this isn’t enforced by React. In principle, you could make a Hook that doesn’t call other Hooks. This is often confusing and limiting so it's best to avoid that pattern. However, there may be rare cases where it is helpful. For example, maybe your function doesn’t use any Hooks right now, but you plan to add some Hook calls to it in the future. Then it makes sense to name it with the `use` prefix:
+
+```tsx
+// ✅ Good: A Hook that will likely use some other Hooks later
+function useAuth() {
+ // TODO: Replace with this line when authentication is implemented:
+ // return useContext(Auth);
+ return TEST_USER;
+}
+```
+
+Then components won’t be able to call it conditionally. This will become important when you actually add Hook calls inside. If you don’t plan to use Hooks inside it (now or later), don’t make it a Hook.
+
+## Examples
+
+### Failing
+
+```tsx
+function useSorted(items) {
+ return items.slice().sort();
+}
+```
+
+```tsx
+// No 'TODO' and 'useContext()' comments inside function body
+function useAuth() {
+ return TEST_USER;
+}
+```
+
+### Passing
+
+```tsx
+function getSorted(items) {
+ return items.slice().sort();
+}
+```
+
+```tsx
+function useAuth() {
+ return useContext(Auth);
+}
+```
+
+```tsx
+function useAuth() {
+ // TODO: Replace with this line when authentication is implemented:
+ // return useContext(Auth);
+ return TEST_USER;
+}
+```
+
+```tsx
+export function useMDXComponents(components) {
+ return {
+ ...components,
+ };
+}
+```
+
+## Implementation
+
+- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-prefix.ts)
+- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-unnecessary-use-prefix.spec.ts)
+
+## Further Reading
+
+- [React Docs: Should all functions called during rendering start with the `use` prefix? (the deep dive)](https://react.dev/learn/reusing-logic-with-custom-hooks#should-all-functions-called-during-rendering-start-with-the-use-prefix)
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.spec.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.spec.ts
new file mode 100644
index 0000000000..037b3a5b87
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.spec.ts
@@ -0,0 +1,199 @@
+import tsx from "dedent";
+
+import { allValid, ruleTester } from "../../../../../test";
+import rule, { RULE_NAME } from "./no-unnecessary-use-prefix";
+
+ruleTester.run(RULE_NAME, rule, {
+ invalid: [
+ {
+ code: tsx`
+ const useClassnames = (obj) => {
+ var k, cls='';
+ for (k in obj) {
+ if (obj[k]) {
+ cls && (cls += ' ');
+ cls += k;
+ }
+ }
+ return cls;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useClassnames",
+ },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ function useClassnames(obj) {
+ var k, cls='';
+ for (k in obj) {
+ if (obj[k]) {
+ cls && (cls += ' ');
+ cls += k;
+ }
+ }
+ return cls;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useClassnames",
+ },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ export function useNestedHook() {
+ const [state, setState] = useState("state");
+ function useInnerHook () {
+ return "inner hook";
+ };
+
+ return [state, setState, useInnerHook] as const;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useInnerHook",
+ },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ export function useNestedHook() {
+ const useInnerHook = () => {
+ const [state, setState] = useState("state");
+ return state;
+ };
+
+ return [state, setState, useInnerHook] as const;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useNestedHook",
+ },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ export function useNestedHook() {
+ const useInnerHook = () => {
+ return "inner hook";
+ };
+
+ return null
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useNestedHook",
+ },
+ },
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useInnerHook",
+ },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ export function useNestedHook() {
+ const fn = () => {
+ const [state, setState] = useState("state");
+ return state;
+ };
+
+ return [state, setState, useInnerHook] as const;
+ }
+ `,
+ errors: [
+ {
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name: "useNestedHook",
+ },
+ },
+ ],
+ },
+ ],
+ valid: [
+ ...allValid,
+ tsx`
+ // Allow empty functions.
+ const useNoop = () => {};
+ `,
+ tsx`
+ export const userInitials = () => {
+ return;
+ };
+ `,
+ tsx`
+ import { useState } from "react";
+
+ const Comp = () => {
+ const [state, setState] = useState(false);
+
+ return ;
+ };
+ `,
+ tsx`
+ const useData = (key) => {
+ return useSWR(key);
+ }
+ `,
+ tsx`
+ const useData = (key) => {
+ return swr.useSWR(key);
+ }
+ `,
+ tsx`
+ function useData(key) {
+ return useSWR(key);
+ }
+ `,
+ tsx`
+ function useData(key) {
+ const data = useSWR(key);
+ return data;
+ }
+ `,
+ tsx`
+ const useData = (key) => useSWR(key);
+ `,
+ tsx`
+ function useAuth() {
+ // TODO: Replace with this line when authentication is implemented:
+ // return useContext(Auth);
+ return TEST_USER;
+ }
+ `,
+ tsx`
+ import type { MDXComponents } from 'mdx/types'
+
+ export function useMDXComponents(components: MDXComponents): MDXComponents {
+ return {
+ ...components,
+ }
+ }
+ `,
+ ],
+});
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.ts
new file mode 100644
index 0000000000..9118415fcc
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/no-unnecessary-use-prefix.ts
@@ -0,0 +1,129 @@
+import type { RuleContext, RuleFeature } from "@eslint-react/kit";
+import type { TSESTree } from "@typescript-eslint/types";
+import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
+import type { CamelCase } from "string-ts";
+import * as AST from "@eslint-react/ast";
+import * as ER from "@eslint-react/core";
+
+import { createRule } from "../utils";
+
+export const RULE_NAME = "no-unnecessary-use-prefix";
+
+export const RULE_FEATURES = [] as const satisfies RuleFeature[];
+
+export type MessageID = CamelCase;
+
+const WELL_KNOWN_HOOKS = [
+ "useMDXComponents",
+];
+
+function containsUseComments(context: RuleContext, node: TSESTree.Node) {
+ return context.sourceCode
+ .getCommentsInside(node)
+ .some(({ value }) => /use\([\s\S]*?\)/u.test(value) || /use[A-Z0-9]\w*\([\s\S]*?\)/u.test(value));
+}
+
+export default createRule<[], MessageID>({
+ meta: {
+ type: "problem",
+ deprecated: {
+ deprecatedSince: "2.0.0",
+ replacedBy: [
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "eslint-plugin-react-x",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x",
+ },
+ rule: {
+ name: "no-unnecessary-use-prefix",
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix",
+ },
+ },
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "@eslint-react/eslint-plugin",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin",
+ },
+ rule: {
+ name: "no-unnecessary-use-prefix",
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix",
+ },
+ },
+ ],
+ },
+ docs: {
+ description: "Enforces that a function with the `use` prefix should use at least one Hook inside of it.",
+ [Symbol.for("rule_features")]: RULE_FEATURES,
+ },
+ messages: {
+ noUnnecessaryUsePrefix:
+ "If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix.",
+ },
+ schema: [],
+ },
+ name: RULE_NAME,
+ create,
+ defaultOptions: [],
+});
+
+export function create(context: RuleContext): RuleListener {
+ const { ctx, listeners } = ER.useHookCollector();
+ return {
+ ...listeners,
+ "Program:exit"(program) {
+ const allHooks = ctx.getAllHooks(program);
+ for (const { id, name, node, hookCalls } of allHooks.values()) {
+ // Skip well-known hooks
+ if (WELL_KNOWN_HOOKS.includes(name)) {
+ continue;
+ }
+ // Skip empty functions
+ if (AST.isEmptyFunction(node)) {
+ continue;
+ }
+ // Skip useful hooks
+ if (hookCalls.length > 0) {
+ continue;
+ }
+ // Skip hooks with comments that contain calls to other hooks
+ if (containsUseComments(context, node)) {
+ continue;
+ }
+ if (id != null) {
+ context.report({
+ messageId: "noUnnecessaryUsePrefix",
+ data: {
+ name,
+ },
+ loc: getPreferredLoc(context, id),
+ });
+ continue;
+ }
+ context.report({
+ messageId: "noUnnecessaryUsePrefix",
+ node,
+ data: {
+ name,
+ },
+ });
+ }
+ },
+ };
+}
+
+function getPreferredLoc(context: RuleContext, id: TSESTree.Identifier) {
+ if (AST.isMultiLine(id)) return id.loc;
+ if (!context.sourceCode.getText(id).startsWith("use")) return id.loc;
+ return {
+ end: {
+ column: id.loc.start.column + 3,
+ line: id.loc.start.line,
+ },
+ start: {
+ column: id.loc.start.column,
+ line: id.loc.start.line,
+ },
+ };
+}
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.md b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.md
new file mode 100644
index 0000000000..0f40c40efd
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.md
@@ -0,0 +1,72 @@
+---
+title: prefer-use-state-lazy-initialization
+---
+
+**Full Name in `eslint-plugin-react-hooks-extra`**
+
+```sh copy
+react-hooks-extra/prefer-use-state-lazy-initialization
+```
+
+**Full Name in `@eslint-react/eslint-plugin`**
+
+```sh copy
+@eslint-react/hooks-extra/prefer-use-state-lazy-initialization
+```
+
+**Presets**
+
+- `recommended`
+- `recommended-typescript`
+- `recommended-type-checked`
+
+## Description
+
+Enforces function calls made inside `useState` to be wrapped in an `initializer function`.
+
+A function can be invoked inside a useState call to help create its initial state. However, subsequent renders will still invoke the function while discarding its return value. This is wasteful and can cause performance issues if the function call is expensive.
+
+To combat this issue React allows useState calls to use an [initializer function](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state) which will only be called on the first render.
+
+## Examples
+
+### Failing
+
+```tsx
+import React, { useState } from "react";
+
+function MyComponent() {
+ const [value, setValue] = useState(generateTodos());
+ // ^^^^^^^^^^^^^^^
+ // - Don't call a function directly inside the 'useState' call.
+
+ return null;
+}
+
+declare function generateTodos(): string[];
+```
+
+### Passing
+
+```tsx
+import React, { useState } from "react";
+
+function MyComponent() {
+ // 🟢 Good: Use an initializer function to avoid recreating the initial state
+ const [value, setValue] = useState(() => generateTodos());
+
+ return null;
+}
+
+declare function generateTodos(): string[];
+```
+
+## Implementation
+
+- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.ts)
+- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.spec.ts)
+
+## Further Reading
+
+- [React Docs: `useState` Hook](https://react.dev/reference/react/useState#setstate)
+ - # [Avoiding recreating the initial state](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state)
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.spec.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.spec.ts
new file mode 100644
index 0000000000..2061388b92
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.spec.ts
@@ -0,0 +1,262 @@
+import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
+import tsx from "dedent";
+
+import { allValid, ruleTester } from "../../../../../test";
+import rule, { RULE_NAME } from "./prefer-use-state-lazy-initialization";
+
+ruleTester.run(RULE_NAME, rule, {
+ invalid: [
+ {
+ code: `import { useState } from "react"; useState(1 || getValue())`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(2 < getValue())`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(1 < 2 ? getValue() : 4)`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(a ? b : getValue())`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(getValue() ? b : c)`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(a ? (b ? getValue() : b2) : c)`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(getValue() && b)`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(a() && new Foo())`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ {
+ type: T.NewExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(+getValue())`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState(getValue() + 1)`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState([getValue()])`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: `import { useState } from "react"; useState({ a: getValue() })`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ },
+ {
+ code: tsx`
+ import { useState, use } from 'react';
+
+ function Component({data}) {
+ const [data, setData] = useState(data ? use(data) : getValue());
+ return null;
+ }
+ `,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
+ {
+ code: tsx`useLocalStorageState(1 || getValue())`,
+ errors: [
+ {
+ type: T.CallExpression,
+ messageId: "preferUseStateLazyInitialization",
+ },
+ ],
+ settings: {
+ "react-x": {
+ additionalHooks: {
+ useState: ["useLocalStorageState"],
+ },
+ },
+ },
+ },
+ ],
+ valid: [
+ ...allValid,
+ "useState()",
+ 'useState("")',
+ "useState(true)",
+ "useState(false)",
+ "useState(null)",
+ "useState(undefined)",
+ "useState(1)",
+ 'useState("test")',
+ "useState(value)",
+ "useState(object.value)",
+ "useState(1 || 2)",
+ "useState(1 || 2 || 3 < 4)",
+ "useState(1 && 2)",
+ "useState(1 < 2)",
+ "useState(1 < 2 ? 3 : 4)",
+ "useState(1 == 2 ? 3 : 4)",
+ "useState(1 === 2 ? 3 : 4)",
+ "React.useState()",
+ 'React.useState("")',
+ "React.useState(true)",
+ "React.useState(false)",
+ "React.useState(null)",
+ "React.useState(undefined)",
+ "React.useState(1)",
+ 'React.useState("test")',
+ "React.useState(value)",
+ "React.useState(object.value)",
+ "React.useState(1 || 2)",
+ "React.useState(1 || 2 || 3 < 4)",
+ "React.useState(1 && 2)",
+ "React.useState(1 < 2)",
+ "React.useState(1 < 2 ? 3 : 4)",
+ "React.useState(1 == 2 ? 3 : 4)",
+ "React.useState(1 === 2 ? 3 : 4)",
+ 'import { useState } from "react"; useState()',
+ 'import { useState } from "react"; useState(() => JSON.parse("{}"))',
+ 'import { useState } from "react"; useState("")',
+ 'import { useState } from "react"; useState(true)',
+ 'import { useState } from "react"; useState(false)',
+ 'import { useState } from "react"; useState(null)',
+ 'import { useState } from "react"; useState(undefined)',
+ 'import { useState } from "react"; useState(1)',
+ 'import { useState } from "react"; useState("test")',
+ 'import { useState } from "react"; useState(value)',
+ 'import { useState } from "react"; useState(object.value)',
+ 'import { useState } from "react"; useState(1 || 2)',
+ 'import { useState } from "react"; useState(1 || 2 || 3 < 4)',
+ 'import { useState } from "react"; useState(1 && 2)',
+ 'import { useState } from "react"; useState(1 < 2)',
+ 'import { useState } from "react"; useState(1 < 2 ? 3 : 4)',
+ 'import { useState } from "react"; useState(1 == 2 ? 3 : 4)',
+ 'import { useState } from "react"; useState(1 === 2 ? 3 : 4)',
+ 'const { useState } = require("react"); useState()',
+ 'const { useState } = require("react"); useState("")',
+ 'const { useState } = require("react"); useState(true)',
+ 'const { useState } = require("react"); useState(false)',
+ 'const { useState } = require("react"); useState(null)',
+ 'const { useState } = require("react"); useState(undefined)',
+ 'const { useState } = require("react"); useState(1)',
+ 'const { useState } = require("react"); useState("test")',
+ 'const { useState } = require("react"); useState(value)',
+ 'const { useState } = require("react"); useState(object.value)',
+ 'const { useState } = require("react"); useState(1 || 2)',
+ 'const { useState } = require("react"); useState(1 || 2 || 3 < 4)',
+ 'const { useState } = require("react"); useState(1 && 2)',
+ 'const { useState } = require("react"); useState(1 < 2)',
+ 'const { useState } = require("react"); useState(1 < 2 ? 3 : 4)',
+ 'const { useState } = require("react"); useState(1 == 2 ? 3 : 4)',
+ 'const { useState } = require("react"); useState(1 === 2 ? 3 : 4)',
+ "const [id, setId] = useState(useId());",
+ "const [state, setState] = useState(use(promise));",
+ "const [serverData, setLikes] = useState(use(getLikes()));",
+ "const [data, setData] = useState(use(getData()) || []);",
+ "const [character, setCharacter] = useState(use(props.character) ?? undefined);",
+ {
+ code: tsx`
+ import { useState, use } from 'react';
+
+ function Shell({data}) {
+ const [root, setRoot] = useState(use(data));
+ updateRoot = setRoot;
+ return root;
+ }
+ `,
+ settings: {
+ "react-x": {
+ version: "19.0.0",
+ },
+ },
+ },
+ {
+ code: "useLocalStorage(() => JSON.parse('{}'))",
+ settings: {
+ "react-x": {
+ additionalHooks: {
+ useState: ["useLocalStorage"],
+ },
+ },
+ },
+ },
+ ],
+});
diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.ts
new file mode 100644
index 0000000000..178cf4a197
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules-removed/prefer-use-state-lazy-initialization.ts
@@ -0,0 +1,108 @@
+// Ported from https://github.com/jsx-eslint/eslint-plugin-react/pull/3579/commits/ebb739a0fe99a2ee77055870bfda9f67a2691374
+import type { RuleContext, RuleFeature } from "@eslint-react/kit";
+import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
+import type { CamelCase } from "string-ts";
+import * as AST from "@eslint-react/ast";
+import * as ER from "@eslint-react/core";
+import { getSettingsFromContext } from "@eslint-react/shared";
+
+import { createRule } from "../utils";
+
+export const RULE_NAME = "prefer-use-state-lazy-initialization";
+
+export const RULE_FEATURES = [
+ "EXP",
+] as const satisfies RuleFeature[];
+
+export type MessageID = CamelCase;
+
+// identifier names for allowed function names
+const ALLOW_LIST = [
+ "Boolean",
+ "String",
+ "Number",
+];
+
+// rule takes inspiration from https://github.com/facebook/react/issues/26520
+export default createRule<[], MessageID>({
+ meta: {
+ type: "problem",
+ deprecated: {
+ deprecatedSince: "2.0.0",
+ replacedBy: [
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "eslint-plugin-react-x",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x",
+ },
+ rule: {
+ name: "prefer-use-state-lazy-initialization",
+ url: "https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization",
+ },
+ },
+ {
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
+ plugin: {
+ name: "@eslint-react/eslint-plugin",
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin",
+ },
+ rule: {
+ name: "prefer-use-state-lazy-initialization",
+ url: "https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization",
+ },
+ },
+ ],
+ },
+ docs: {
+ description: "Enforces function calls made inside `useState` to be wrapped in an `initializer function`.",
+ [Symbol.for("rule_features")]: RULE_FEATURES,
+ },
+ messages: {
+ preferUseStateLazyInitialization:
+ "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'.",
+ },
+ schema: [],
+ },
+ name: RULE_NAME,
+ create,
+ defaultOptions: [],
+});
+
+export function create(context: RuleContext): RuleListener {
+ const alias = getSettingsFromContext(context).additionalHooks.useState ?? [];
+ const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", alias);
+ return {
+ CallExpression(node) {
+ if (!ER.isReactHookCall(node)) {
+ return;
+ }
+ if (!isUseStateCall(node)) {
+ return;
+ }
+ const [useStateInput] = node.arguments;
+ if (useStateInput == null) {
+ return;
+ }
+ for (const expr of AST.getNestedNewExpressions(useStateInput)) {
+ if (!("name" in expr.callee)) continue;
+ if (ALLOW_LIST.includes(expr.callee.name)) continue;
+ if (AST.findParentNode(expr, (n) => ER.isUseCall(context, n)) != null) continue;
+ context.report({
+ messageId: "preferUseStateLazyInitialization",
+ node: expr,
+ });
+ }
+ for (const expr of AST.getNestedCallExpressions(useStateInput)) {
+ if (!("name" in expr.callee)) continue;
+ if (ER.isReactHookName(expr.callee.name)) continue;
+ if (ALLOW_LIST.includes(expr.callee.name)) continue;
+ if (AST.findParentNode(expr, (n) => ER.isUseCall(context, n)) != null) continue;
+ context.report({
+ messageId: "preferUseStateLazyInitialization",
+ node: expr,
+ });
+ }
+ },
+ };
+}
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-hooks/use-no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules-hooks/use-no-direct-set-state-in-use-effect.ts
new file mode 100644
index 0000000000..b0d811719e
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-x/src/rules-hooks/use-no-direct-set-state-in-use-effect.ts
@@ -0,0 +1,341 @@
+import type { RuleContext } from "@eslint-react/kit";
+import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+import type { Scope } from "@typescript-eslint/utils/ts-eslint";
+import * as AST from "@eslint-react/ast";
+import * as ER from "@eslint-react/core";
+import { constVoid, getOrElseUpdate, not } from "@eslint-react/eff";
+import { getSettingsFromContext } from "@eslint-react/shared";
+import * as VAR from "@eslint-react/var";
+import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
+
+import { match } from "ts-pattern";
+
+type CallKind =
+ | "useEffect"
+ | "useLayoutEffect"
+ | "useState"
+ | "setState"
+ | "then"
+ | "other";
+
+type FunctionKind =
+ | "setup"
+ | "cleanup"
+ | "deferred"
+ | "immediate"
+ | "other";
+
+export declare namespace useNoDirectSetStateInUseEffect {
+ type Options = {
+ onViolation: (context: Ctx, node: TSESTree.Node | TSESTree.Token, data: { name: string }) => void;
+ useEffectKind: "useEffect" | "useLayoutEffect";
+ };
+ type ReturnType = ESLintUtils.RuleListener;
+}
+
+export function useNoDirectSetStateInUseEffect(
+ context: Ctx,
+ options: useNoDirectSetStateInUseEffect.Options,
+): useNoDirectSetStateInUseEffect.ReturnType {
+ const { onViolation, useEffectKind } = options;
+ const settings = getSettingsFromContext(context);
+ const hooks = settings.additionalHooks;
+ const getText = (n: TSESTree.Node) => context.sourceCode.getText(n);
+ const isUseEffectLikeCall = ER.isReactHookCallWithNameAlias(context, useEffectKind, hooks[useEffectKind]);
+ const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState);
+ const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", hooks.useMemo);
+ const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", hooks.useCallback);
+
+ const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = [];
+ const setupFunctionRef: { current: AST.TSESTreeFunction | null } = { current: null };
+ const setupFunctionIdentifiers: TSESTree.Identifier[] = [];
+
+ const indFunctionCalls: TSESTree.CallExpression[] = [];
+ const indSetStateCalls = new WeakMap();
+ const indSetStateCallsInUseEffectArg0 = new WeakMap();
+ const indSetStateCallsInUseEffectSetup = new Map();
+ const indSetStateCallsInUseMemoOrCallback = new WeakMap();
+
+ const onSetupFunctionEnter = (node: AST.TSESTreeFunction) => {
+ setupFunctionRef.current = node;
+ };
+
+ const onSetupFunctionExit = (node: AST.TSESTreeFunction) => {
+ if (setupFunctionRef.current === node) {
+ setupFunctionRef.current = null;
+ }
+ };
+
+ function isFunctionOfUseEffectSetup(node: TSESTree.Node) {
+ return node.parent?.type === T.CallExpression
+ && node.parent.callee !== node
+ && isUseEffectLikeCall(node.parent);
+ }
+
+ function getCallName(node: TSESTree.Node) {
+ if (node.type === T.CallExpression) {
+ return AST.toStringFormat(node.callee, getText);
+ }
+ return AST.toStringFormat(node, getText);
+ }
+
+ function getCallKind(node: TSESTree.CallExpression) {
+ return match(node)
+ .when(isUseStateCall, () => "useState")
+ .when(isUseEffectLikeCall, () => useEffectKind)
+ .when(isSetStateCall, () => "setState")
+ .when(AST.isThenCall, () => "then")
+ .otherwise(() => "other");
+ }
+
+ function getFunctionKind(node: AST.TSESTreeFunction) {
+ const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent;
+ switch (true) {
+ case node.async:
+ case parent.type === T.CallExpression
+ && AST.isThenCall(parent):
+ return "deferred";
+ case node.type !== T.FunctionDeclaration
+ && parent.type === T.CallExpression
+ && parent.callee === node:
+ return "immediate";
+ case isFunctionOfUseEffectSetup(node):
+ return "setup";
+ default:
+ return "other";
+ }
+ }
+
+ function isIdFromUseStateCall(topLevelId: TSESTree.Identifier, at?: number) {
+ const variable = VAR.findVariable(topLevelId, context.sourceCode.getScope(topLevelId));
+ const variableNode = VAR.getVariableInitNode(variable, 0);
+ if (variableNode == null) return false;
+ if (variableNode.type !== T.CallExpression) return false;
+ if (!ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState)(variableNode)) return false;
+ const variableNodeParent = variableNode.parent;
+ if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== T.ArrayPattern) {
+ return true;
+ }
+ return variableNodeParent
+ .id
+ .elements
+ .findIndex((e) => e?.type === T.Identifier && e.name === topLevelId.name) === at;
+ }
+
+ function isSetStateCall(node: TSESTree.CallExpression) {
+ switch (node.callee.type) {
+ // const data = useState();
+ // data.at(1)();
+ case T.CallExpression: {
+ const { callee } = node.callee;
+ if (callee.type !== T.MemberExpression) {
+ return false;
+ }
+ if (!("name" in callee.object)) {
+ return false;
+ }
+ const isAt = callee.property.type === T.Identifier && callee.property.name === "at";
+ const [index] = node.callee.arguments;
+ if (!isAt || index == null) {
+ return false;
+ }
+ const indexScope = context.sourceCode.getScope(node);
+ const indexValue = VAR.toStaticValue({
+ kind: "lazy",
+ node: index,
+ initialScope: indexScope,
+ }).value;
+ return indexValue === 1 && isIdFromUseStateCall(callee.object);
+ }
+ // const [data, setData] = useState();
+ // setData();
+ case T.Identifier: {
+ return isIdFromUseStateCall(node.callee, 1);
+ }
+ // const data = useState();
+ // data[1]();
+ case T.MemberExpression: {
+ if (!("name" in node.callee.object)) {
+ return false;
+ }
+ const property = node.callee.property;
+ const propertyScope = context.sourceCode.getScope(node);
+ const propertyValue = VAR.toStaticValue({
+ kind: "lazy",
+ node: property,
+ initialScope: propertyScope,
+ }).value;
+ return propertyValue === 1 && isIdFromUseStateCall(node.callee.object, 1);
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ return {
+ ":function"(node: AST.TSESTreeFunction) {
+ const kind = getFunctionKind(node);
+ functionEntries.push({ kind, node });
+ if (kind === "setup") {
+ onSetupFunctionEnter(node);
+ }
+ },
+ ":function:exit"(node: AST.TSESTreeFunction) {
+ const { kind } = functionEntries.at(-1) ?? {};
+ if (kind === "setup") {
+ onSetupFunctionExit(node);
+ }
+ functionEntries.pop();
+ },
+ CallExpression(node) {
+ const setupFunction = setupFunctionRef.current;
+ const pEntry = functionEntries.at(-1);
+ if (pEntry == null || pEntry.node.async) {
+ return;
+ }
+ match(getCallKind(node))
+ .with("setState", () => {
+ switch (true) {
+ case pEntry.kind === "deferred":
+ case pEntry.node.async:
+ // do nothing, this is a deferred setState call
+ break;
+ case pEntry.node === setupFunction:
+ case pEntry.kind === "immediate"
+ && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction: {
+ onViolation(context, node, {
+ name: context.sourceCode.getText(node.callee),
+ });
+ return;
+ }
+ default: {
+ const vd = AST.findParentNode(node, isVariableDeclaratorFromHookCall);
+ if (vd == null) getOrElseUpdate(indSetStateCalls, pEntry.node, () => []).push(node);
+ else getOrElseUpdate(indSetStateCallsInUseMemoOrCallback, vd.init, () => []).push(node);
+ }
+ }
+ })
+ .with(useEffectKind, () => {
+ if (AST.isFunction(node.arguments.at(0))) return;
+ setupFunctionIdentifiers.push(...AST.getNestedIdentifiers(node));
+ })
+ .with("other", () => {
+ if (pEntry.node !== setupFunction) return;
+ indFunctionCalls.push(node);
+ })
+ .otherwise(constVoid);
+ },
+ Identifier(node) {
+ if (node.parent.type === T.CallExpression && node.parent.callee === node) {
+ return;
+ }
+ if (!isIdFromUseStateCall(node, 1)) {
+ return;
+ }
+ switch (node.parent.type) {
+ case T.ArrowFunctionExpression: {
+ const parent = node.parent.parent;
+ if (parent.type !== T.CallExpression) {
+ break;
+ }
+ // const [state, setState] = useState();
+ // const set = useMemo(() => setState, []);
+ // useEffect(set, []);
+ if (!isUseMemoCall(parent)) {
+ break;
+ }
+ const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall);
+ if (vd != null) {
+ getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
+ }
+ break;
+ }
+ case T.CallExpression: {
+ if (node !== node.parent.arguments.at(0)) {
+ break;
+ }
+ // const [state, setState] = useState();
+ // const set = useCallback(setState, []);
+ // useEffect(set, []);
+ if (isUseCallbackCall(node.parent)) {
+ const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall);
+ if (vd != null) {
+ getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
+ }
+ break;
+ }
+ // const [state, setState] = useState();
+ // useEffect(setState);
+ if (isUseEffectLikeCall(node.parent)) {
+ getOrElseUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node);
+ }
+ }
+ }
+ },
+ "Program:exit"() {
+ const getSetStateCalls = (
+ id: string | TSESTree.Identifier,
+ initialScope: Scope.Scope,
+ ): TSESTree.CallExpression[] | TSESTree.Identifier[] => {
+ const node = VAR.getVariableInitNode(VAR.findVariable(id, initialScope), 0);
+ switch (node?.type) {
+ case T.ArrowFunctionExpression:
+ case T.FunctionDeclaration:
+ case T.FunctionExpression:
+ return indSetStateCalls.get(node) ?? [];
+ case T.CallExpression:
+ return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? [];
+ }
+ return [];
+ };
+ for (const [, calls] of indSetStateCallsInUseEffectSetup) {
+ for (const call of calls) {
+ onViolation(context, call, { name: call.name });
+ }
+ }
+ for (const { callee } of indFunctionCalls) {
+ if (!("name" in callee)) {
+ continue;
+ }
+ const { name } = callee;
+ const setStateCalls = getSetStateCalls(name, context.sourceCode.getScope(callee));
+ for (const setStateCall of setStateCalls) {
+ onViolation(context, setStateCall, {
+ name: getCallName(setStateCall),
+ });
+ }
+ }
+ for (const id of setupFunctionIdentifiers) {
+ const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
+ for (const setStateCall of setStateCalls) {
+ onViolation(context, setStateCall, {
+ name: getCallName(setStateCall),
+ });
+ }
+ }
+ },
+ };
+}
+
+function isInitFromHookCall(init: TSESTree.Expression | null) {
+ if (init?.type !== T.CallExpression) return false;
+ switch (init.callee.type) {
+ case T.Identifier:
+ return ER.isReactHookName(init.callee.name);
+ case T.MemberExpression:
+ return init.callee.property.type === T.Identifier
+ && ER.isReactHookName(init.callee.property.name);
+ default:
+ return false;
+ }
+}
+
+function isVariableDeclaratorFromHookCall(node: TSESTree.Node): node is
+ & TSESTree.VariableDeclarator
+ & { init: TSESTree.VariableDeclarator["init"] & {} }
+{
+ if (node.type !== T.VariableDeclarator) return false;
+ if (node.id.type !== T.Identifier) return false;
+ return isInitFromHookCall(node.init);
+}
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts
index d6e088e0d8..b2d706f0c0 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts
@@ -1,16 +1,7 @@
import type { RuleContext, RuleFeature } from "@eslint-react/kit";
-import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
-import type { Scope } from "@typescript-eslint/utils/ts-eslint";
import type { CamelCase } from "string-ts";
-import * as AST from "@eslint-react/ast";
-import * as ER from "@eslint-react/core";
-import { constVoid, getOrElseUpdate, not } from "@eslint-react/eff";
-import { getSettingsFromContext } from "@eslint-react/shared";
-import * as VAR from "@eslint-react/var";
-import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
-
-import { match } from "ts-pattern";
+import { useNoDirectSetStateInUseEffect } from "../rules-hooks/use-no-direct-set-state-in-use-effect";
import { createRule } from "../utils";
export const RULE_NAME = "no-unnecessary-use-effect";
@@ -42,341 +33,17 @@ export default createRule<[], MessageID>({
export function create(context: RuleContext): RuleListener {
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
- return useNoDirectSetStateInUseEffect(context, {
+
+ const noDirectSetStateInUseEffectListeners = useNoDirectSetStateInUseEffect(context, {
onViolation(ctx, node, data) {
ctx.report({ messageId: "noUnnecessaryUseEffect", node, data });
},
useEffectKind: "useEffect",
});
-}
-
-type CallKind =
- | "useEffect"
- | "useLayoutEffect"
- | "useState"
- | "setState"
- | "then"
- | "other";
-
-type FunctionKind =
- | "setup"
- | "cleanup"
- | "deferred"
- | "immediate"
- | "other";
-
-export declare namespace useNoDirectSetStateInUseEffect {
- type Options = {
- onViolation: (context: Ctx, node: TSESTree.Node | TSESTree.Token, data: { name: string }) => void;
- useEffectKind: "useEffect" | "useLayoutEffect";
- };
- type ReturnType = ESLintUtils.RuleListener;
-}
-
-export function useNoDirectSetStateInUseEffect(
- context: Ctx,
- options: useNoDirectSetStateInUseEffect.Options,
-): useNoDirectSetStateInUseEffect.ReturnType {
- const { onViolation, useEffectKind } = options;
- const settings = getSettingsFromContext(context);
- const hooks = settings.additionalHooks;
- const getText = (n: TSESTree.Node) => context.sourceCode.getText(n);
- const isUseEffectLikeCall = ER.isReactHookCallWithNameAlias(context, useEffectKind, hooks[useEffectKind]);
- const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState);
- const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", hooks.useMemo);
- const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", hooks.useCallback);
-
- const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = [];
- const setupFunctionRef: { current: AST.TSESTreeFunction | null } = { current: null };
- const setupFunctionIdentifiers: TSESTree.Identifier[] = [];
-
- const indFunctionCalls: TSESTree.CallExpression[] = [];
- const indSetStateCalls = new WeakMap();
- const indSetStateCallsInUseEffectArg0 = new WeakMap();
- const indSetStateCallsInUseEffectSetup = new Map();
- const indSetStateCallsInUseMemoOrCallback = new WeakMap();
-
- const onSetupFunctionEnter = (node: AST.TSESTreeFunction) => {
- setupFunctionRef.current = node;
- };
-
- const onSetupFunctionExit = (node: AST.TSESTreeFunction) => {
- if (setupFunctionRef.current === node) {
- setupFunctionRef.current = null;
- }
- };
- function isFunctionOfUseEffectSetup(node: TSESTree.Node) {
- return node.parent?.type === T.CallExpression
- && node.parent.callee !== node
- && isUseEffectLikeCall(node.parent);
- }
-
- function getCallName(node: TSESTree.Node) {
- if (node.type === T.CallExpression) {
- return AST.toStringFormat(node.callee, getText);
- }
- return AST.toStringFormat(node, getText);
- }
-
- function getCallKind(node: TSESTree.CallExpression) {
- return match(node)
- .when(isUseStateCall, () => "useState")
- .when(isUseEffectLikeCall, () => useEffectKind)
- .when(isSetStateCall, () => "setState")
- .when(AST.isThenCall, () => "then")
- .otherwise(() => "other");
- }
-
- function getFunctionKind(node: AST.TSESTreeFunction) {
- const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent;
- switch (true) {
- case node.async:
- case parent.type === T.CallExpression
- && AST.isThenCall(parent):
- return "deferred";
- case node.type !== T.FunctionDeclaration
- && parent.type === T.CallExpression
- && parent.callee === node:
- return "immediate";
- case isFunctionOfUseEffectSetup(node):
- return "setup";
- default:
- return "other";
- }
- }
-
- function isIdFromUseStateCall(topLevelId: TSESTree.Identifier, at?: number) {
- const variable = VAR.findVariable(topLevelId, context.sourceCode.getScope(topLevelId));
- const variableNode = VAR.getVariableInitNode(variable, 0);
- if (variableNode == null) return false;
- if (variableNode.type !== T.CallExpression) return false;
- if (!ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState)(variableNode)) return false;
- const variableNodeParent = variableNode.parent;
- if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== T.ArrayPattern) {
- return true;
- }
- return variableNodeParent
- .id
- .elements
- .findIndex((e) => e?.type === T.Identifier && e.name === topLevelId.name) === at;
- }
-
- function isSetStateCall(node: TSESTree.CallExpression) {
- switch (node.callee.type) {
- // const data = useState();
- // data.at(1)();
- case T.CallExpression: {
- const { callee } = node.callee;
- if (callee.type !== T.MemberExpression) {
- return false;
- }
- if (!("name" in callee.object)) {
- return false;
- }
- const isAt = callee.property.type === T.Identifier && callee.property.name === "at";
- const [index] = node.callee.arguments;
- if (!isAt || index == null) {
- return false;
- }
- const indexScope = context.sourceCode.getScope(node);
- const indexValue = VAR.toStaticValue({
- kind: "lazy",
- node: index,
- initialScope: indexScope,
- }).value;
- return indexValue === 1 && isIdFromUseStateCall(callee.object);
- }
- // const [data, setData] = useState();
- // setData();
- case T.Identifier: {
- return isIdFromUseStateCall(node.callee, 1);
- }
- // const data = useState();
- // data[1]();
- case T.MemberExpression: {
- if (!("name" in node.callee.object)) {
- return false;
- }
- const property = node.callee.property;
- const propertyScope = context.sourceCode.getScope(node);
- const propertyValue = VAR.toStaticValue({
- kind: "lazy",
- node: property,
- initialScope: propertyScope,
- }).value;
- return propertyValue === 1 && isIdFromUseStateCall(node.callee.object, 1);
- }
- default: {
- return false;
- }
- }
- }
+ // TODO: Implement the logic to check and report other scenarios described in react.dev/learn/you-might-not-need-an-effect.
return {
- ":function"(node: AST.TSESTreeFunction) {
- const kind = getFunctionKind(node);
- functionEntries.push({ kind, node });
- if (kind === "setup") {
- onSetupFunctionEnter(node);
- }
- },
- ":function:exit"(node: AST.TSESTreeFunction) {
- const { kind } = functionEntries.at(-1) ?? {};
- if (kind === "setup") {
- onSetupFunctionExit(node);
- }
- functionEntries.pop();
- },
- CallExpression(node) {
- const setupFunction = setupFunctionRef.current;
- const pEntry = functionEntries.at(-1);
- if (pEntry == null || pEntry.node.async) {
- return;
- }
- match(getCallKind(node))
- .with("setState", () => {
- switch (true) {
- case pEntry.kind === "deferred":
- case pEntry.node.async: {
- // do nothing, this is a deferred setState call
- break;
- }
- case pEntry.node === setupFunction:
- case pEntry.kind === "immediate"
- && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction: {
- onViolation(context, node, {
- name: context.sourceCode.getText(node.callee),
- });
- return;
- }
- default: {
- const vd = AST.findParentNode(node, isVariableDeclaratorFromHookCall);
- if (vd == null) getOrElseUpdate(indSetStateCalls, pEntry.node, () => []).push(node);
- else getOrElseUpdate(indSetStateCallsInUseMemoOrCallback, vd.init, () => []).push(node);
- }
- }
- })
- .with(useEffectKind, () => {
- if (AST.isFunction(node.arguments.at(0))) return;
- setupFunctionIdentifiers.push(...AST.getNestedIdentifiers(node));
- })
- .with("other", () => {
- if (pEntry.node !== setupFunction) return;
- indFunctionCalls.push(node);
- })
- .otherwise(constVoid);
- },
- Identifier(node) {
- if (node.parent.type === T.CallExpression && node.parent.callee === node) {
- return;
- }
- if (!isIdFromUseStateCall(node, 1)) {
- return;
- }
- switch (node.parent.type) {
- case T.ArrowFunctionExpression: {
- const parent = node.parent.parent;
- if (parent.type !== T.CallExpression) {
- break;
- }
- // const [state, setState] = useState();
- // const set = useMemo(() => setState, []);
- // useEffect(set, []);
- if (!isUseMemoCall(parent)) {
- break;
- }
- const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall);
- if (vd != null) {
- getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
- }
- break;
- }
- case T.CallExpression: {
- if (node !== node.parent.arguments.at(0)) {
- break;
- }
- // const [state, setState] = useState();
- // const set = useCallback(setState, []);
- // useEffect(set, []);
- if (isUseCallbackCall(node.parent)) {
- const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall);
- if (vd != null) {
- getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
- }
- break;
- }
- // const [state, setState] = useState();
- // useEffect(setState);
- if (isUseEffectLikeCall(node.parent)) {
- getOrElseUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node);
- }
- }
- }
- },
- "Program:exit"() {
- const getSetStateCalls = (
- id: string | TSESTree.Identifier,
- initialScope: Scope.Scope,
- ): TSESTree.CallExpression[] | TSESTree.Identifier[] => {
- const node = VAR.getVariableInitNode(VAR.findVariable(id, initialScope), 0);
- switch (node?.type) {
- case T.ArrowFunctionExpression:
- case T.FunctionDeclaration:
- case T.FunctionExpression:
- return indSetStateCalls.get(node) ?? [];
- case T.CallExpression:
- return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? [];
- }
- return [];
- };
- for (const [, calls] of indSetStateCallsInUseEffectSetup) {
- for (const call of calls) {
- onViolation(context, call, { name: call.name });
- }
- }
- for (const { callee } of indFunctionCalls) {
- if (!("name" in callee)) {
- continue;
- }
- const { name } = callee;
- const setStateCalls = getSetStateCalls(name, context.sourceCode.getScope(callee));
- for (const setStateCall of setStateCalls) {
- onViolation(context, setStateCall, {
- name: getCallName(setStateCall),
- });
- }
- }
- for (const id of setupFunctionIdentifiers) {
- const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
- for (const setStateCall of setStateCalls) {
- onViolation(context, setStateCall, {
- name: getCallName(setStateCall),
- });
- }
- }
- },
+ ...noDirectSetStateInUseEffectListeners,
};
}
-
-function isInitFromHookCall(init: TSESTree.Expression | null) {
- if (init?.type !== T.CallExpression) return false;
- switch (init.callee.type) {
- case T.Identifier:
- return ER.isReactHookName(init.callee.name);
- case T.MemberExpression:
- return init.callee.property.type === T.Identifier
- && ER.isReactHookName(init.callee.property.name);
- default:
- return false;
- }
-}
-
-function isVariableDeclaratorFromHookCall(node: TSESTree.Node): node is
- & TSESTree.VariableDeclarator
- & { init: TSESTree.VariableDeclarator["init"] & {} }
-{
- if (node.type !== T.VariableDeclarator) return false;
- if (node.id.type !== T.Identifier) return false;
- return isInitFromHookCall(node.init);
-}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index eb7409f26b..0770444996 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -38,7 +38,7 @@
"@eslint-react/kit": "workspace:*",
"@typescript-eslint/utils": "^8.33.1",
"ts-pattern": "^5.7.1",
- "zod": "^3.25.49"
+ "zod": "^3.25.50"
},
"devDependencies": {
"@local/configs": "workspace:*",
diff --git a/packages/utilities/kit/package.json b/packages/utilities/kit/package.json
index bad6c429e9..5df525d4c2 100644
--- a/packages/utilities/kit/package.json
+++ b/packages/utilities/kit/package.json
@@ -37,7 +37,7 @@
"@eslint-react/eff": "workspace:*",
"@typescript-eslint/utils": "^8.33.1",
"ts-pattern": "^5.7.1",
- "zod": "^3.25.49"
+ "zod": "^3.25.50"
},
"devDependencies": {
"@local/configs": "workspace:*",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b93e95e636..fbe01bfe7d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,7 +9,7 @@ overrides:
'@types/react-dom': ^19.1.5
cross-spawn: ^7.0.6
esbuild: ^0.25.5
- lucide-react: ^0.511.0
+ lucide-react: ^0.512.0
next: ^15.3.3
react: ^19.1.0
react-dom: ^19.1.0
@@ -83,7 +83,7 @@ importers:
version: 2.1.0(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-vitest:
specifier: ^0.5.4
- version: 0.5.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))
+ version: 0.5.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))
jiti:
specifier: ^2.4.2
version: 2.4.2
@@ -145,8 +145,8 @@ importers:
specifier: ^8.33.1
version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
vitest:
- specifier: ^3.2.0
- version: 3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
+ specifier: ^3.2.1
+ version: 3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
.pkgs/configs:
dependencies:
@@ -166,11 +166,11 @@ importers:
specifier: ^50.7.1
version: 50.7.1(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-perfectionist:
- specifier: ^4.13.0
- version: 4.13.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
+ specifier: ^4.14.0
+ version: 4.14.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint-plugin-regexp:
- specifier: ^2.7.0
- version: 2.7.0(eslint@9.28.0(jiti@2.4.2))
+ specifier: ^2.8.0
+ version: 2.8.0(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-unicorn:
specifier: ^59.0.1
version: 59.0.1(eslint@9.28.0(jiti@2.4.2))
@@ -223,11 +223,11 @@ importers:
specifier: ^50.7.1
version: 50.7.1(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-perfectionist:
- specifier: ^4.13.0
- version: 4.13.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
+ specifier: ^4.14.0
+ version: 4.14.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint-plugin-regexp:
- specifier: ^2.7.0
- version: 2.7.0(eslint@9.28.0(jiti@2.4.2))
+ specifier: ^2.8.0
+ version: 2.8.0(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-unicorn:
specifier: ^59.0.1
version: 59.0.1(eslint@9.28.0(jiti@2.4.2))
@@ -287,8 +287,8 @@ importers:
specifier: 15.5.0
version: 15.5.0(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.8)
lucide-react:
- specifier: ^0.511.0
- version: 0.511.0(react@19.1.0)
+ specifier: ^0.512.0
+ version: 0.512.0(react@19.1.0)
next:
specifier: ^15.3.3
version: 15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -378,8 +378,8 @@ importers:
specifier: ^4.15.0
version: 4.15.0(@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-perfectionist:
- specifier: ^4.13.0
- version: 4.13.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
+ specifier: ^4.14.0
+ version: 4.14.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@9.28.0(jiti@2.4.2))
@@ -626,8 +626,8 @@ importers:
specifier: ^7.27.4
version: 7.27.4
'@babel/eslint-parser':
- specifier: ^7.27.1
- version: 7.27.1(@babel/core@7.27.4)(eslint@9.28.0(jiti@2.4.2))
+ specifier: ^7.27.5
+ version: 7.27.5(@babel/core@7.27.4)(eslint@9.28.0(jiti@2.4.2))
'@babel/preset-env':
specifier: ^7.27.2
version: 7.27.2(@babel/core@7.27.4)
@@ -1220,8 +1220,8 @@ importers:
specifier: ^5.7.1
version: 5.7.1
zod:
- specifier: ^3.25.49
- version: 3.25.49
+ specifier: ^3.25.50
+ version: 3.25.50
devDependencies:
'@local/configs':
specifier: workspace:*
@@ -1288,8 +1288,8 @@ importers:
specifier: ^5.7.1
version: 5.7.1
zod:
- specifier: ^3.25.49
- version: 3.25.49
+ specifier: ^3.25.50
+ version: 3.25.50
devDependencies:
'@local/configs':
specifier: workspace:*
@@ -1371,8 +1371,8 @@ packages:
resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==}
engines: {node: '>=6.9.0'}
- '@babel/eslint-parser@7.27.1':
- resolution: {integrity: sha512-q8rjOuadH0V6Zo4XLMkJ3RMQ9MSBqwaDByyYB0izsYdaIWGNLmEblbCOf1vyFHICcg16CD7Fsi51vcQnYxmt6Q==}
+ '@babel/eslint-parser@7.27.5':
+ resolution: {integrity: sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==}
engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
peerDependencies:
'@babel/core': ^7.11.0
@@ -4288,11 +4288,11 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
- '@vitest/expect@3.2.0':
- resolution: {integrity: sha512-0v4YVbhDKX3SKoy0PHWXpKhj44w+3zZkIoVES9Ex2pq+u6+Bijijbi2ua5kE+h3qT6LBWFTNZSCOEU37H8Y5sA==}
+ '@vitest/expect@3.2.1':
+ resolution: {integrity: sha512-FqS/BnDOzV6+IpxrTg5GQRyLOCtcJqkwMwcS8qGCI2IyRVDwPAtutztaf1CjtPHlZlWtl1yUPCd7HM0cNiDOYw==}
- '@vitest/mocker@3.2.0':
- resolution: {integrity: sha512-HFcW0lAMx3eN9vQqis63H0Pscv0QcVMo1Kv8BNysZbxcmHu3ZUYv59DS6BGYiGQ8F5lUkmsfMMlPm4DJFJdf/A==}
+ '@vitest/mocker@3.2.1':
+ resolution: {integrity: sha512-OXxMJnx1lkB+Vl65Re5BrsZEHc90s5NMjD23ZQ9NlU7f7nZiETGoX4NeKZSmsKjseuMq2uOYXdLOeoM0pJU+qw==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
@@ -4302,20 +4302,20 @@ packages:
vite:
optional: true
- '@vitest/pretty-format@3.2.0':
- resolution: {integrity: sha512-gUUhaUmPBHFkrqnOokmfMGRBMHhgpICud9nrz/xpNV3/4OXCn35oG+Pl8rYYsKaTNd/FAIrqRHnwpDpmYxCYZw==}
+ '@vitest/pretty-format@3.2.1':
+ resolution: {integrity: sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==}
- '@vitest/runner@3.2.0':
- resolution: {integrity: sha512-bXdmnHxuB7fXJdh+8vvnlwi/m1zvu+I06i1dICVcDQFhyV4iKw2RExC/acavtDn93m/dRuawUObKsrNE1gJacA==}
+ '@vitest/runner@3.2.1':
+ resolution: {integrity: sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==}
- '@vitest/snapshot@3.2.0':
- resolution: {integrity: sha512-z7P/EneBRMe7hdvWhcHoXjhA6at0Q4ipcoZo6SqgxLyQQ8KSMMCmvw1cSt7FHib3ozt0wnRHc37ivuUMbxzG/A==}
+ '@vitest/snapshot@3.2.1':
+ resolution: {integrity: sha512-5xko/ZpW2Yc65NVK9Gpfg2y4BFvcF+At7yRT5AHUpTg9JvZ4xZoyuRY4ASlmNcBZjMslV08VRLDrBOmUe2YX3g==}
- '@vitest/spy@3.2.0':
- resolution: {integrity: sha512-s3+TkCNUIEOX99S0JwNDfsHRaZDDZZR/n8F0mop0PmsEbQGKZikCGpTGZ6JRiHuONKew3Fb5//EPwCP+pUX9cw==}
+ '@vitest/spy@3.2.1':
+ resolution: {integrity: sha512-Nbfib34Z2rfcJGSetMxjDCznn4pCYPZOtQYox2kzebIJcgH75yheIKd5QYSFmR8DIZf2M8fwOm66qSDIfRFFfQ==}
- '@vitest/utils@3.2.0':
- resolution: {integrity: sha512-gXXOe7Fj6toCsZKVQouTRLJftJwmvbhH5lKOBR6rlP950zUq9AitTUjnFoXS/CqjBC2aoejAztLPzzuva++XBw==}
+ '@vitest/utils@3.2.1':
+ resolution: {integrity: sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==}
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
@@ -5601,8 +5601,8 @@ packages:
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
- eslint-plugin-perfectionist@4.13.0:
- resolution: {integrity: sha512-dsPwXwV7IrG26PJ+h1crQ1f5kxay/gQAU0NJnbVTQc91l5Mz9kPjyIZ7fXgie+QSgi8a+0TwGbfaJx+GIhzuoQ==}
+ eslint-plugin-perfectionist@4.14.0:
+ resolution: {integrity: sha512-BkhiOqzdum8vQSFgj1/q5+6UUWPMn4GELdxuX7uIsGegmAeH/+LnWsiVxgMrxalD0p68sYfMeKaHF1NfrpI/mg==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
eslint: '>=8.45.0'
@@ -5618,8 +5618,8 @@ packages:
peerDependencies:
eslint: '>=8.40'
- eslint-plugin-regexp@2.7.0:
- resolution: {integrity: sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==}
+ eslint-plugin-regexp@2.8.0:
+ resolution: {integrity: sha512-xme90IvkMgdyS+NJC21FM0H6ek4urGsdlIFTXpZRqH2BKJKVSd8hRbyrCpbcqfGBi2jth3eQoLiO3RC1gxZHiw==}
engines: {node: ^18 || >=20}
peerDependencies:
eslint: '>=8.44.0'
@@ -6762,8 +6762,8 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
- lucide-react@0.511.0:
- resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==}
+ lucide-react@0.512.0:
+ resolution: {integrity: sha512-VCLpMynBVa+UvEPhs8fXluoa5nh7oPn3JtJ9F29+LmNi6q70IRxx80OBFT5KT6/T5/2hX+XkoWOC8zcRQI7Mzg==}
peerDependencies:
react: ^19.1.0
@@ -8707,8 +8707,8 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
- vite-node@3.2.0:
- resolution: {integrity: sha512-8Fc5Ko5Y4URIJkmMF/iFP1C0/OJyY+VGVe9Nw6WAdZyw4bTO+eVg9mwxWkQp/y8NnAoQY3o9KAvE1ZdA2v+Vmg==}
+ vite-node@3.2.1:
+ resolution: {integrity: sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
@@ -8752,16 +8752,16 @@ packages:
yaml:
optional: true
- vitest@3.2.0:
- resolution: {integrity: sha512-P7Nvwuli8WBNmeMHHek7PnGW4oAZl9za1fddfRVidZar8wDZRi7hpznLKQePQ8JPLwSBEYDK11g+++j7uFJV8Q==}
+ vitest@3.2.1:
+ resolution: {integrity: sha512-VZ40MBnlE1/V5uTgdqY3DmjUgZtIzsYq758JGlyQrv5syIsaYcabkfPkEuWML49Ph0D/SoqpVFd0dyVTr551oA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
- '@vitest/browser': 3.2.0
- '@vitest/ui': 3.2.0
+ '@vitest/browser': 3.2.1
+ '@vitest/ui': 3.2.1
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
@@ -8964,8 +8964,8 @@ packages:
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
engines: {node: '>=12.20'}
- zod@3.25.49:
- resolution: {integrity: sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==}
+ zod@3.25.50:
+ resolution: {integrity: sha512-VstOnRxf4tlSq0raIwbn0n+LA34SxVoZ8r3pkwSUM0jqNiA/HCMQEVjTuS5FZmHsge+9MDGTiAuHyml5T0um6A==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -9022,7 +9022,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/eslint-parser@7.27.1(@babel/core@7.27.4)(eslint@9.28.0(jiti@2.4.2))':
+ '@babel/eslint-parser@7.27.5(@babel/core@7.27.4)(eslint@9.28.0(jiti@2.4.2))':
dependencies:
'@babel/core': 7.27.4
'@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
@@ -12092,44 +12092,44 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/expect@3.2.0':
+ '@vitest/expect@3.2.1':
dependencies:
'@types/chai': 5.2.2
- '@vitest/spy': 3.2.0
- '@vitest/utils': 3.2.0
+ '@vitest/spy': 3.2.1
+ '@vitest/utils': 3.2.1
chai: 5.2.0
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.0(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))':
+ '@vitest/mocker@3.2.1(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
- '@vitest/spy': 3.2.0
+ '@vitest/spy': 3.2.1
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
- '@vitest/pretty-format@3.2.0':
+ '@vitest/pretty-format@3.2.1':
dependencies:
tinyrainbow: 2.0.0
- '@vitest/runner@3.2.0':
+ '@vitest/runner@3.2.1':
dependencies:
- '@vitest/utils': 3.2.0
+ '@vitest/utils': 3.2.1
pathe: 2.0.3
- '@vitest/snapshot@3.2.0':
+ '@vitest/snapshot@3.2.1':
dependencies:
- '@vitest/pretty-format': 3.2.0
+ '@vitest/pretty-format': 3.2.1
magic-string: 0.30.17
pathe: 2.0.3
- '@vitest/spy@3.2.0':
+ '@vitest/spy@3.2.1':
dependencies:
tinyspy: 4.0.3
- '@vitest/utils@3.2.0':
+ '@vitest/utils@3.2.1':
dependencies:
- '@vitest/pretty-format': 3.2.0
+ '@vitest/pretty-format': 3.2.1
loupe: 3.1.3
tinyrainbow: 2.0.0
@@ -13647,7 +13647,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-perfectionist@4.13.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3):
+ eslint-plugin-perfectionist@4.14.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3):
dependencies:
'@typescript-eslint/types': 8.33.1
'@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
@@ -13665,7 +13665,7 @@ snapshots:
dependencies:
eslint: 9.28.0(jiti@2.4.2)
- eslint-plugin-regexp@2.7.0(eslint@9.28.0(jiti@2.4.2)):
+ eslint-plugin-regexp@2.8.0(eslint@9.28.0(jiti@2.4.2)):
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1
@@ -13697,12 +13697,12 @@ snapshots:
semver: 7.7.2
strip-indent: 4.0.0
- eslint-plugin-vitest@0.5.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)):
+ eslint-plugin-vitest@0.5.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@typescript-eslint/utils': 7.18.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.28.0(jiti@2.4.2)
optionalDependencies:
- vitest: 3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
+ vitest: 3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
- typescript
@@ -14028,7 +14028,7 @@ snapshots:
npm-to-yarn: 3.0.1
oxc-transform: 0.53.0
unist-util-visit: 5.0.0
- zod: 3.25.49
+ zod: 3.25.50
fumadocs-mdx@11.6.6(acorn@8.14.1)(fumadocs-core@15.5.0(@types/react@19.1.6)(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
dependencies:
@@ -14046,7 +14046,7 @@ snapshots:
tinyexec: 1.0.1
tinyglobby: 0.2.14
unist-util-visit: 5.0.0
- zod: 3.25.49
+ zod: 3.25.50
transitivePeerDependencies:
- acorn
- supports-color
@@ -14900,7 +14900,7 @@ snapshots:
dependencies:
yallist: 3.1.1
- lucide-react@0.511.0(react@19.1.0):
+ lucide-react@0.512.0(react@19.1.0):
dependencies:
react: 19.1.0
@@ -17336,7 +17336,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
- vite-node@3.2.0(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0):
+ vite-node@3.2.1(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
@@ -17375,16 +17375,16 @@ snapshots:
tsx: 4.19.4
yaml: 2.8.0
- vitest@3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0):
+ vitest@3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
'@types/chai': 5.2.2
- '@vitest/expect': 3.2.0
- '@vitest/mocker': 3.2.0(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))
- '@vitest/pretty-format': 3.2.0
- '@vitest/runner': 3.2.0
- '@vitest/snapshot': 3.2.0
- '@vitest/spy': 3.2.0
- '@vitest/utils': 3.2.0
+ '@vitest/expect': 3.2.1
+ '@vitest/mocker': 3.2.1(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))
+ '@vitest/pretty-format': 3.2.1
+ '@vitest/runner': 3.2.1
+ '@vitest/snapshot': 3.2.1
+ '@vitest/spy': 3.2.1
+ '@vitest/utils': 3.2.1
chai: 5.2.0
debug: 4.4.1
expect-type: 1.2.1
@@ -17398,7 +17398,7 @@ snapshots:
tinypool: 1.1.0
tinyrainbow: 2.0.0
vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
- vite-node: 3.2.0(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
+ vite-node: 3.2.1(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
@@ -17620,6 +17620,6 @@ snapshots:
yocto-queue@1.2.1: {}
- zod@3.25.49: {}
+ zod@3.25.50: {}
zwitch@2.0.4: {}