diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..1cfd3d8e
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,26 @@
+{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
+ "extends": [
+ "eslint:recommended",
+ "plugin:import/recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier",
+ "plugin:react-hooks/recommended",
+ "plugin:react-perf/recommended"
+ ],
+ "env": {
+ "node": true,
+ "es6": true
+ },
+ "settings": {
+ "import/parsers": {
+ "@typescript-eslint/parser": [
+ ".ts"
+ ]
+ },
+ "import/resolver": {
+ "typescript": {}
+ }
+ }
+}
diff --git a/package.json b/package.json
index c12c7955..9ad89899 100644
--- a/package.json
+++ b/package.json
@@ -91,9 +91,9 @@
"build:cjs": "tsc -p tsconfig.cjs.json",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"**/*.{ts,tsx,md}\" --write",
- "fix:tslint": "tslint --fix --project .",
+ "fix:eslint": "eslint src --fix",
"test": "run-s build test:*",
- "test:lint": "tslint --project . && prettier \"src/**/*.{ts,tsx}\" --list-different",
+ "test:lint": "eslint src && prettier \"src/**/*.{ts,tsx}\" --list-different",
"test:unit": "jest --coverage",
"cov:send": "codecov",
"watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"",
@@ -116,7 +116,8 @@
"next": ">=11",
"react": ">=16.8",
"react-dom": ">=16.8",
- "zustand": ">=4.4.0"
+ "zustand": ">=4.4.0",
+ "type-fest": ">=4"
},
"peerDependenciesMeta": {
"next": {
@@ -127,6 +128,12 @@
}
},
"devDependencies": {
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-react-perf": "^3.3.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-import-resolver-typescript": "^3.6.0",
+ "type-fest": "^4.3.1",
"@babel/core": "^7.20.12",
"@bitjson/npm-scripts-info": "^1.0.0",
"@storybook/addon-actions": "^6.5.15",
@@ -170,9 +177,10 @@
"trash-cli": "^5.0.0",
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
- "tslint": "^6.1.3",
- "tslint-config-prettier": "^1.18.0",
- "tslint-immutable": "^6.0.1",
+ "eslint": "^8.48.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@typescript-eslint/parser": "^6.5.0",
+ "@typescript-eslint/eslint-plugin": "^6.5.0",
"typedoc": "^0.23.24",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
diff --git a/src/__tests__/defaults.tsx b/src/__tests__/defaults.tsx
index 79521f7d..322fb3f3 100644
--- a/src/__tests__/defaults.tsx
+++ b/src/__tests__/defaults.tsx
@@ -1,4 +1,4 @@
-/* tslint:disable:no-expression-statement no-object-mutation */
+/* eslint-disable react-perf/jsx-no-new-function-as-prop */
import { render, cleanup, screen, act } from '@testing-library/react'
import userEventImport from '@testing-library/user-event'
import { createMemoryHistory } from 'history'
diff --git a/src/__tests__/index.tsx b/src/__tests__/index.tsx
index 446fb606..51277acf 100644
--- a/src/__tests__/index.tsx
+++ b/src/__tests__/index.tsx
@@ -1,10 +1,12 @@
-/* tslint:disable:no-expression-statement no-object-mutation */
+/* eslint-disable react-perf/jsx-no-new-function-as-prop */
import { render, cleanup, screen, act } from '@testing-library/react'
import userEventImport from '@testing-library/user-event'
import { createMemoryHistory } from 'history'
import React from 'react'
import { factoryParameters, pm, serializers, useBatchQuery } from '../index.js'
import Geschichte from '../lib/adapters/historyjs/index.js'
+import { InferNamespaceValues } from '../lib/store.js'
+
afterEach(cleanup)
const userEvent = userEventImport.default || userEventImport
@@ -23,7 +25,7 @@ describe('', () => {
const { useQuery: secondNamespaceUseQuery } = factoryParameters(
{
- someParameter: pm('wow', serializers.string),
+ other: pm('wow', serializers.string),
},
{ someParameter: 'test' },
'test2'
@@ -35,9 +37,14 @@ describe('', () => {
pushState,
resetPush,
} = useQuery()
- const { values: secondValues } = secondNamespaceUseQuery()
- // tslint:disable-next-line:readonly-keyword
- const { batchPushState } = useBatchQuery<{ someParameter: string }>()
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { values: secondValues } = secondNamespaceUseQuery() // we have to use the other query so it's registered
+
+ type FullStore = {
+ test: InferNamespaceValues
+ test2: InferNamespaceValues
+ }
+ const { batchPushState } = useBatchQuery()
return (
<>
{someParameter}
@@ -51,8 +58,12 @@ describe('', () => {
title="pushBatch"
onClick={() =>
batchPushState(['test', 'test2'], (stateFirst, stateSecond) => {
- stateFirst.someParameter = 'wasBatch'
- stateSecond.someParameter = 'anotherOne'
+ if (stateFirst && stateSecond) {
+ stateFirst.someParameter = 'wasBatch'
+ if ('other' in stateSecond) {
+ stateSecond.other = 'anotherOne'
+ }
+ }
})
}
/>
diff --git a/src/__tests__/skip.ts b/src/__tests__/skip.ts
index b982d671..b4700943 100644
--- a/src/__tests__/skip.ts
+++ b/src/__tests__/skip.ts
@@ -1,10 +1,8 @@
-/* tslint:disable:no-expression-statement no-object-mutation */
-
import { factoryParameters } from '../lib/store.js'
import { defaultSkipValue, pm } from '../lib/utils.js'
import { Serializer, serializers } from '../lib/serializers.js'
-export const nullableBooleanSerializer: Serializer = {
+export const nullableBooleanSerializer: Serializer = {
deserialize: (value: string | null) => {
if (value === '1') {
return true
diff --git a/src/__tests__/static-render.tsx b/src/__tests__/static-render.tsx
index be625580..405d1316 100644
--- a/src/__tests__/static-render.tsx
+++ b/src/__tests__/static-render.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-perf/jsx-no-new-function-as-prop */
/* tslint:disable:no-expression-statement no-object-mutation */
import { render, cleanup, screen, act } from '@testing-library/react'
diff --git a/src/__tests__/static.ts b/src/__tests__/static.ts
index adb362c2..92212621 100644
--- a/src/__tests__/static.ts
+++ b/src/__tests__/static.ts
@@ -1,9 +1,29 @@
-/* tslint:disable:no-expression-statement no-object-mutation */
-
import { factoryParameters } from '../lib/store.js'
import { pm } from '../lib/utils.js'
import { serializers } from '../lib/serializers.js'
+describe('deep partial createQueryString', () => {
+ const initialValues = {
+ some: {
+ parameter: 'hello',
+ otherValue: 'world',
+ },
+ }
+ const { createQueryString } = factoryParameters(
+ {
+ some: {
+ parameter: pm('foo', serializers.string),
+ otherValue: pm('bar', serializers.string),
+ },
+ },
+ initialValues
+ )
+
+ it('should allow deep partial values', () => {
+ expect(createQueryString({ some: { parameter: 'wow' } })).toEqual('foo=wow')
+ })
+})
+
describe('static parseQueryString', () => {
const initialValues = { someParameter: 'test' }
const { parseQueryString } = factoryParameters(
diff --git a/src/examples/defaults.tsx b/src/examples/defaults.tsx
index 8a61e9b6..4ffbbf76 100644
--- a/src/examples/defaults.tsx
+++ b/src/examples/defaults.tsx
@@ -10,7 +10,7 @@ const defaultValues = () => ({
someParameter: 'test',
})
-interface Props {
+interface Props> {
readonly defaultValues: T
}
diff --git a/src/examples/index.tsx b/src/examples/index.tsx
index ebdb6e3e..d42cad86 100644
--- a/src/examples/index.tsx
+++ b/src/examples/index.tsx
@@ -1,4 +1,4 @@
-/* tslint:disable:no-expression-statement no-object-mutation */
+/* eslint-disable react-perf/jsx-no-new-function-as-prop */
import { createBrowserHistory } from 'history'
import React, { useCallback, useState } from 'react'
import { factoryParameters, pm, serializers } from '../index.js'
diff --git a/src/index.tsx b/src/index.tsx
index 6aa26ff3..589fe4ae 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -9,13 +9,14 @@ export {
export { type StoreState } from './lib/middleware.js'
export {
type HistoryManagement,
- useGeschichte,
+ createGeschichte,
factoryParameters,
useBatchQuery,
useStore,
DEFAULT_NAMESPACE,
StoreContext,
type Config,
+ type InferNamespaceValues,
} from './lib/store.js'
export {
pm,
diff --git a/src/lib/__tests__/serializers.ts b/src/lib/__tests__/serializers.ts
index eb14ee9d..d0934539 100644
--- a/src/lib/__tests__/serializers.ts
+++ b/src/lib/__tests__/serializers.ts
@@ -1,4 +1,3 @@
-/* tslint:disable:no-expression-statement */
import { serializers } from '../serializers.js'
describe('serializers', () => {
diff --git a/src/lib/__tests__/utils.ts b/src/lib/__tests__/utils.ts
index 94cf8b38..7039f00d 100644
--- a/src/lib/__tests__/utils.ts
+++ b/src/lib/__tests__/utils.ts
@@ -1,4 +1,3 @@
-/* tslint:disable:no-expression-statement */
import { Patch } from 'immer'
import { serializers } from '../serializers.js'
import { DEFAULT_NAMESPACE } from '../store.js'
diff --git a/src/lib/adapters/historyjs/index.tsx b/src/lib/adapters/historyjs/index.tsx
index 58ab4ee9..a76e21bf 100644
--- a/src/lib/adapters/historyjs/index.tsx
+++ b/src/lib/adapters/historyjs/index.tsx
@@ -1,4 +1,3 @@
-/* tslint:disable:no-expression-statement readonly-array */
import type { Action, History, Location } from 'history'
import React, {
forwardRef,
@@ -7,10 +6,13 @@ import React, {
useImperativeHandle,
useMemo,
} from 'react'
-// tslint:disable-next-line:no-submodule-imports
import { shallow } from 'zustand/shallow'
import { StoreState } from '../../middleware.js'
-import { HistoryManagement, StoreContext, useGeschichte } from '../../store.js'
+import {
+ HistoryManagement,
+ StoreContext,
+ createGeschichte,
+} from '../../store.js'
import { createSearch } from '../../utils.js'
export interface Props {
@@ -43,7 +45,6 @@ const handleHistoryV5 = ({
}
}
-// tslint:disable-next-line
let handler: typeof handleHistoryV4 | typeof handleHistoryV5
export const handleHistoryEvent = (action?: Action) => {
@@ -82,11 +83,14 @@ export const GeschichteWithHistory = forwardRef(
}, [history])
const value = useMemo(
- () => useGeschichte(historyInstance),
+ () => createGeschichte(historyInstance),
[historyInstance]
)
const state = value(
- ({ unregister, updateFromQuery }: StoreState) => ({
+ ({
+ unregister,
+ updateFromQuery,
+ }: StoreState>) => ({
unregister,
updateFromQuery,
}),
@@ -94,6 +98,7 @@ export const GeschichteWithHistory = forwardRef(
)
useEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return history.listen((update, maybeAction) => {
const { location, action } = (
@@ -103,6 +108,7 @@ export const GeschichteWithHistory = forwardRef(
if (
(action === 'REPLACE' || action === 'PUSH') &&
location.state &&
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
location.state.__g__
) {
@@ -110,6 +116,7 @@ export const GeschichteWithHistory = forwardRef(
}
state.updateFromQuery(location.search)
})
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [history, state.updateFromQuery])
useImperativeHandle(
diff --git a/src/lib/adapters/nextjs-app-router/index.tsx b/src/lib/adapters/nextjs-app-router/index.tsx
index b5bb31d0..3d339ffd 100644
--- a/src/lib/adapters/nextjs-app-router/index.tsx
+++ b/src/lib/adapters/nextjs-app-router/index.tsx
@@ -1,9 +1,11 @@
-/* tslint:disable:no-expression-statement no-object-mutation no-submodule-imports */
-
'use client'
import { useSearchParams, useRouter, usePathname } from 'next/navigation.js'
-import { HistoryManagement, StoreContext, useGeschichte } from '../../store.js'
+import {
+ HistoryManagement,
+ StoreContext,
+ createGeschichte,
+} from '../../store.js'
import React, { memo, ReactNode, useEffect, useMemo, useRef } from 'react'
import { StoreState } from '../../middleware.js'
import { shallow } from 'zustand/shallow'
@@ -21,7 +23,6 @@ const GeschichteForNextAppRouter = ({ children }: Props) => {
const router = useRef({ push, replace, searchParams, pathname })
const historyInstance: HistoryManagement = useMemo(() => {
- // tslint:disable-next-line:no-shadowed-variable
const { searchParams, push, replace, pathname } = router.current
return {
initialSearch: () => searchParams,
@@ -35,13 +36,12 @@ const GeschichteForNextAppRouter = ({ children }: Props) => {
}, [])
const useStore = useMemo(
- () => useGeschichte(historyInstance),
+ () => createGeschichte(historyInstance),
[historyInstance]
)
const state = useStore(
- // tslint:disable-next-line:no-shadowed-variable
- ({ unregister, updateFromQuery }: StoreState