diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d87d5fe..96f60c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: - version: 9 + version: 10 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm - run: pnpm install - run: pnpm run lint diff --git a/demos/components/Loading.tsx b/demos/components/Loading.tsx index 64e98c7..7351ec4 100644 --- a/demos/components/Loading.tsx +++ b/demos/components/Loading.tsx @@ -1,5 +1,11 @@ import {css} from '@linaria/core'; -import {type CSSProperties, type ReactPortal, useMemo, useEffect, useState} from 'react'; +import { + type CSSProperties, + type ReactPortal, + useMemo, + useEffect, + useState +} from 'react'; import {createPortal} from 'react-dom'; import {cancel, useLoading, useRouter} from 'native-router-react'; @@ -15,7 +21,6 @@ export default function Loading(): ReactPortal | null { setPercent(0); }, [key]); - // eslint-disable-next-line consistent-return useEffect(() => { const remove = () => { if (el.parentElement) document.body.removeChild(el); @@ -47,9 +52,9 @@ export default function Loading(): ReactPortal | null { return createPortal( - home + home ); } diff --git a/demos/util/index.ts b/demos/util/index.ts index 89f6657..e307294 100644 --- a/demos/util/index.ts +++ b/demos/util/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export function sleep(interval: number): Promise { return new Promise((resolve) => { setTimeout(resolve, interval); diff --git a/demos/util/styles.tsx b/demos/util/styles.tsx index e842e54..c023be6 100644 --- a/demos/util/styles.tsx +++ b/demos/util/styles.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import {css} from '@linaria/core'; export const center = css` diff --git a/demos/views/index.tsx b/demos/views/index.tsx index 1560e3b..8908725 100644 --- a/demos/views/index.tsx +++ b/demos/views/index.tsx @@ -36,7 +36,6 @@ export default function App() { return ( } > diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e610f98 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,67 @@ +const path = require('path'); +const eslintPluginPrettier = require('eslint-plugin-prettier'); +const eslintPluginImport = require('eslint-plugin-import'); +const eslintPluginReact = require('eslint-plugin-react'); +const eslintPluginReactHooks = require('eslint-plugin-react-hooks'); +const eslintPluginJsxA11y = require('eslint-plugin-jsx-a11y'); +const tsEslintPlugin = require('@typescript-eslint/eslint-plugin'); +const tsEslintParser = require('@typescript-eslint/parser'); + +module.exports = [ + { + ignores: ['dist', 'node_modules', 'coverage', 'eslint.config.js'] + }, + { + files: ['**/*.{js,jsx,ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + browser: true, + node: true, + es6: true, + __DEV__: true + }, + parser: tsEslintParser, + parserOptions: { + project: './tsconfig.json' + } + }, + plugins: { + prettier: eslintPluginPrettier, + import: eslintPluginImport, + react: eslintPluginReact, + 'react-hooks': eslintPluginReactHooks, + 'jsx-a11y': eslintPluginJsxA11y, + '@typescript-eslint': tsEslintPlugin + }, + rules: { + 'prettier/prettier': 'error', + 'react/jsx-props-no-spreading': 'off', + 'no-return-assign': ['error', 'except-parens'], + 'no-sequences': 'off', + 'no-shadow': 'off', + 'no-plusplus': 'off', + 'no-param-reassign': 'off', + 'no-void': 'off', + 'react/require-default-props': 'off', + 'react/react-in-jsx-scope': 'off', + '@typescript-eslint/no-use-before-define': ['error', {functions: false}], + 'no-use-before-define': ['error', {functions: false}], + 'import/no-extraneous-dependencies': [ + 'error', + {devDependencies: ['demos/**/*', 'test/**/*']} + ], + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' + }, + settings: { + react: { + version: 'detect' + }, + 'import/resolver': { + typescript: {} + } + } + } +]; diff --git a/src/memo.tsx b/src/memo.tsx index 839763a..f015da5 100644 --- a/src/memo.tsx +++ b/src/memo.tsx @@ -39,8 +39,10 @@ export function memoBase

