chore: migrate from ESLint to Biome for linting (#3694)
Replace ESLint with Biome for ~14x faster linting (~360ms vs ~5s). - Add biome.json with rules matching ESLint parity (Next.js, a11y, TypeScript, unused vars/imports) - Remove eslint, @typescript-eslint/*, eslint-config-next, eslint-plugin-jsx-a11y, @eslint/* from deps - Remove eslint.config.mjs - Update lint script to: tsgo --noEmit && biome check . - Fix 11 real code issues caught by Biome (banned types, explicit any, unsafe finally, unreachable code, shadowed names) - Disable Biome formatter (Prettier stays for Tailwind class sorting) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,3 +24,6 @@
|
||||
- TTS uses independent section tracking (`#ttsSectionIndex`) decoupled from view
|
||||
- Safe area insets flow: Native plugin -> useSafeAreaInsets hook -> component styles
|
||||
- Dropdown menus use `DropdownContext` (not blur-based) for screen reader compat
|
||||
|
||||
## Workflow
|
||||
- [Always rebase before PR](feedback_pr_rebase.md) — rebase onto origin/main before creating PRs
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Always rebase before PR
|
||||
description: Rebase to origin/main before creating pull requests
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Always rebase the branch onto origin/main before creating a pull request.
|
||||
|
||||
**Why:** The user wants PRs to be up-to-date with main to avoid merge conflicts and keep a clean history.
|
||||
|
||||
**How to apply:** Before running `gh pr create`, always run `git fetch origin && git rebase origin/main` first. If there are conflicts, resolve them before proceeding.
|
||||
@@ -22,7 +22,7 @@ pnpm tauri:dev:test # Start Tauri app with webdriver
|
||||
pnpm test:tauri # Run Tauri integration tests
|
||||
|
||||
# Linting & Formatting
|
||||
pnpm lint # ESLint
|
||||
pnpm lint # Biome (linter) + tsgo (type check)
|
||||
pnpm format # Prettier (runs from monorepo root)
|
||||
pnpm format:check # Check formatting without writing
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
||||
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
||||
"files": { "ignoreUnknown": true },
|
||||
"formatter": { "enabled": false },
|
||||
"assist": { "enabled": false },
|
||||
"css": { "linter": { "enabled": false } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"includes": [
|
||||
"**",
|
||||
"!.next/**",
|
||||
"!.open-next/**",
|
||||
"!.wrangler/**",
|
||||
"!.claude/**",
|
||||
"!dist/**",
|
||||
"!out/**",
|
||||
"!build/**",
|
||||
"!public/**",
|
||||
"!src-tauri/**",
|
||||
"!next-env.d.ts",
|
||||
"!i18next-scanner.config.cjs"
|
||||
],
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"recommended": true,
|
||||
"useKeyWithClickEvents": "off",
|
||||
"useKeyWithMouseEvents": "off",
|
||||
"noSvgWithoutTitle": "off",
|
||||
"noLabelWithoutControl": "off",
|
||||
"useSemanticElements": "off",
|
||||
"noAriaHiddenOnFocusable": "off",
|
||||
"noInteractiveElementToNoninteractiveRole": "off",
|
||||
"noNoninteractiveElementToInteractiveRole": "off",
|
||||
"noNoninteractiveElementInteractions": "off",
|
||||
"noStaticElementInteractions": "off",
|
||||
"noNoninteractiveTabindex": "off",
|
||||
"useButtonType": "off",
|
||||
"useAriaPropsSupportedByRole": "off",
|
||||
"useFocusableInteractive": "off",
|
||||
"noAutofocus": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noStaticOnlyClass": "off",
|
||||
"noUselessSwitchCase": "off",
|
||||
"noUselessFragments": "off",
|
||||
"noUselessCatch": "off",
|
||||
"useLiteralKeys": "off",
|
||||
"useOptionalChain": "off",
|
||||
"noThisInStatic": "off",
|
||||
"useArrowFunction": "off",
|
||||
"noUselessEscapeInRegex": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"noUnusedImports": "warn",
|
||||
"useExhaustiveDependencies": "off",
|
||||
"useHookAtTopLevel": "error",
|
||||
"useJsxKeyInIterable": "error",
|
||||
"noChildrenProp": "error",
|
||||
"noNextAsyncClientComponent": "warn",
|
||||
"noSwitchDeclarations": "off",
|
||||
"noUndeclaredVariables": "off",
|
||||
"noEmptyCharacterClassInRegex": "off",
|
||||
"useParseIntRadix": "off",
|
||||
"noEmptyPattern": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"noBeforeInteractiveScriptOutsideDocument": "warn",
|
||||
"noDuplicateEnumValues": "error",
|
||||
"noSyncScripts": "warn",
|
||||
"useInlineScriptId": "error"
|
||||
},
|
||||
"performance": {
|
||||
"noImgElement": "off",
|
||||
"noUnwantedPolyfillio": "warn",
|
||||
"useGoogleFontPreconnect": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noBlankTarget": "off",
|
||||
"noDangerouslySetInnerHtmlWithChildren": "off",
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"useImportType": "off",
|
||||
"noParameterAssign": "off",
|
||||
"useDefaultParameterLast": "off",
|
||||
"noUselessElse": "off",
|
||||
"noHeadElement": "warn",
|
||||
"noCommonJs": "off",
|
||||
"useFilenamingConvention": "off",
|
||||
"useNamingConvention": "off",
|
||||
"noUnusedTemplateLiteral": "off",
|
||||
"useTemplate": "off",
|
||||
"useExponentiationOperator": "off",
|
||||
"useNodejsImportProtocol": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noArrayIndexKey": "off",
|
||||
"noAssignInExpressions": "off",
|
||||
"noDoubleEquals": "off",
|
||||
"noDocumentImportInPage": "error",
|
||||
"noHeadImportInDocument": "error",
|
||||
"useGoogleFontDisplay": "warn",
|
||||
"noCommentText": "error",
|
||||
"noDuplicateJsxProps": "error",
|
||||
"noAsyncPromiseExecutor": "off",
|
||||
"noImplicitAnyLet": "off",
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noEmptyBlockStatements": "off",
|
||||
"useIterableCallbackReturn": "off",
|
||||
"noGlobalIsNan": "off",
|
||||
"noConfusingVoidType": "off",
|
||||
"noConstEnum": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import next from 'eslint-config-next';
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||
import tseslint from 'eslint-config-next/typescript';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...tseslint,
|
||||
...next,
|
||||
...nextVitals,
|
||||
{
|
||||
rules: {
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
globalIgnores([
|
||||
'node_modules/**',
|
||||
'.next/**',
|
||||
'.open-next/**',
|
||||
'.wrangler/**',
|
||||
'.claude/**',
|
||||
'dist/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'public/**',
|
||||
'src-tauri/**',
|
||||
'next-env.d.ts',
|
||||
'i18next-scanner.config.cjs',
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -15,7 +15,7 @@
|
||||
"start-web:vinext": "dotenv -e .env.web -- vinext start",
|
||||
"build-tauri": "dotenv -e .env.tauri -- next build",
|
||||
"i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs",
|
||||
"lint": "tsgo --noEmit && eslint --cache .",
|
||||
"lint": "tsgo --noEmit && biome check .",
|
||||
"test": "dotenv -e .env -e .env.test.local vitest",
|
||||
"test:browser": "vitest --config vitest.browser.config.mts --watch=false",
|
||||
"test:tauri": "bash scripts/test-tauri.sh",
|
||||
@@ -193,8 +193,6 @@
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitejs/plugin-rsc": "^0.5.21",
|
||||
@@ -211,9 +209,6 @@
|
||||
"cpx2": "^8.0.0",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import NumberInput from '@/components/settings/NumberInput';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { StaticListRow } from '@/app/reader/components/sidebar/TOCItem';
|
||||
import { TOCItem } from '@/libs/document';
|
||||
|
||||
@@ -45,8 +45,7 @@ export const setupSupabaseMocks = async (
|
||||
eq: vi.fn().mockResolvedValue(customResponses.update || { data: {}, error: null }),
|
||||
})),
|
||||
insert: vi.fn().mockResolvedValue(customResponses.insert || { data: {}, error: null }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
} as unknown as ReturnType<typeof supabase.from>);
|
||||
|
||||
vi.mocked(createSupabaseAdminClient).mockReturnValue({
|
||||
from: vi.fn(() => ({
|
||||
@@ -102,6 +101,5 @@ export const setupSupabaseMocks = async (
|
||||
match: vi.fn().mockResolvedValue(customResponses.adminDelete || { data: {}, error: null }),
|
||||
})),
|
||||
})),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
} as unknown as ReturnType<typeof createSupabaseAdminClient>);
|
||||
};
|
||||
|
||||
@@ -211,7 +211,7 @@ describe('createBookGroups', () => {
|
||||
|
||||
expect(john?.books).toHaveLength(1);
|
||||
expect(jane?.books).toHaveLength(1);
|
||||
expect(john?.books[0]!.hash).toBe(jane?.books[0]!.hash);
|
||||
expect(john?.books[0]?.hash).toBe(jane?.books[0]?.hash);
|
||||
});
|
||||
|
||||
it('should leave books without author as ungrouped', () => {
|
||||
|
||||
@@ -1103,8 +1103,7 @@ describe('proofreadTransformer', () => {
|
||||
enabled: true,
|
||||
isRegex: false,
|
||||
caseSensitive: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
order: undefined as any,
|
||||
order: 0,
|
||||
wholeWord: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,10 +4,13 @@ import { TxtToEpubConverter } from '@/utils/txt';
|
||||
/**
|
||||
* Access private createChapterRegexps via a thin test subclass.
|
||||
*/
|
||||
type TxtConverterPrivateAPI = {
|
||||
createChapterRegexps(language: string): RegExp[];
|
||||
};
|
||||
|
||||
class TestableConverter extends TxtToEpubConverter {
|
||||
getChapterRegexps(language: string): RegExp[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (this as any).createChapterRegexps(language);
|
||||
return (this as unknown as TxtConverterPrivateAPI).createChapterRegexps(language);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ interface ErrorPageProps {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export default function Error({ error, reset }: ErrorPageProps) {
|
||||
export default function ErrorPage({ error, reset }: ErrorPageProps) {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const [browserInfo, setBrowserInfo] = useState('');
|
||||
|
||||
@@ -149,8 +149,8 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
} finally {
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
setLoading(false);
|
||||
return available;
|
||||
}
|
||||
return available;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
RiFolderOpenLine,
|
||||
RiCheckboxCircleFill,
|
||||
|
||||
@@ -892,7 +892,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
(libraryBooks.some((book) => !book.deletedAt) ? (
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
aria-label=''
|
||||
aria-label='Library bookshelf'
|
||||
ref={osRef}
|
||||
className='flex-grow'
|
||||
options={{ scrollbars: { autoHide: 'scroll' } }}
|
||||
|
||||
@@ -483,7 +483,6 @@ export default function BrowserPage() {
|
||||
console.error('Download error:', e);
|
||||
throw e;
|
||||
}
|
||||
return;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[user, state.baseURL, appService, libraryLoaded],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { RiTranslateAi } from 'react-icons/ri';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
|
||||
@@ -118,7 +118,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
const templateData = {
|
||||
title: bookTitle,
|
||||
author: bookAuthor,
|
||||
exportDate: new Date().getTime(),
|
||||
exportDate: Date.now(),
|
||||
chapters: sortedGroups.map((group) => ({
|
||||
title: group.label || _('Untitled'),
|
||||
annotations: group.booknotes.map((note) => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { MdInfoOutline } from 'react-icons/md';
|
||||
import { Book } from '@/types/book';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import {
|
||||
MdPlayArrow,
|
||||
MdOutlinePause,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdAlarm } from 'react-icons/md';
|
||||
import { TbChevronCompactDown, TbChevronCompactUp } from 'react-icons/tb';
|
||||
import { RiVoiceAiFill } from 'react-icons/ri';
|
||||
|
||||
@@ -49,7 +49,7 @@ const useBooksManager = () => {
|
||||
};
|
||||
|
||||
const getNextBookKey = (bookKey: string) => {
|
||||
const index = bookKeys.findIndex((key) => key === bookKey);
|
||||
const index = bookKeys.indexOf(bookKey);
|
||||
const nextIndex = (index + 1) % bookKeys.length;
|
||||
return bookKeys[nextIndex]!;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { isMacPlatform } from '@/services/environment';
|
||||
import { getShortcutsForDisplay } from '@/helpers/shortcuts';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useState, useContext, ReactNode, useEffect } from 'react';
|
||||
import { createContext, useState, useContext, ReactNode, useEffect } from 'react';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
@@ -39,7 +39,7 @@ export type StripeAvailablePlan = AvailablePlan & {
|
||||
export const fetchStripePlans = async () => {
|
||||
const response = await fetch(WEB_STRIPE_PLANS_URL);
|
||||
const data = await response.json();
|
||||
return data && data instanceof Array ? data : [];
|
||||
return data && Array.isArray(data) ? data : [];
|
||||
};
|
||||
|
||||
export const createStripeCheckoutSession = async (
|
||||
|
||||
@@ -121,7 +121,7 @@ export async function GET(req: NextRequest) {
|
||||
// TODO: Remove this hotfix for the initial race condition for books sync
|
||||
if (results.books?.length === 0 && since.getTime() < 1000) {
|
||||
const dummyHash = '00000000000000000000000000000000';
|
||||
const now = new Date().getTime();
|
||||
const now = Date.now();
|
||||
results.books.push({
|
||||
user_id: user.id,
|
||||
id: dummyHash,
|
||||
|
||||
@@ -3,8 +3,11 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
// Helper method to wait for a middleware to execute before continuing
|
||||
// And to throw an error when an error happens in a middleware
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export const runMiddleware = (req: NextApiRequest, res: NextApiResponse, fn: Function) => {
|
||||
export const runMiddleware = (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
fn: (req: NextApiRequest, res: NextApiResponse, cb: (result: unknown) => void) => void,
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn(req, res, (result: unknown) => {
|
||||
if (result instanceof Error) {
|
||||
|
||||
@@ -17,8 +17,8 @@ const createReaderWindow = (appService: AppService, url: string) => {
|
||||
center: true,
|
||||
resizable: true,
|
||||
title: appService.isMacOSApp ? '' : 'Readest',
|
||||
decorations: appService.isMacOSApp ? true : false,
|
||||
transparent: appService.isMacOSApp ? false : true,
|
||||
decorations: !!appService.isMacOSApp,
|
||||
transparent: !appService.isMacOSApp,
|
||||
shadow: appService.isMacOSApp ? undefined : true,
|
||||
titleBarStyle: appService.isMacOSApp ? 'overlay' : undefined,
|
||||
});
|
||||
|
||||
@@ -207,9 +207,7 @@ export class ParagraphIterator {
|
||||
this.#index = i;
|
||||
return block;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return this.first();
|
||||
}
|
||||
@@ -232,9 +230,7 @@ export class ParagraphIterator {
|
||||
this.#index = i;
|
||||
return block;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -36,5 +36,3 @@ workerContext.onmessage = async (event: MessageEvent<TxtConverterWorkerRequest>)
|
||||
workerContext.postMessage(response);
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
|
||||
@@ -43,7 +43,7 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
include: ['src/**/*.browser.test.ts'],
|
||||
onConsoleLog(log, type) {
|
||||
onConsoleLog(_log, type) {
|
||||
if (type === 'stdout') return false;
|
||||
},
|
||||
browser: {
|
||||
|
||||
+1
-4
@@ -14,11 +14,8 @@
|
||||
},
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@biomejs/biome": "^2.4.9",
|
||||
"@sindresorhus/tsconfig": "^6.0.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.3.3",
|
||||
|
||||
Generated
+95
-2137
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user