From 0bfc89bca647de4056acbf0edcce84818639fc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Xalambr=C3=AD?= Date: Fri, 6 Oct 2023 12:57:57 -0500 Subject: [PATCH 1/5] Fix import paths on docs --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 58276ce..35c873e 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ First create a new CSRF instance. ```ts // app/utils/csrf.server.ts -import { CSRF } from "remix-utils/csrf"; +import { CSRF } from "remix-utils/csrf/server"; import { createCookie } from "@remix-run/node"; // or cloudflare/deno export const cookie = createCookie("csrf", { @@ -470,7 +470,7 @@ export async function loader({ request }: LoaderArgs) { Now that you returned the token and set it in a cookie, you can use the `AuthenticityTokenProvider` component to provide the token to your React components. ```tsx -import { AuthenticityTokenProvider } from "remix-utils/authenticity-token"; +import { AuthenticityTokenProvider } from "remix-utils/csrf/react"; let { csrf } = useLoaderData(); return ( @@ -486,7 +486,7 @@ When you create a form in some route, you can use the `AuthenticityTokenInput` c ```tsx import { Form } from "@remix-run/react"; -import { AuthenticityTokenInput } from "remix-utils/authenticity-token"; +import { AuthenticityTokenInput } from "remix-utils/csrf/react"; export default function Component() { return ( @@ -512,7 +512,7 @@ If you need to use `useFetcher` (or `useSubmit`) instead of `Form` you can also ```tsx import { useFetcher } from "@remix-run/react"; -import { useAuthenticityToken } from "remix-utils/authenticity-token"; +import { useAuthenticityToken } from "remix-utils/csrf/react"; export function useMarkAsRead() { let fetcher = useFetcher(); @@ -529,7 +529,7 @@ export function useMarkAsRead() { Finally, you need to validate the authenticity token in the action that received the request. ```ts -import { CSRFError } from "remix-utils/csrf"; +import { CSRFError } from "remix-utils/csrf/server"; import { redirectBack } from "remix-utils/redirect-back"; import { csrf } from "~/utils/csrf.server"; @@ -719,7 +719,7 @@ return ( This hook allows you to read the value of `transition.state`, every `fetcher.state` in the app, and `revalidator.state`. ```ts -import { useGlobalNavigationState } from "remix-utils/use-global-navigation-state"; +import { useGlobalNavigationState } from "remix-utils/use-global-pending-state"; export function GlobalPendingUI() { let states = useGlobalNavigationState(); @@ -747,7 +747,7 @@ The return value of `useGlobalNavigationState` can be `"idle"`, `"loading"` or ` This hook lets you know if the global navigation, if one of any active fetchers is either loading or submitting, or if the revalidator is running. ```ts -import { useGlobalPendingState } from "remix-utils/use-global-navigation-state"; +import { useGlobalPendingState } from "remix-utils/use-global-pending-state"; export function GlobalPendingUI() { let globalState = useGlobalPendingState(); @@ -770,7 +770,7 @@ The return value of `useGlobalPendingState` is either `"idle"` or `"pending"`. This hook lets you know if the global transition or if one of any active fetchers is submitting. ```ts -import { useGlobalSubmittingState } from "remix-utils/use-global-navigation-state"; +import { useGlobalSubmittingState } from "remix-utils/use-global-pending-state"; export function GlobalPendingUI() { let globalState = useGlobalSubmittingState(); @@ -789,7 +789,7 @@ The return value of `useGlobalSubmittingState` is either `"idle"` or `"submittin This hook lets you know if the global transition, if one of any active fetchers is loading, or if the revalidator is running ```ts -import { useGlobalLoadingState } from "remix-utils/use-global-navigation-state"; +import { useGlobalLoadingState } from "remix-utils/use-global-pending-state"; export function GlobalPendingUI() { let globalState = useGlobalLoadingState(); @@ -1302,7 +1302,7 @@ The `eventStream` function is used to create a new event stream response needed ```ts // app/routes/sse.time.ts -import { eventStream } from "remix-utils/event-stream"; +import { eventStream } from "remix-utils/sse/server"; export async function loader({ request }: LoaderArgs) { return eventStream(request.signal, function setup(send) { @@ -1321,7 +1321,7 @@ Then, inside any component, you can use the `useEventSource` hook to connect to ```tsx // app/components/counter.ts -import { useEventSource } from "remix-utils/use-event-source"; +import { useEventSource } from "remix-utils/sse/react"; function Counter() { // Here `/sse/time` is the resource route returning an eventStream response @@ -1509,6 +1509,8 @@ You can do this with the functions `preloadRouteAssets`, `preloadLinkedAssets` a All functions follows the same signature: ```ts +import { preloadRouteAssets, preloadLinkedAssets, preloadModuleAssets } from "remix-utils/preload-route-assets"; + // entry.server.tsx export default function handleRequest( request: Request, @@ -1551,6 +1553,8 @@ To help you prevent this Remix Utils gives you a `safeRedirect` function which c > **Note**: In this context, safe means the URL starts with `/` but not `//`, this means the URL is a pathname inside the same app and not an external link. ```ts +import { safeRedirect } from "remix-utils/safe-redirect"; + export async function loader({ request }: LoaderArgs) { let { searchParams } = new URL(request.url); let redirectTo = searchParams.get("redirectTo"); @@ -1582,6 +1586,8 @@ export async function loader({ params }: LoaderData) { The `jsonHash` function lets you define those functions directly in the `json`, reducing the need to create extra functions and variables. ```ts +import { jsonHash } from "remix-utils/json-hash"; + export async function loader({ params }: LoaderData) { let postId = z.string().parse(params.postId); return jsonHash({ @@ -1600,6 +1606,8 @@ It also calls your functions using `Promise.all` so you can be sure the data is Additionally, you can pass non-async functions, values and promises. ```ts +import { jsonHash } from "remix-utils/json-hash"; + export async function loader({ params }: LoaderData) { let postId = z.string().parse(params.postId); return jsonHash({ @@ -1834,7 +1842,7 @@ There's a pair of utils in Remix Utils to help you implement this. First, create a `honeypot.server.ts` where you will instantiate and configure your Honeypot. ```tsx -import { Honeypot } from "remix-utils/honeypot"; +import { Honeypot } from "remix-utils/honeypot/server"; // Create a new Honeypot instance, the values here are the defaults, you can // customize them @@ -1861,7 +1869,7 @@ export async function loader() { And in the `app/root` component render the `HoneypotProvider` component wrapping the rest of the UI. ```tsx -import { HoneypotProvider } from "remix-utils/honeypot-inputs"; +import { HoneypotProvider } from "remix-utils/honeypot/react"; export default function Component() { // more code here @@ -1895,7 +1903,7 @@ function SomePublicForm() { Finally, in the action the form submits to, you can call `honeypot.check`. ```ts -import { SpamError } from "remix-utils/honeypot"; +import { SpamError } from "remix-utils/honeypot/server"; import { honeypot } from "~/honeypot.server"; export async function action({ request }) { From 29a47103d4e3671925be0c93e5715dd8aa889852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Xalambr=C3=AD?= Date: Fri, 6 Oct 2023 12:58:06 -0500 Subject: [PATCH 2/5] Add missing IP header --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 35c873e..0ffd432 100644 --- a/README.md +++ b/README.md @@ -939,6 +939,7 @@ The function uses the following list of headers, in order of preference: - X-Client-IP - X-Forwarded-For +- HTTP-X-Forwarded-For - Fly-Client-IP - CF-Connecting-IP - Fastly-Client-Ip From 6791644c663cc6d9f67514e3f337da1d83bef58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Xalambr=C3=AD?= Date: Fri, 6 Oct 2023 13:06:42 -0500 Subject: [PATCH 3/5] Add note about IP address being null locally --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0ffd432..16d19d6 100644 --- a/README.md +++ b/README.md @@ -954,6 +954,9 @@ The function uses the following list of headers, in order of preference: When a header is found that contains a valid IP address, it will return without checking the other headers. +> **Note** +> On local development the function is most likely to return `null`. This is because the browser doesn't send any of the above headers, if you want to simulate those headers you will need to either add it to the request Remix receives in your HTTP server or run a reverse proxy like NGINX that can add them for you. + ### getClientLocales > **Note**: This depends on `intl-parse-accept-language`. From 305d751c0117e13de86ea424689be6c488908e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Xalambr=C3=AD?= Date: Fri, 6 Oct 2023 13:06:54 -0500 Subject: [PATCH 4/5] Use a better example for the async function case in jsonHash --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16d19d6..f961fb3 100644 --- a/README.md +++ b/README.md @@ -1623,7 +1623,7 @@ export async function loader({ params }: LoaderData) { }, async post() { // Async function - // Implement me + return await getPost(postId); }, }); From 7f6de70c0ee23e885ce266f68cc9839594ad2972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Xalambr=C3=AD?= Date: Fri, 6 Oct 2023 13:07:20 -0500 Subject: [PATCH 5/5] Pass Prettier on package.json and fix use-debounce-fetcher export --- package.json | 348 +++++++++++++++++++++++++-------------------------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/package.json b/package.json index c9319bd..724463b 100644 --- a/package.json +++ b/package.json @@ -1,176 +1,176 @@ { - "name": "remix-utils", - "version": "7.0.1", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "type": "module", - "exports": { - "./package.json": "./package.json", - "./promise": "./build/common/promise.js", - "./cache-assets": "./build/client/cache-assets.js", - "./cors": "./build/server/cors.js", - "./get-client-ip-address": "./build/server/get-client-ip-address.js", - "./is-prefetch": "./build/server/is-prefetch.js", - "./json-hash": "./build/server/json-hash.js", - "./named-action": "./build/server/named-action.js", - "./parse-accept-header": "./build/server/parse-accept-header.js", - "./preload-route-assets": "./build/server/preload-route-assets.js", - "./redirect-back": "./build/server/redirect-back.js", - "./respond-to": "./build/server/respond-to.js", - "./responses": "./build/server/responses.js", - "./rolling-cookie": "./build/server/rolling-cookie.js", - "./safe-redirect": "./build/server/safe-redirect.js", - "./typed-cookie": "./build/server/typed-cookie.js", - "./typed-session": "./build/server/typed-session.js", - "./client-only": "./build/react/client-only.js", - "./external-scripts": "./build/react/external-scripts.js", - "./fetcher-type": "./build/react/fetcher-type.js", - "./server-only": "./build/react/server-only.js", - "./use-debounced-fetcher": "./build/react/use-debounced-fetcher.js", - "./use-delegated-anchors": "./build/react/use-delegated-anchors.js", - "./use-global-pending-state": "./build/react/use-global-pending-state.js", - "./use-hydrated": "./build/react/use-hydrated.js", - "./use-should-hydrate": "./build/react/use-should-hydrate.js", - "./sse/server": "./build/server/event-stream.js", - "./sse/react": "./build/react/use-event-source.js", - "./locales/server": "./build/server/get-client-locales.js", - "./locales/react": "./build/react/use-locales.js", - "./honeypot/server": "./build/server/honeypot.js", - "./honeypot/react": "./build/react/honeypot.js", - "./csrf/server": "./build/server/csrf.js", - "./csrf/react": "./build/react/authenticity-token.js" - }, - "sideEffects": false, - "scripts": { - "prepare": "npm run build", - "build": "tsc --project tsconfig.json --outDir ./build", - "postbuild": "prettier --write \"build/**/*.js\" \"build/**/*.d.ts\"", - "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"test/**/*.ts\" \"test/**/*.tsx\" \"*.md\"", - "typecheck": "tsc --project tsconfig.json --noEmit", - "lint": "eslint --ext .ts,.tsx src/", - "test": "vitest --run", - "test:watch": "vitest", - "test:coverage": "vitest --coverage" - }, - "author": { - "name": "Sergio Xalambrí", - "url": "https://sergiodxa.com", - "email": "hello@sergiodxa.com" - }, - "repository": { - "type": "git", - "url": "https://github.com/sergiodxa/remix-utils" - }, - "keywords": [ - "remix", - "remix.run", - "react", - "utils", - "request", - "response", - "csrf", - "redirect-back", - "client-only", - "hydrated", - "server-only", - "cors", - "rolling cookie", - "safe redirect", - "typed cookie", - "typed session", - "client IP address", - "client locale", - "json hash", - "prefetch", - "named action" - ], - "peerDependencies": { - "@remix-run/cloudflare": "^2.0.0", - "@remix-run/deno": "^2.0.0", - "@remix-run/node": "^2.0.0", - "@remix-run/react": "^2.0.0", - "@remix-run/router": "^1.7.2", - "crypto-js": "^4.1.1", - "intl-parse-accept-language": "^1.0.0", - "is-ip": "^5.0.1", - "react": "^18.0.0", - "zod": "^3.22.4" - }, - "peerDependenciesMeta": { - "@remix-run/cloudflare": { - "optional": true - }, - "@remix-run/deno": { - "optional": true - }, - "@remix-run/node": { - "optional": true - }, - "@remix-run/react": { - "optional": true - }, - "@remix-run/router": { - "optional": true - }, - "crypto-js": { - "optional": true - }, - "intl-parse-accept-language": { - "optional": true - }, - "is-ip": { - "optional": true - }, - "react": { - "optional": true - }, - "zod": { - "optional": true - } - }, - "devDependencies": { - "@remix-run/node": "^2.0.0", - "@remix-run/react": "^2.0.0", - "@remix-run/router": "^1.7.2", - "@remix-run/testing": "^2.0.0", - "@testing-library/jest-dom": "^6.1.3", - "@testing-library/react": "^14.0.0", - "@types/crypto-js": "^4.1.2", - "@types/react": "^18.2.25", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "@vitejs/plugin-react": "^4.1.0", - "@vitest/coverage-v8": "^0.34.6", - "crypto-js": "^4.1.1", - "eslint": "^8.12.0", - "eslint-config-prettier": "^9.0.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-cypress": "^2.15.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jest-dom": "^5.1.0", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-testing-library": "^6.0.2", - "eslint-plugin-unicorn": "^48.0.1", - "happy-dom": "^12.9.0", - "intl-parse-accept-language": "^1.0.0", - "is-ip": "5.0.1", - "msw": "^1.3.2", - "prettier": "^3.0.3", - "react": "^18.0.0", - "react-dom": "^18.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "vite": "^4.4.11", - "vitest": "^0.34.6", - "zod": "^3.22.4" - }, - "dependencies": { - "type-fest": "^4.3.3" - } + "name": "remix-utils", + "version": "7.0.1", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "type": "module", + "exports": { + "./package.json": "./package.json", + "./promise": "./build/common/promise.js", + "./cache-assets": "./build/client/cache-assets.js", + "./cors": "./build/server/cors.js", + "./get-client-ip-address": "./build/server/get-client-ip-address.js", + "./is-prefetch": "./build/server/is-prefetch.js", + "./json-hash": "./build/server/json-hash.js", + "./named-action": "./build/server/named-action.js", + "./parse-accept-header": "./build/server/parse-accept-header.js", + "./preload-route-assets": "./build/server/preload-route-assets.js", + "./redirect-back": "./build/server/redirect-back.js", + "./respond-to": "./build/server/respond-to.js", + "./responses": "./build/server/responses.js", + "./rolling-cookie": "./build/server/rolling-cookie.js", + "./safe-redirect": "./build/server/safe-redirect.js", + "./typed-cookie": "./build/server/typed-cookie.js", + "./typed-session": "./build/server/typed-session.js", + "./client-only": "./build/react/client-only.js", + "./external-scripts": "./build/react/external-scripts.js", + "./fetcher-type": "./build/react/fetcher-type.js", + "./server-only": "./build/react/server-only.js", + "./use-debounce-fetcher": "./build/react/use-debounce-fetcher.js", + "./use-delegated-anchors": "./build/react/use-delegated-anchors.js", + "./use-global-pending-state": "./build/react/use-global-pending-state.js", + "./use-hydrated": "./build/react/use-hydrated.js", + "./use-should-hydrate": "./build/react/use-should-hydrate.js", + "./sse/server": "./build/server/event-stream.js", + "./sse/react": "./build/react/use-event-source.js", + "./locales/server": "./build/server/get-client-locales.js", + "./locales/react": "./build/react/use-locales.js", + "./honeypot/server": "./build/server/honeypot.js", + "./honeypot/react": "./build/react/honeypot.js", + "./csrf/server": "./build/server/csrf.js", + "./csrf/react": "./build/react/authenticity-token.js" + }, + "sideEffects": false, + "scripts": { + "prepare": "npm run build", + "build": "tsc --project tsconfig.json --outDir ./build", + "postbuild": "prettier --write \"build/**/*.js\" \"build/**/*.d.ts\"", + "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"test/**/*.ts\" \"test/**/*.tsx\" \"*.md\"", + "typecheck": "tsc --project tsconfig.json --noEmit", + "lint": "eslint --ext .ts,.tsx src/", + "test": "vitest --run", + "test:watch": "vitest", + "test:coverage": "vitest --coverage" + }, + "author": { + "name": "Sergio Xalambrí", + "url": "https://sergiodxa.com", + "email": "hello@sergiodxa.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/sergiodxa/remix-utils" + }, + "keywords": [ + "remix", + "remix.run", + "react", + "utils", + "request", + "response", + "csrf", + "redirect-back", + "client-only", + "hydrated", + "server-only", + "cors", + "rolling cookie", + "safe redirect", + "typed cookie", + "typed session", + "client IP address", + "client locale", + "json hash", + "prefetch", + "named action" + ], + "peerDependencies": { + "@remix-run/cloudflare": "^2.0.0", + "@remix-run/deno": "^2.0.0", + "@remix-run/node": "^2.0.0", + "@remix-run/react": "^2.0.0", + "@remix-run/router": "^1.7.2", + "crypto-js": "^4.1.1", + "intl-parse-accept-language": "^1.0.0", + "is-ip": "^5.0.1", + "react": "^18.0.0", + "zod": "^3.22.4" + }, + "peerDependenciesMeta": { + "@remix-run/cloudflare": { + "optional": true + }, + "@remix-run/deno": { + "optional": true + }, + "@remix-run/node": { + "optional": true + }, + "@remix-run/react": { + "optional": true + }, + "@remix-run/router": { + "optional": true + }, + "crypto-js": { + "optional": true + }, + "intl-parse-accept-language": { + "optional": true + }, + "is-ip": { + "optional": true + }, + "react": { + "optional": true + }, + "zod": { + "optional": true + } + }, + "devDependencies": { + "@remix-run/node": "^2.0.0", + "@remix-run/react": "^2.0.0", + "@remix-run/router": "^1.7.2", + "@remix-run/testing": "^2.0.0", + "@testing-library/jest-dom": "^6.1.3", + "@testing-library/react": "^14.0.0", + "@types/crypto-js": "^4.1.2", + "@types/react": "^18.2.25", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "@vitejs/plugin-react": "^4.1.0", + "@vitest/coverage-v8": "^0.34.6", + "crypto-js": "^4.1.1", + "eslint": "^8.12.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-cypress": "^2.15.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jest-dom": "^5.1.0", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^6.0.2", + "eslint-plugin-unicorn": "^48.0.1", + "happy-dom": "^12.9.0", + "intl-parse-accept-language": "^1.0.0", + "is-ip": "5.0.1", + "msw": "^1.3.2", + "prettier": "^3.0.3", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.2.2", + "vite": "^4.4.11", + "vitest": "^0.34.6", + "zod": "^3.22.4" + }, + "dependencies": { + "type-fest": "^4.3.3" + } }