diff --git a/README.md b/README.md index 4db0087..78b2f07 100644 --- a/README.md +++ b/README.md @@ -167,9 +167,6 @@ function MyComponent() { // can pass `searchParams` from server components const { urlState, setUrl, setState } = useUrlState({ defaultState: userState }); - // won't let you to accidently mutate state directly, requires TS - // urlState.name = 'John' // <- error - return (
=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", @@ -25005,6 +25022,187 @@ "node": ">=6" } }, + "node_modules/pin-github-action": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/pin-github-action/-/pin-github-action-1.9.1.tgz", + "integrity": "sha512-ZwICGpJjnQKctwK+Wvq+hYT49PT2M9Psd9P+D4x9dBHuOAPP3AeNk68mvhkLtXhyahH4kjQkUMDohQ86NEvVnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/rest": "^18", + "commander": "^9", + "debug": "^4.3.4", + "matcher": "^4.0.0", + "yaml": "^2.1.3" + }, + "bin": { + "pin-github-action": "bin.js" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pin-github-action/node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "node_modules/pin-github-action/node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/pin-github-action/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/pin-github-action/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", diff --git a/package.json b/package.json index 5e28e9f..23a7b72 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "reinstall": "npm run cleanup && npm install", "setup": "playwright install --with-deps", "prepack": "npm run build && cp package.json dist/package.json && npm run build:demo", + "pin-gh-deps": "pin-github-action .github/workflows/tests.yml", "prepare": "npx husky" }, "wireit": { @@ -363,6 +364,7 @@ "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.9", "only-allow": "^1.2.1", + "pin-github-action": "^1.9.1", "playwright": "^1.48.2", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.6.8", diff --git a/packages/example-nextjs14/src/app/Form-for-test.tsx b/packages/example-nextjs14/src/app/Form-for-test.tsx index d7fdf46..c125df4 100644 --- a/packages/example-nextjs14/src/app/Form-for-test.tsx +++ b/packages/example-nextjs14/src/app/Form-for-test.tsx @@ -40,12 +40,6 @@ export const Form = ({ // Just to test ts types if (urlState?.tags?.length === 10) { - // @ts-expect-error should be readonly - urlState.age = 18; - // @ts-expect-error should be readonly - urlState.tags[0].value = { text: 'jjj', time: new Date() }; - // @ts-expect-error should be readonly - urlState.tags[0].value.text = 'jjj'; setState(urlState); setState((st) => st); setState((st) => ({ ...st, age: 18 })); diff --git a/packages/example-nextjs15/src/app/Form-for-test.tsx b/packages/example-nextjs15/src/app/Form-for-test.tsx index f332eca..e5f71e7 100644 --- a/packages/example-nextjs15/src/app/Form-for-test.tsx +++ b/packages/example-nextjs15/src/app/Form-for-test.tsx @@ -40,12 +40,6 @@ export const Form = ({ // Just to test ts types if (urlState?.tags?.length === 10) { - // @ts-expect-error should be readonly - urlState.age = 18; - // @ts-expect-error should be readonly - urlState.tags[0].value = { text: 'jjj', time: new Date() }; - // @ts-expect-error should be readonly - urlState.tags[0].value.text = 'jjj'; setState(urlState); setState((st) => st); setState((st) => ({ ...st, age: 18 })); diff --git a/packages/example-react-router6/src/Form-for-test.tsx b/packages/example-react-router6/src/Form-for-test.tsx index 14699f0..774e1bc 100644 --- a/packages/example-react-router6/src/Form-for-test.tsx +++ b/packages/example-react-router6/src/Form-for-test.tsx @@ -31,12 +31,6 @@ export const Form = ({ className }: { className?: string }) => { // Just to test ts types if (urlState?.tags?.length === 10) { - // @ts-expect-error should be readonly - urlState.age = 18; - // @ts-expect-error should be readonly - urlState.tags[0].value = { text: "jjj", time: new Date() }; - // @ts-expect-error should be readonly - urlState.tags[0].value.text = "jjj"; setState(urlState); setState((st) => st); setState((st) => ({ ...st, age: 18 })); diff --git a/packages/urlstate/next/useUrlState/README.md b/packages/urlstate/next/useUrlState/README.md index 6aac70f..ff06376 100644 --- a/packages/urlstate/next/useUrlState/README.md +++ b/packages/urlstate/next/useUrlState/README.md @@ -16,7 +16,8 @@ A custom React hook that manages state and synchronizes it with URL search param ### Returns: An object containing: -- `urlState: object` - The current state (readonly). + +- `urlState: object` - The current state. - `setState: Function` - Function to update the state without updating the URL. - `setUrl: Function` - Function to update both the state and the URL. diff --git a/packages/urlstate/next/useUrlState/useUrlState.ts b/packages/urlstate/next/useUrlState/useUrlState.ts index 6bced38..66d7c79 100644 --- a/packages/urlstate/next/useUrlState/useUrlState.ts +++ b/packages/urlstate/next/useUrlState/useUrlState.ts @@ -5,7 +5,6 @@ import React from "react"; import { parseSPObj } from "../../parseSPObj"; import { useUrlStateBase } from "../../useUrlStateBase"; import { - type DeepReadonly, filterUnknownParams, filterUnknownParamsClient, isSSR, @@ -115,11 +114,14 @@ export function useUrlState({ * @deprecated use `setUrl` */ updateUrl, - urlState: state as DeepReadonly, + /** + * State object. Don't mutate directly, use `setState` or `setUrl` + */ + urlState: state, /** * @deprecated use `urlState` */ - state: state as DeepReadonly, + state, getState, }; } diff --git a/packages/urlstate/react-router/useUrlState/README.md b/packages/urlstate/react-router/useUrlState/README.md index 03fdbf2..363f664 100644 --- a/packages/urlstate/react-router/useUrlState/README.md +++ b/packages/urlstate/react-router/useUrlState/README.md @@ -16,7 +16,8 @@ A custom React hook that manages state and synchronizes it with URL search param ### Returns: An object containing: -- `urlState: object` - The current state (readonly). + +- `urlState: object` - The current state. - `setState: Function` - Function to update the state without updating the URL. - `setUrl: Function` - Function to update both the state and the URL. diff --git a/packages/urlstate/react-router/useUrlState/useUrlState.ts b/packages/urlstate/react-router/useUrlState/useUrlState.ts index ff49925..49a07c7 100644 --- a/packages/urlstate/react-router/useUrlState/useUrlState.ts +++ b/packages/urlstate/react-router/useUrlState/useUrlState.ts @@ -9,7 +9,6 @@ import { parseSPObj } from "../../parseSPObj"; import { useUrlStateBase } from "../../useUrlStateBase"; import { assignValue, - type DeepReadonly, filterUnknownParams, filterUnknownParamsClient, type JSONCompatible, @@ -121,11 +120,14 @@ export function useUrlState({ * @deprecated use `setUrl` */ updateUrl, - urlState: state as DeepReadonly, + /** + * State object. Don't mutate directly, use `setState` or `setUrl` + */ + urlState: state, /** * @deprecated use `urlState` */ - state: state as DeepReadonly, + state, getState, }; } diff --git a/packages/urlstate/useSharedState/README.md b/packages/urlstate/useSharedState/README.md index 88f2ce5..76580bd 100644 --- a/packages/urlstate/useSharedState/README.md +++ b/packages/urlstate/useSharedState/README.md @@ -14,7 +14,8 @@ A custom React hook that manages shared state across components. ### Returns: An object containing: -- `state: T` - The current state (readonly). + +- `state: T` - The current state. - `getState: () => T` - Function to get the current state. - `setState: T | Partial | (T) => void` - Function to update the state. @@ -42,7 +43,7 @@ Updates the shared state. ### Parameters: -- `value: T | DeepReadonly | ((currState: T) => T)` - New state value or a function that receives the current state and returns the new state. +- `value: T | ((currState: T) => T)` - New state value or a function that receives the current state and returns the new state. ## `getState` diff --git a/packages/urlstate/useSharedState/useSharedState.ts b/packages/urlstate/useSharedState/useSharedState.ts index 62bea33..89ef9d1 100644 --- a/packages/urlstate/useSharedState/useSharedState.ts +++ b/packages/urlstate/useSharedState/useSharedState.ts @@ -2,12 +2,7 @@ import React from "react"; import { stateMap, subscribers } from "../subscribers"; import { useInsertionEffect } from "../useInsertionEffect"; -import { - type DeepReadonly, - isEqual, - isSSR, - type JSONCompatible, -} from "../utils"; +import { isEqual, isSSR, type JSONCompatible } from "../utils"; /** * Custom React hook for sharing state between unrelated components. @@ -51,7 +46,7 @@ export function useSharedState( const setState = React.useCallback( ( value: - | (Partial | Partial>) + | Partial | ((currState: typeof stateShape.current) => typeof stateShape.current), ): void => { const curr = stateMap.get(stateShape.current); diff --git a/packages/urlstate/useUrlStateBase/README.md b/packages/urlstate/useUrlStateBase/README.md index ba973c1..ef85a31 100644 --- a/packages/urlstate/useUrlStateBase/README.md +++ b/packages/urlstate/useUrlStateBase/README.md @@ -15,7 +15,8 @@ A custom React hook to create custom `useUrlState` hooks. ### Returns: An object containing: -- `state: object` - The current state (readonly). + +- `state: object` - The current state. - `getState: Function` - Function to get state. - `updateState: Function` - Function to update the state without updating the URL. - `updateUrl: Function` - Function to update both the state and the URL. diff --git a/packages/urlstate/useUrlStateBase/useUrlStateBase.ts b/packages/urlstate/useUrlStateBase/useUrlStateBase.ts index 76c9391..99b95dc 100644 --- a/packages/urlstate/useUrlStateBase/useUrlStateBase.ts +++ b/packages/urlstate/useUrlStateBase/useUrlStateBase.ts @@ -4,7 +4,6 @@ import { useInsertionEffect } from "../useInsertionEffect"; import { useSharedState } from "../useSharedState"; import { useUrlEncode } from "../useUrlEncode"; import { - type DeepReadonly, filterUnknownParamsClient, type JSONCompatible, type Router, @@ -110,7 +109,7 @@ export function useUrlStateBase( return { updateState: setState, updateUrl, - state: state as DeepReadonly, + state, getState, }; } diff --git a/packages/urlstate/utils.ts b/packages/urlstate/utils.ts index 1e7887f..8698c1b 100644 --- a/packages/urlstate/utils.ts +++ b/packages/urlstate/utils.ts @@ -49,16 +49,6 @@ export type JSONCompatible = { [prop: string]: JSON | JSON[]; }; -// Always will be some compromise between how strict checks are and readability -export type DeepReadonly = - T extends Map - ? ReadonlyMap, DeepReadonly> - : T extends Set - ? ReadonlySet> - : T extends object - ? { readonly [K in keyof T]: DeepReadonly } - : T; - export const getParams = (strOrSearchParams?: string | URLSearchParams) => new URLSearchParams( typeof strOrSearchParams === "string"