( ) { const MemoComponent = reactMemo(WrappedComponent, propsAreEqual); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -const FixedComponent = forwardRef(function FixedComponent(props: any, ref: any) { + const FixedComponent = forwardRef(function FixedComponent( + props: any, + ref: any + ) { const memoEventsRef = useRef>({}); const memoEvents = memoEventsRef.current; const newMemoEvents = Object.keys(props) diff --git a/test/async.test.tsx b/test/async.test.tsx index a81d6fd..9035fa4 100644 --- a/test/async.test.tsx +++ b/test/async.test.tsx @@ -1,33 +1,33 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import { useState, useEffect } from 'react'; -import { useRun, useInjectable, createMemoryCacheProvider } from '../src/async'; +import {describe, it, expect, vi} from 'vitest'; +import {render, screen, waitFor} from '@testing-library/react'; +import {useState, useEffect} from 'react'; +import {useRun, useInjectable, createMemoryCacheProvider} from '../src/async'; describe('async hooks', () => { describe('useRun', () => { it('should run function on mount', () => { const fn = vi.fn(() => 'result'); - + function TestComponent() { useRun(fn, []); return

done
; } - + render(); expect(fn).toHaveBeenCalled(); }); it('should re-run when dependencies change', () => { const fn = vi.fn(() => 'result'); - - function TestComponent({ deps }: { deps: number[] }) { + + function TestComponent({deps}: {deps: number[]}) { useRun(fn, deps); return
done
; } - - const { rerender } = render(); + + const {rerender} = render(); expect(fn).toHaveBeenCalledTimes(1); - + rerender(); expect(fn).toHaveBeenCalledTimes(2); }); @@ -36,20 +36,20 @@ describe('async hooks', () => { describe('useInjectable', () => { it('should create injectable function', async () => { const fetchData = vi.fn(async (id: number) => `result ${id}`); - + function TestComponent() { const injectable = useInjectable(fetchData); const [result, setResult] = useState(''); - + useEffect(() => { injectable(1).then(setResult); }, [injectable]); - + return
{result}
; } - + render(); - + await waitFor(() => { expect(screen.getByText('result 1')).toBeDefined(); }); @@ -60,9 +60,9 @@ describe('async hooks', () => { it('should create cache provider', () => { const provider = createMemoryCacheProvider({ cacheTime: 60000, - hash: (key) => JSON.stringify(key), + hash: (key) => JSON.stringify(key) }); - + expect(provider).toBeDefined(); expect(provider.get).toBeDefined(); expect(provider.set).toBeDefined(); @@ -74,21 +74,21 @@ describe('async hooks', () => { it('should cache and retrieve data', async () => { const provider = createMemoryCacheProvider({ cacheTime: 60000, - hash: (key) => JSON.stringify(key), + hash: (key) => JSON.stringify(key) }); - + provider.set(['test'], 'cached value'); const result = await provider.get(['test']); - + expect(result).toEqual(['cached value', expect.any(Number)]); }); it('should return undefined for missing key', async () => { const provider = createMemoryCacheProvider({ cacheTime: 60000, - hash: (key) => JSON.stringify(key), + hash: (key) => JSON.stringify(key) }); - + const result = await provider.get(['missing']); expect(result).toBeUndefined(); }); @@ -96,25 +96,25 @@ describe('async hooks', () => { it('should delete cache entry', () => { const provider = createMemoryCacheProvider({ cacheTime: 60000, - hash: (key) => JSON.stringify(key), + hash: (key) => JSON.stringify(key) }); - + provider.set(['test'], 'value'); provider.delete(['test']); - + expect(provider.get(['test'])).toBeUndefined(); }); it('should clear all cache', () => { const provider = createMemoryCacheProvider({ cacheTime: 60000, - hash: (key) => JSON.stringify(key), + hash: (key) => JSON.stringify(key) }); - + provider.set(['a'], 'value a'); provider.set(['b'], 'value b'); provider.clear(); - + expect(provider.get(['a'])).toBeUndefined(); expect(provider.get(['b'])).toBeUndefined(); }); @@ -122,13 +122,13 @@ describe('async hooks', () => { it('should use hook correctly', () => { const provider = createMemoryCacheProvider({ cacheTime: 60000, - hash: (key) => JSON.stringify(key), + hash: (key) => JSON.stringify(key) }); - + const cleanup = provider.use(); expect(cleanup).toBeDefined(); expect(typeof cleanup).toBe('function'); - + cleanup(); }); }); diff --git a/test/memo.test.tsx b/test/memo.test.tsx index 420cbc5..e16f5ca 100644 --- a/test/memo.test.tsx +++ b/test/memo.test.tsx @@ -1,7 +1,7 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; -import React, { forwardRef, useState } from 'react'; -import { defaultTestEvent, memoBase } from '../src/memo'; +import {describe, it, expect, vi} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; +import React, {forwardRef, useState} from 'react'; +import {defaultTestEvent, memoBase} from '../src/memo'; describe('memo', () => { describe('defaultTestEvent', () => { @@ -48,50 +48,63 @@ describe('memo', () => { describe('memoBase', () => { it('should Render component with options', () => { - const Component = ({ name }: { name: string }) =>
{name}
; - const MemoComponent = memoBase(Component, { testEvent: defaultTestEvent }); - - render(); - + const Component = ({name}: {name: string}) =>
{name}
; + const MemoComponent = memoBase(Component, {testEvent: defaultTestEvent}); + + render(); + expect(screen.getByText('test')).toBeDefined(); }); it('should forward ref', () => { - const Component = ({ name }: { name: string }, ref: React.Ref) =>
{name}
; - const MemoComponent = memoBase(React.memo(forwardRef(Component)), { testEvent: defaultTestEvent }); - const ref = { current: null }; - - render(); - + const Component = ( + {name}: {name: string}, + ref: React.Ref + ) =>
{name}
; + const MemoComponent = memoBase(React.memo(forwardRef(Component)), { + testEvent: defaultTestEvent + }); + const ref = {current: null}; + + render(); + expect(ref.current).toBeDefined(); }); it('should use custom testEvent', () => { const testEvent = (k: string) => k.startsWith('data-'); - const Component = ({ dataId, onClick }: { dataId: string; onClick: () => void }) => ( -
test
+ const Component = ({ + dataId, + onClick + }: { + dataId: string; + onClick: () => void; + }) => ( +
+ test +
); - const MemoComponent = memoBase(Component, { testEvent }); - + const MemoComponent = memoBase(Component, {testEvent}); + const onClick = vi.fn(); - render(); - + render(); + fireEvent.click(screen.getByTestId('123')); expect(onClick).toHaveBeenCalled(); }); it('should memoize event handlers', async () => { - const Component = ({ onClick }: { onClick: () => void }) => ( + const Component = ({onClick}: {onClick: () => void}) => ( ); - const MemoComponent = memoBase(Component, { testEvent: defaultTestEvent }); - + const MemoComponent = memoBase(Component, {testEvent: defaultTestEvent}); + const onClick = vi.fn(); - const { rerender } = render(); - + const {rerender} = render(); + fireEvent.click(screen.getByRole('button')); expect(onClick).toHaveBeenCalledTimes(1); - + rerender(); fireEvent.click(screen.getByRole('button')); expect(onClick).toHaveBeenCalledTimes(2); @@ -101,43 +114,59 @@ describe('memo', () => { const testEvent = (k: string) => k.startsWith('on'); const onClick = vi.fn(); const onFocus = vi.fn(); - - const Component = ({ onClick, onFocus }: { onClick: () => void; onFocus: () => void }) => ( + + const Component = ({ + onClick, + onFocus + }: { + onClick: () => void; + onFocus: () => void; + }) => (
- +
); - const MemoComponent = memoBase(Component, { testEvent }); - + const MemoComponent = memoBase(Component, {testEvent}); + render(); - + fireEvent.click(screen.getByRole('button')); expect(onClick).toHaveBeenCalled(); }); it('should not memoize non-function props', () => { - const Component = ({ name, onClick }: { name: string; onClick: () => void }) => ( + const Component = ({ + name, + onClick + }: { + name: string; + onClick: () => void; + }) => (
{name}
); - const MemoComponent = memoBase(Component, { testEvent: defaultTestEvent }); - + const MemoComponent = memoBase(Component, {testEvent: defaultTestEvent}); + const onClick = vi.fn(); - const { rerender } = render(); - - rerender(); - + const {rerender} = render( + + ); + + rerender(); + expect(screen.getByText('second')).toBeDefined(); }); it('should have correct displayName', () => { - const Component = function TestComponent({ name }: { name: string }) { + const Component = function TestComponent({name}: {name: string}) { return
{name}
; }; - const MemoComponent = memoBase(Component, { testEvent: defaultTestEvent }); - + const MemoComponent = memoBase(Component, {testEvent: defaultTestEvent}); + expect(MemoComponent.displayName).toContain('FixedEvents'); }); }); diff --git a/test/setup.ts b/test/setup.ts index 10b2cd2..0c6b3a3 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,6 +1,6 @@ import '@testing-library/react'; -import { expect, afterEach, vi } from 'vitest'; -import { cleanup } from '@testing-library/react'; +import {expect, afterEach, vi} from 'vitest'; +import {cleanup} from '@testing-library/react'; afterEach(() => { cleanup(); @@ -10,5 +10,5 @@ global.localStorage = { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), - clear: vi.fn(), + clear: vi.fn() };