diff --git a/.pkgs/configs/package.json b/.pkgs/configs/package.json index 65f69c9a76..c4814424af 100644 --- a/.pkgs/configs/package.json +++ b/.pkgs/configs/package.json @@ -23,6 +23,7 @@ "@stylistic/eslint-plugin": "^5.5.0", "eslint-plugin-de-morgan": "^2.0.0", "eslint-plugin-function": "^0.0.33", + "eslint-plugin-function-rule": "^0.0.7", "eslint-plugin-jsdoc": "^61.1.12", "eslint-plugin-perfectionist": "^4.15.1", "eslint-plugin-regexp": "^2.10.0", diff --git a/.pkgs/eslint-plugin-local/.gitignore b/.pkgs/eslint-plugin-local/.gitignore deleted file mode 100644 index 5a4c9838a9..0000000000 --- a/.pkgs/eslint-plugin-local/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# Node.js -node_modules/ - -# Build -!dist/ -out/ -build -docs/internals/ - -# Editor -.vim/ -.idea/ - -# Test -coverage/ - -.eslintcache - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# turbo -.turbo - -# vercel -.vercel - -stats.html -*.config-*.mjs -/eslint-config.json -*.bundled_*.mjs -*.tgz -eslint-results.sarif \ No newline at end of file diff --git a/.pkgs/eslint-plugin-local/dist/index.d.ts b/.pkgs/eslint-plugin-local/dist/index.d.ts deleted file mode 100644 index 9241f69e80..0000000000 --- a/.pkgs/eslint-plugin-local/dist/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CompatiblePlugin } from "@eslint-react/shared"; - -//#region src/index.d.ts -declare const plugin: CompatiblePlugin; -//#endregion -export { plugin as default }; \ No newline at end of file diff --git a/.pkgs/eslint-plugin-local/dist/index.js b/.pkgs/eslint-plugin-local/dist/index.js deleted file mode 100644 index 5b7dcdd894..0000000000 --- a/.pkgs/eslint-plugin-local/dist/index.js +++ /dev/null @@ -1,109 +0,0 @@ -import * as AST from "@eslint-react/ast"; -import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils"; -import { NullThrowsReasons, nullThrows } from "@typescript-eslint/utils/eslint-utils"; - -//#region package.json -var name = "@local/eslint-plugin-local"; -var version = "0.0.0"; - -//#endregion -//#region src/utils/create-rule.ts -function getDocsUrl() { - return "TODO: add docs for local ESLint rules"; -} -const createRule = ESLintUtils.RuleCreator(getDocsUrl); - -//#endregion -//#region src/rules/avoid-multiline-template-expression.ts -const RULE_NAME$1 = "avoid-multiline-template-expression"; -const RULE_FEATURES = []; -var avoid_multiline_template_expression_default = createRule({ - meta: { - type: "problem", - docs: { - description: "disallow multiline template expressions", - [Symbol.for("rule_features")]: RULE_FEATURES - }, - messages: { avoidMultilineTemplateExpression: "Avoid multiline template expressions." }, - schema: [] - }, - name: RULE_NAME$1, - create: create$1, - defaultOptions: [] -}); -function create$1(context) { - return { TemplateLiteral: (node) => { - if (AST.isMultiLine(node)) context.report({ - messageId: "avoidMultilineTemplateExpression", - node - }); - } }; -} - -//#endregion -//#region src/rules/prefer-eqeq-nullish-comparison.ts -const RULE_NAME = "prefer-eqeq-nullish-comparison"; -var prefer_eqeq_nullish_comparison_default = createRule({ - meta: { - type: "suggestion", - docs: { description: "Enforces eqeqeq preferences around nullish comparisons." }, - fixable: "code", - hasSuggestions: true, - messages: { - unexpectedComparison: "Unexpected strict comparison (`{{strictOperator}}`) with `{{nullishKind}}`. In this codebase, we prefer to use loose equality as a general-purpose nullish check when possible.", - useLooseComparisonSuggestion: "Use loose comparison (`{{looseOperator}} null`) instead, to check both nullish values." - }, - schema: [] - }, - name: RULE_NAME, - create, - defaultOptions: [] -}); -function create(context) { - return { BinaryExpression(node) { - if (node.operator === "===" || node.operator === "!==") { - const offendingChild = [node.left, node.right].find((child) => child.type === AST_NODE_TYPES.Identifier && child.name === "undefined" || child.type === AST_NODE_TYPES.Literal && child.raw === "null"); - if (offendingChild == null) return; - const operatorToken = nullThrows(context.sourceCode.getFirstTokenBetween(node.left, node.right, (token) => token.value === node.operator), NullThrowsReasons.MissingToken(node.operator, "binary expression")); - const wasLeft = node.left === offendingChild; - const nullishKind = offendingChild.type === AST_NODE_TYPES.Identifier ? "undefined" : "null"; - const looseOperator = node.operator === "===" ? "==" : "!="; - context.report({ - messageId: "unexpectedComparison", - data: { - nullishKind, - strictOperator: node.operator - }, - loc: wasLeft ? { - end: operatorToken.loc.end, - start: node.left.loc.start - } : { - end: node.right.loc.end, - start: operatorToken.loc.start - }, - suggest: [{ - messageId: "useLooseComparisonSuggestion", - data: { looseOperator }, - fix: (fixer) => [fixer.replaceText(offendingChild, "null"), fixer.replaceText(operatorToken, looseOperator)] - }] - }); - } - } }; -} - -//#endregion -//#region src/index.ts -const plugin = { - meta: { - name, - version - }, - rules: { - "avoid-multiline-template-expression": avoid_multiline_template_expression_default, - "prefer-eqeq-nullish-comparison": prefer_eqeq_nullish_comparison_default - } -}; -var src_default = plugin; - -//#endregion -export { src_default as default }; \ No newline at end of file diff --git a/.pkgs/eslint-plugin-local/package.json b/.pkgs/eslint-plugin-local/package.json deleted file mode 100644 index 67fbff7540..0000000000 --- a/.pkgs/eslint-plugin-local/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@local/eslint-plugin-local", - "version": "0.0.0", - "private": true, - "description": "Local ESLint plugin for use in the workspace", - "sideEffects": false, - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "files": [ - "dist", - "package.json" - ], - "scripts": { - "build": "tsdown --dts-resolve", - "lint:publish": "publint" - }, - "dependencies": { - "@eslint-react/ast": "workspace:*", - "@eslint-react/eff": "workspace:*", - "@eslint-react/shared": "workspace:*", - "@eslint-react/var": "workspace:*", - "@eslint/js": "^9.39.1", - "@stylistic/eslint-plugin": "^5.5.0", - "@typescript-eslint/scope-manager": "^8.46.3", - "@typescript-eslint/type-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", - "@typescript-eslint/utils": "^8.46.3", - "eslint-plugin-de-morgan": "^2.0.0", - "eslint-plugin-jsdoc": "^61.1.12", - "eslint-plugin-perfectionist": "^4.15.1", - "eslint-plugin-regexp": "^2.10.0", - "eslint-plugin-unicorn": "^62.0.0", - "string-ts": "^2.2.1", - "ts-pattern": "^5.9.0" - }, - "devDependencies": { - "@local/configs": "workspace:*", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "tsdown": "^0.16.1" - }, - "peerDependencies": { - "eslint": "^9.38.0", - "typescript": "^5.9.3" - }, - "engines": { - "node": ">=20.19.0" - } -} diff --git a/.pkgs/eslint-plugin-local/src/index.ts b/.pkgs/eslint-plugin-local/src/index.ts deleted file mode 100644 index 2f72701acf..0000000000 --- a/.pkgs/eslint-plugin-local/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { name, version } from "../package.json"; - -import type { CompatiblePlugin } from "@eslint-react/shared"; - -import avoidMultilineTemplateExpression from "./rules/avoid-multiline-template-expression"; -import preferEqeqNullishComparison from "./rules/prefer-eqeq-nullish-comparison"; - -const plugin: CompatiblePlugin = { - meta: { - name, - version, - }, - rules: { - "avoid-multiline-template-expression": avoidMultilineTemplateExpression, - "prefer-eqeq-nullish-comparison": preferEqeqNullishComparison, - }, -}; - -export default plugin; diff --git a/.pkgs/eslint-plugin-local/src/rules/avoid-multiline-template-expression.spec.ts b/.pkgs/eslint-plugin-local/src/rules/avoid-multiline-template-expression.spec.ts deleted file mode 100644 index c9cbbac69e..0000000000 --- a/.pkgs/eslint-plugin-local/src/rules/avoid-multiline-template-expression.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ruleTester } from "../../../../test"; -import rule, { RULE_NAME } from "./avoid-multiline-template-expression"; - -ruleTester.run(RULE_NAME, rule, { - invalid: [ - { - code: [ - "const foo = `foo", - "bar`;", - ].join("\n"), - errors: [{ messageId: "avoidMultilineTemplateExpression" }], - }, - ], - valid: [ - "const foo = `foo`;", - "const foo = `foo${bar}`;", - "const foo = `foo${bar}baz\\n`;", - ], -}); diff --git a/.pkgs/eslint-plugin-local/src/rules/avoid-multiline-template-expression.ts b/.pkgs/eslint-plugin-local/src/rules/avoid-multiline-template-expression.ts deleted file mode 100644 index a75b9c6a2e..0000000000 --- a/.pkgs/eslint-plugin-local/src/rules/avoid-multiline-template-expression.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as AST from "@eslint-react/ast"; -import type { RuleContext, RuleFeature } from "@eslint-react/shared"; -import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; -import type { CamelCase } from "string-ts"; - -import { createRule } from "../utils"; - -export const RULE_NAME = "avoid-multiline-template-expression"; - -export const RULE_FEATURES = [] as const satisfies RuleFeature[]; - -export type MessageID = CamelCase; - -export default createRule<[], MessageID>({ - meta: { - type: "problem", - docs: { - description: "disallow multiline template expressions", - [Symbol.for("rule_features")]: RULE_FEATURES, - }, - messages: { - avoidMultilineTemplateExpression: "Avoid multiline template expressions.", - }, - schema: [], - }, - name: RULE_NAME, - create, - defaultOptions: [], -}); - -export function create(context: RuleContext): RuleListener { - return { - TemplateLiteral: (node) => { - if (AST.isMultiLine(node)) { - context.report({ - messageId: "avoidMultilineTemplateExpression", - node, - }); - } - }, - }; -} diff --git a/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.spec.ts b/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.spec.ts deleted file mode 100644 index 33a5da5a36..0000000000 --- a/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ruleTester } from "../../../../test"; -import rule, { RULE_NAME } from "./prefer-eqeq-nullish-comparison"; - -ruleTester.run(RULE_NAME, rule, { - invalid: [ - { - code: "something === undefined;", - errors: [ - { - messageId: "unexpectedComparison", - column: 11, - data: { - nullishKind: "undefined", - strictOperator: "===", - }, - endColumn: 24, - endLine: 1, - line: 1, - suggestions: [ - { - messageId: "useLooseComparisonSuggestion", - data: { - looseOperator: "==", - }, - output: `something == null;`, - }, - ], - }, - ], - }, - { - code: "undefined !== something;", - errors: [ - { - messageId: "unexpectedComparison", - column: 1, - data: { - nullishKind: "undefined", - strictOperator: "!==", - }, - endColumn: 14, - endLine: 1, - line: 1, - suggestions: [ - { - messageId: "useLooseComparisonSuggestion", - data: { - looseOperator: "!=", - }, - output: `null != something;`, - }, - ], - }, - ], - }, - { - code: "null !== something;", - errors: [ - { - messageId: "unexpectedComparison", - column: 1, - data: { - nullishKind: "null", - strictOperator: "!==", - }, - endColumn: 9, - endLine: 1, - line: 1, - suggestions: [ - { - messageId: "useLooseComparisonSuggestion", - data: { - looseOperator: "!=", - }, - output: `null != something;`, - }, - ], - }, - ], - }, - ], - valid: [ - "null == a;", - "foo != null;", - "foo === bar;", - "foo !== bar;", - // We're not trying to duplicate eqeqeq's reports. - "a == b;", - "something == undefined;", - "undefined != something;", - ], -}); diff --git a/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts b/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts deleted file mode 100644 index 61ce09715f..0000000000 --- a/.pkgs/eslint-plugin-local/src/rules/prefer-eqeq-nullish-comparison.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Ported from: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin-internal/src/rules/eqeq-nullish.ts - -import type { RuleContext, RuleFeature } from "@eslint-react/shared"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; -import { NullThrowsReasons, type RuleListener, nullThrows } from "@typescript-eslint/utils/eslint-utils"; - -import { createRule } from "../utils"; - -export const RULE_NAME = "prefer-eqeq-nullish-comparison"; - -export const RULE_FEATURES = [ - "FIX", -] as const satisfies RuleFeature[]; - -export type MessageID = - | "unexpectedComparison" - | "useLooseComparisonSuggestion"; - -export default createRule({ - meta: { - type: "suggestion", - docs: { - description: "Enforces eqeqeq preferences around nullish comparisons.", - }, - fixable: "code", - hasSuggestions: true, - messages: { - unexpectedComparison: - "Unexpected strict comparison (`{{strictOperator}}`) with `{{nullishKind}}`. In this codebase, we prefer to use loose equality as a general-purpose nullish check when possible.", - useLooseComparisonSuggestion: - "Use loose comparison (`{{looseOperator}} null`) instead, to check both nullish values.", - }, - schema: [], - }, - name: RULE_NAME, - create, - defaultOptions: [], -}); - -export function create(context: RuleContext): RuleListener { - return { - BinaryExpression(node): void { - if (node.operator === "===" || node.operator === "!==") { - const offendingChild = [node.left, node.right].find( - (child) => - (child.type === T.Identifier - && child.name === "undefined") - || (child.type === T.Literal && child.raw === "null"), - ); - - if (offendingChild == null) { - return; - } - - const operatorToken = nullThrows( - context.sourceCode.getFirstTokenBetween( - node.left, - node.right, - (token) => token.value === node.operator, - ), - NullThrowsReasons.MissingToken(node.operator, "binary expression"), - ); - - const wasLeft = node.left === offendingChild; - - const nullishKind = offendingChild.type === T.Identifier - ? "undefined" - : "null"; - - const looseOperator = node.operator === "===" ? "==" : "!="; - - context.report({ - messageId: "unexpectedComparison", - - data: { - nullishKind, - strictOperator: node.operator, - }, - loc: wasLeft - ? { - end: operatorToken.loc.end, - start: node.left.loc.start, - } - : { - end: node.right.loc.end, - start: operatorToken.loc.start, - }, - suggest: [ - { - messageId: "useLooseComparisonSuggestion", - data: { - looseOperator, - }, - fix: (fixer) => [ - fixer.replaceText(offendingChild, "null"), - fixer.replaceText(operatorToken, looseOperator), - ], - }, - ], - }); - } - }, - }; -} diff --git a/.pkgs/eslint-plugin-local/src/utils/create-rule.ts b/.pkgs/eslint-plugin-local/src/utils/create-rule.ts deleted file mode 100644 index 6552ee166d..0000000000 --- a/.pkgs/eslint-plugin-local/src/utils/create-rule.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ESLintUtils } from "@typescript-eslint/utils"; - -function getDocsUrl() { - return "TODO: add docs for local ESLint rules"; -} - -export const createRule = ESLintUtils.RuleCreator(getDocsUrl); diff --git a/.pkgs/eslint-plugin-local/src/utils/index.ts b/.pkgs/eslint-plugin-local/src/utils/index.ts deleted file mode 100644 index 67590faada..0000000000 --- a/.pkgs/eslint-plugin-local/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./create-rule"; -export * from "./is-initialized-from-source"; diff --git a/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts b/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts deleted file mode 100644 index f3205b06ca..0000000000 --- a/.pkgs/eslint-plugin-local/src/utils/is-initialized-from-source.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as AST from "@eslint-react/ast"; -import { unit } from "@eslint-react/eff"; -import { findVariable } from "@eslint-react/var"; -import type { Scope } from "@typescript-eslint/scope-manager"; -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -/** - * Check if an identifier is initialized from the given source - * @param name The top-level identifier's name - * @param source The import source to check against - * @param initialScope Initial scope to search for the identifier - * @returns Whether the identifier is initialized from the given source - */ -export function isInitializedFromSource( - name: string, - source: string, - initialScope: Scope, -): boolean { - const latestDef = findVariable(name, initialScope)?.defs.at(-1); - if (latestDef == null) return false; - const { node, parent } = latestDef; - if (node.type === T.VariableDeclarator && node.init != null) { - const { init } = node; - // check for: `variable = Source.variable` - if (init.type === T.MemberExpression && init.object.type === T.Identifier) { - return isInitializedFromSource(init.object.name, source, initialScope); - } - // check for: `{ variable } = Source` - if (init.type === T.Identifier) { - return isInitializedFromSource(init.name, source, initialScope); - } - // check for: `variable = require('source')` or `variable = require('source').variable` - const args = getRequireExpressionArguments(init); - const arg0 = args?.[0]; - if (arg0 == null || !AST.isLiteral(arg0, "string")) { - return false; - } - // check for: `require('source')` or `require('source/...')` - return arg0.value === source - || arg0 - .value - .startsWith(`${source}/`); - } - // latest definition is an import declaration: import { variable } from 'source' - return parent?.type === T.ImportDeclaration && parent.source.value === source; -} - -function getRequireExpressionArguments(node: TSESTree.Node): TSESTree.CallExpressionArgument[] | unit { - switch (true) { - // require('source') - case node.type === T.CallExpression - && node.callee.type === T.Identifier - && node.callee.name === "require": { - return node.arguments; - } - // require('source').variable - case node.type === T.MemberExpression: { - return getRequireExpressionArguments(node.object); - } - } - return unit; -} diff --git a/.pkgs/eslint-plugin-local/tsconfig.json b/.pkgs/eslint-plugin-local/tsconfig.json deleted file mode 100644 index 8ce1a35fcd..0000000000 --- a/.pkgs/eslint-plugin-local/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "extends": [ - "@local/configs/tsconfig.base.json", - "@tsconfig/node22/tsconfig.json" - ], - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", - "skipLibCheck": true, - "moduleDetection": "force", - "isolatedModules": true, - "verbatimModuleSyntax": true, - "resolveJsonModule": true, - "allowJs": false, - "checkJs": false, - "strict": true, - "erasableSyntaxOnly": true, - "noImplicitAny": true, - "noImplicitThis": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "exactOptionalPropertyTypes": true, - "composite": false, - "stripInternal": false - }, - "exclude": [ - "node_modules", - "**/dist", - "test/fixtures" - ], - "include": [ - "src" - ] -} diff --git a/.pkgs/function-rules/.gitignore b/.pkgs/function-rules/.gitignore new file mode 100644 index 0000000000..a14702c409 --- /dev/null +++ b/.pkgs/function-rules/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.pkgs/function-rules/index.ts b/.pkgs/function-rules/index.ts new file mode 100644 index 0000000000..941c798d0d --- /dev/null +++ b/.pkgs/function-rules/index.ts @@ -0,0 +1 @@ +export * from "./rules/nullishComparison"; diff --git a/.pkgs/function-rules/package.json b/.pkgs/function-rules/package.json new file mode 100644 index 0000000000..d76730f087 --- /dev/null +++ b/.pkgs/function-rules/package.json @@ -0,0 +1,31 @@ +{ + "name": "@local/function-rules", + "version": "0.0.0", + "private": true, + "description": "Local function rules for use in the workspace", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "package.json" + ], + "scripts": { + "build": "tsdown", + "lint:publish": "publint", + "lint:ts": "tsc --noEmit" + }, + "devDependencies": { + "eslint": "^9.39.1", + "tsdown": "^0.16.1" + }, + "peerDependencies": { + "eslint": "^9.39.1", + "typescript": "^5" + } +} diff --git a/.pkgs/function-rules/rules/nullishComparison.ts b/.pkgs/function-rules/rules/nullishComparison.ts new file mode 100644 index 0000000000..75eeb512d6 --- /dev/null +++ b/.pkgs/function-rules/rules/nullishComparison.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { Rule } from "eslint"; +import { defineRuleListener } from "eslint-plugin-function-rule"; + +export interface nullishComparisonOptions { + enforce: "eqeq" | "eqeqeq"; +} + +// TODO: Implement different enforce options +export function nullishComparison(options?: nullishComparisonOptions) { + return (context: Rule.RuleContext): Rule.RuleListener => { + return defineRuleListener({ + BinaryExpression(node): void { + if (node.operator === "===" || node.operator === "!==") { + const offendingChild = [node.left, node.right].find( + (child) => + (child.type === "Identifier" + && child.name === "undefined") + || (child.type === "Literal" && child.raw === "null"), + ); + + if (offendingChild == null) { + return; + } + + const operatorToken = context.sourceCode.getFirstTokenBetween( + node.left, + node.right, + (token) => token.value === node.operator, + ); + + if (operatorToken == null) throw new Error("Can't get operator token"); + + const wasLeft = node.left === offendingChild; + + const nullishKind = offendingChild.type === "Identifier" + ? "undefined" + : "null"; + + const looseOperator = node.operator === "===" ? "==" : "!="; + + context.report({ + loc: wasLeft + ? { + end: operatorToken.loc.end, + start: node.left.loc!.start, + } + : { + end: node.right.loc!.end, + start: operatorToken.loc.start, + }, + message: + `Unexpected strict comparison ('${node.operator}') with '${nullishKind}'. In this codebase, we prefer to use loose equality as a general-purpose nullish check when possible.`, + suggest: [ + { + desc: `Use loose comparison ('${looseOperator} null') instead, to check both nullish values.`, + fix: (fixer) => [ + fixer.replaceText(offendingChild, "null"), + fixer.replaceText(operatorToken, looseOperator), + ], + }, + ], + }); + } + }, + }); + }; +} diff --git a/.pkgs/function-rules/tsconfig.json b/.pkgs/function-rules/tsconfig.json new file mode 100644 index 0000000000..d4467c87a9 --- /dev/null +++ b/.pkgs/function-rules/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/.pkgs/eslint-plugin-local/tsdown.config.ts b/.pkgs/function-rules/tsdown.config.ts similarity index 91% rename from .pkgs/eslint-plugin-local/tsdown.config.ts rename to .pkgs/function-rules/tsdown.config.ts index a8113ef8a8..6c42ba50b8 100644 --- a/.pkgs/eslint-plugin-local/tsdown.config.ts +++ b/.pkgs/function-rules/tsdown.config.ts @@ -3,8 +3,9 @@ import type { UserConfig } from "tsdown"; export default { clean: true, dts: true, - entry: ["src/index.ts"], + entry: ["index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/apps/website/content/docs/contributing.mdx b/apps/website/content/docs/contributing.mdx index 5534363ccc..d8c77e3ca1 100644 --- a/apps/website/content/docs/contributing.mdx +++ b/apps/website/content/docs/contributing.mdx @@ -130,7 +130,6 @@ This section provides a summary of the packages in the monorepo. ### Local Packages - `.pkgs/configs`: Workspace config bases -- `.pkgs/eslint-plugin-local`: Internal workspace ESLint plugin ### Internal Packages diff --git a/apps/website/package.json b/apps/website/package.json index d231112cfe..2ea0995aea 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -25,7 +25,7 @@ "next-view-transitions": "^0.3.4", "react": "^19.2.0", "react-dom": "^19.2.0", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "twoslash": "^0.3.4" }, "devDependencies": { diff --git a/eslint.config.ts b/eslint.config.ts index cbd6f5599c..7a12dbef60 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -11,8 +11,9 @@ import { disableTypeChecked, strictTypeChecked, } from "@local/configs/eslint"; -import pluginLocal from "@local/eslint-plugin-local"; +import { nullishComparison } from "@local/function-rules"; import { recommended as fastImportRecommended } from "eslint-plugin-fast-import"; +import functionRule from "eslint-plugin-function-rule"; import pluginVitest from "eslint-plugin-vitest"; import { defineConfig, globalIgnores } from "eslint/config"; import tseslint from "typescript-eslint"; @@ -25,6 +26,8 @@ const packagesTsConfigs = [ "packages/*/*/tsconfig.json", ]; +const nullishComparisonRule = nullishComparison(); + export default defineConfig([ includeIgnoreFile(gitignore, "Imported .gitignore patterns") as never, globalIgnores([ @@ -52,14 +55,21 @@ export default defineConfig([ }, }, plugins: { - local: pluginLocal, + "function-rule": functionRule((context) => ({ + ...nullishComparisonRule(context), + TemplateLiteral(node) { + if (node.loc?.start.line !== node.loc?.end.line) { + context.report({ + node, + message: "Avoid multiline template expressions.", + }); + } + }, + })), }, rules: { - // Part: local rules - "local/avoid-multiline-template-expression": "warn", - "local/prefer-eqeq-nullish-comparison": "warn", - "fast-import/no-unused-exports": "off", + "function-rule/function-rule": "error", }, }, { @@ -97,7 +107,7 @@ export default defineConfig([ }, rules: { "@typescript-eslint/no-empty-function": ["error", { allow: ["arrowFunctions"] }], - "local/avoid-multiline-template-expression": "off", + "function-rule/function-rule": "off", }, }, disableProblematicEslintJsRules, diff --git a/package.json b/package.json index 5dbd22bda3..0939237d59 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@eslint/compat": "^1.4.1", "@eslint/config-inspector": "^1.3.0", "@local/configs": "workspace:*", - "@local/eslint-plugin-local": "workspace:*", + "@local/function-rules": "workspace:*", "@radix-ui/react-toast": "^1.2.15", "@tsconfig/node22": "^22.0.2", "@tsconfig/strictest": "^2.0.7", @@ -74,6 +74,7 @@ "effect": "^3.19.3", "eslint": "^9.39.1", "eslint-plugin-fast-import": "^1.5.3", + "eslint-plugin-function-rule": "^0.0.7", "eslint-plugin-vitest": "^0.5.4", "mdxlint": "^1.0.0", "publint": "^0.3.15", diff --git a/packages/plugins/eslint-plugin-react-debug/tsdown.config.ts b/packages/plugins/eslint-plugin-react-debug/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin-react-debug/tsdown.config.ts +++ b/packages/plugins/eslint-plugin-react-debug/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/plugins/eslint-plugin-react-dom/tsdown.config.ts b/packages/plugins/eslint-plugin-react-dom/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin-react-dom/tsdown.config.ts +++ b/packages/plugins/eslint-plugin-react-dom/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/tsdown.config.ts b/packages/plugins/eslint-plugin-react-hooks-extra/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/tsdown.config.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/plugins/eslint-plugin-react-naming-convention/tsdown.config.ts b/packages/plugins/eslint-plugin-react-naming-convention/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/tsdown.config.ts +++ b/packages/plugins/eslint-plugin-react-naming-convention/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/plugins/eslint-plugin-react-web-api/tsdown.config.ts b/packages/plugins/eslint-plugin-react-web-api/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin-react-web-api/tsdown.config.ts +++ b/packages/plugins/eslint-plugin-react-web-api/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts index 8e14ceb300..378a52c1cd 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts @@ -75,7 +75,8 @@ export function create(context: RuleContext): RuleListener { } // Enforce explicit `={true}` for boolean attributes (e.g., `prop={true}` instead of `prop`) case policy === -1 - && value === null: { // eslint-disable-line local/prefer-eqeq-nullish-comparison + // eslint-disable-next-line function-rule/function-rule + && value === null: { context.report({ messageId: "jsxShorthandBoolean", node: node.value ?? node, diff --git a/packages/plugins/eslint-plugin-react-x/tsdown.config.ts b/packages/plugins/eslint-plugin-react-x/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin-react-x/tsdown.config.ts +++ b/packages/plugins/eslint-plugin-react-x/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/plugins/eslint-plugin/tsdown.config.ts b/packages/plugins/eslint-plugin/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/plugins/eslint-plugin/tsdown.config.ts +++ b/packages/plugins/eslint-plugin/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/shared/tsdown.config.ts b/packages/shared/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/shared/tsdown.config.ts +++ b/packages/shared/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/utilities/ast/src/literal.ts b/packages/utilities/ast/src/literal.ts index 951744098c..cccbf6ab35 100644 --- a/packages/utilities/ast/src/literal.ts +++ b/packages/utilities/ast/src/literal.ts @@ -21,7 +21,7 @@ export function isLiteral(node: TSESTree.Node, type?: LiteralType) { case "boolean": return typeof node.value === "boolean"; case "null": - // eslint-disable-next-line local/prefer-eqeq-nullish-comparison + // eslint-disable-next-line function-rule/function-rule return node.value === null; case "number": return typeof node.value === "number"; diff --git a/packages/utilities/ast/src/node-format.ts b/packages/utilities/ast/src/node-format.ts index 9601f24f30..f0a6193b1d 100644 --- a/packages/utilities/ast/src/node-format.ts +++ b/packages/utilities/ast/src/node-format.ts @@ -6,7 +6,7 @@ import { delimiterCase, replace, toLowerCase } from "string-ts"; import { isJSX } from "./node-is"; function getLiteralValueType(input: bigint | boolean | null | number | string | symbol) { - // eslint-disable-next-line local/prefer-eqeq-nullish-comparison + // eslint-disable-next-line function-rule/function-rule if (input === null) return "null"; return typeof input; } diff --git a/packages/utilities/ast/tsdown.config.ts b/packages/utilities/ast/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/utilities/ast/tsdown.config.ts +++ b/packages/utilities/ast/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/utilities/eff/src/index.ts b/packages/utilities/eff/src/index.ts index 481935396e..471b8b8272 100644 --- a/packages/utilities/eff/src/index.ts +++ b/packages/utilities/eff/src/index.ts @@ -60,7 +60,6 @@ /* eslint-disable @typescript-eslint/unified-signatures */ /* eslint-disable jsdoc/check-param-names */ /* eslint-disable jsdoc/require-param-description */ -/* eslint-disable local/prefer-eqeq-nullish-comparison */ /* eslint-disable prefer-rest-params */ // #endregion @@ -145,6 +144,7 @@ export function isArray(data: ArrayLike | T): data is NarrowedTo(data: T | object): data is NarrowedTo { + // eslint-disable-next-line function-rule/function-rule return typeof data === "object" && data !== null; } diff --git a/packages/utilities/eff/tsdown.config.ts b/packages/utilities/eff/tsdown.config.ts index 7a7985ad59..cb3efcc0cc 100644 --- a/packages/utilities/eff/tsdown.config.ts +++ b/packages/utilities/eff/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/packages/utilities/var/tsdown.config.ts b/packages/utilities/var/tsdown.config.ts index a8113ef8a8..7a01bd7532 100644 --- a/packages/utilities/var/tsdown.config.ts +++ b/packages/utilities/var/tsdown.config.ts @@ -5,6 +5,7 @@ export default { dts: true, entry: ["src/index.ts"], external: ["eslint", "typescript"], + fixedExtension: false, format: ["esm"], minify: false, outDir: "dist", @@ -12,5 +13,4 @@ export default { sourcemap: false, target: "node20", treeshake: true, - fixedExtension: false, } satisfies UserConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3e2fa6fd3..de8eca775b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,9 @@ importers: '@local/configs': specifier: workspace:* version: link:.pkgs/configs - '@local/eslint-plugin-local': + '@local/function-rules': specifier: workspace:* - version: link:.pkgs/eslint-plugin-local + version: link:.pkgs/function-rules '@radix-ui/react-toast': specifier: ^1.2.15 version: 1.2.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -93,6 +93,9 @@ importers: eslint-plugin-fast-import: specifier: ^1.5.3 version: 1.5.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-function-rule: + specifier: ^0.0.7 + version: 0.0.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-vitest: specifier: ^0.5.4 version: 0.5.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) @@ -171,6 +174,9 @@ importers: eslint-plugin-function: specifier: ^0.0.33 version: 0.0.33(eslint@9.39.1(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3) + eslint-plugin-function-rule: + specifier: ^0.0.7 + version: 0.0.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-jsdoc: specifier: ^61.1.12 version: 61.1.12(eslint@9.39.1(jiti@2.6.1)) @@ -190,75 +196,15 @@ importers: specifier: ^8.46.3 version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - .pkgs/eslint-plugin-local: + .pkgs/function-rules: dependencies: - '@eslint-react/ast': - specifier: workspace:* - version: link:../../packages/utilities/ast - '@eslint-react/eff': - specifier: workspace:* - version: link:../../packages/utilities/eff - '@eslint-react/shared': - specifier: workspace:* - version: link:../../packages/shared - '@eslint-react/var': - specifier: workspace:* - version: link:../../packages/utilities/var - '@eslint/js': - specifier: 9.39.1 - version: 9.39.1 - '@stylistic/eslint-plugin': - specifier: ^5.5.0 - version: 5.5.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': - specifier: ^8.46.3 - version: 8.46.3 - '@typescript-eslint/type-utils': - specifier: ^8.46.3 - version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': - specifier: ^8.46.3 - version: 8.46.3 - '@typescript-eslint/utils': - specifier: ^8.46.3 - version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: - specifier: 9.39.1 - version: 9.39.1(jiti@2.6.1) - eslint-plugin-de-morgan: - specifier: ^2.0.0 - version: 2.0.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-jsdoc: - specifier: ^61.1.12 - version: 61.1.12(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-perfectionist: - specifier: ^4.15.1 - version: 4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-regexp: - specifier: ^2.10.0 - version: 2.10.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) - string-ts: - specifier: ^2.2.1 - version: 2.2.1 - ts-pattern: - specifier: ^5.9.0 - version: 5.9.0 typescript: specifier: ^5.9.3 version: 5.9.3 devDependencies: - '@local/configs': - specifier: workspace:* - version: link:../configs - '@types/react': - specifier: ^19.2.2 - version: 19.2.2 - '@types/react-dom': - specifier: ^19.2.2 - version: 19.2.2(@types/react@19.2.2) + eslint: + specifier: 9.39.1 + version: 9.39.1(jiti@2.6.1) tsdown: specifier: ^0.16.1 version: 0.16.1(publint@0.3.15)(typescript@5.9.3) @@ -311,8 +257,8 @@ importers: specifier: ^19.2.0 version: 19.2.0(react@19.2.0) tailwind-merge: - specifier: ^3.3.1 - version: 3.3.1 + specifier: ^3.4.0 + version: 3.4.0 twoslash: specifier: ^0.3.4 version: 0.3.4(typescript@5.9.3) @@ -4560,6 +4506,12 @@ packages: peerDependencies: eslint: 9.39.1 + eslint-plugin-function-rule@0.0.7: + resolution: {integrity: sha512-wL+5krOLAwCOZa0sRMgLeD0vneBB6R4t+qEnsbvhSgbY32gnSf5lsHbhPLdDivhFIaywQmzesdx4axsPnhiutQ==} + peerDependencies: + eslint: 9.39.1 + typescript: ^5.9.3 + eslint-plugin-function@0.0.33: resolution: {integrity: sha512-Y82nfIPBiMV/RAQqpKPuX+QpOh0leJTG6BKPkX6jN/VayzHWOdtRIBE5QOe/0z9OV+Z3Xj7WOpupNQOd7MDRRQ==} engines: {node: '>=20.10.0'} @@ -5369,6 +5321,7 @@ packages: lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -6399,8 +6352,8 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} tailwindcss-animated@2.0.0: resolution: {integrity: sha512-anNNGpxNgjydD8p1lcJjxxH+XbjW6KR8Xs29owTrbcf3tOJ6IRblpyFob43HBkfxFJJTAfFQqugoEG2b1EsR0A==} @@ -10340,6 +10293,12 @@ snapshots: - supports-color - typescript + eslint-plugin-function-rule@0.0.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint/core': 0.17.0 + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + eslint-plugin-function@0.0.33(eslint@9.39.1(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3): dependencies: '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -10739,7 +10698,7 @@ snapshots: mdast-util-to-hast: 13.2.0 react: 19.2.0 shiki: 3.14.0 - tailwind-merge: 3.3.1 + tailwind-merge: 3.4.0 twoslash: 0.3.4(typescript@5.9.3) optionalDependencies: '@types/react': 19.2.2 @@ -10788,7 +10747,7 @@ snapshots: react-dom: 19.2.0(react@19.2.0) react-medium-image-zoom: 5.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) scroll-into-view-if-needed: 3.1.0 - tailwind-merge: 3.3.1 + tailwind-merge: 3.4.0 optionalDependencies: '@types/react': 19.2.2 next: 16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -12720,7 +12679,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwind-merge@3.3.1: {} + tailwind-merge@3.4.0: {} tailwindcss-animated@2.0.0(tailwindcss@4.1.17): dependencies: