Socket
Socket
Sign inDemoInstall

remix-utils

Package Overview
Dependencies
75
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 7.0.0 to 7.0.1

.eslintrc.cjs

15

build/common/promise.d.ts

@@ -6,3 +6,3 @@ /**

export type AwaitedPromiseHash<Hash extends PromiseHash> = {
[Key in keyof Hash]: Awaited<Hash[Key]>;
[Key in keyof Hash]: Awaited<Hash[Key]>;
};

@@ -37,5 +37,3 @@ /**

*/
export declare function promiseHash<Hash extends PromiseHash>(
hash: Hash,
): Promise<AwaitedPromiseHash<Hash>>;
export declare function promiseHash<Hash extends PromiseHash>(hash: Hash): Promise<AwaitedPromiseHash<Hash>>;
/**

@@ -74,9 +72,6 @@ * Attach a timeout to any promise, if the timeout resolves first ignore the

*/
export declare function timeout<Value>(
promise: Promise<Value>,
options: {
export declare function timeout<Value>(promise: Promise<Value>, options: {
controller?: AbortController;
ms: number;
},
): Promise<Value>;
}): Promise<Value>;
/**

@@ -94,3 +89,3 @@ * An error thrown when a timeout occurs

export declare class TimeoutError extends Error {
constructor(message: string);
constructor(message: string);
}

@@ -30,7 +30,3 @@ /**

export async function promiseHash(hash) {
return Object.fromEntries(
await Promise.all(
Object.entries(hash).map(async ([key, promise]) => [key, await promise]),
),
);
return Object.fromEntries(await Promise.all(Object.entries(hash).map(async ([key, promise]) => [key, await promise])));
}

@@ -76,22 +72,26 @@ /**

export function timeout(promise, options) {
return new Promise(async (resolve, reject) => {
let timer = null;
try {
let result = await Promise.race([
promise,
new Promise((resolve) => {
timer = setTimeout(() => resolve(TIMEOUT), options.ms);
}),
]);
if (timer) clearTimeout(timer);
if (result === TIMEOUT) {
if (options.controller) options.controller.abort();
return reject(new TimeoutError(`Timed out after ${options.ms}ms`));
}
return resolve(result);
} catch (error) {
if (timer) clearTimeout(timer);
reject(error);
}
});
return new Promise(async (resolve, reject) => {
let timer = null;
try {
let result = await Promise.race([
promise,
new Promise((resolve) => {
timer = setTimeout(() => resolve(TIMEOUT), options.ms);
}),
]);
if (timer)
clearTimeout(timer);
if (result === TIMEOUT) {
if (options.controller)
options.controller.abort();
return reject(new TimeoutError(`Timed out after ${options.ms}ms`));
}
return resolve(result);
}
catch (error) {
if (timer)
clearTimeout(timer);
reject(error);
}
});
}

@@ -110,6 +110,6 @@ /**

export class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = "TimeoutError";
}
constructor(message) {
super(message);
this.name = "TimeoutError";
}
}

@@ -1,10 +0,10 @@

import { ReactNode } from "react";
import * as React from "react";
type Props = {
/**
* You are encouraged to add a fallback that is the same dimensions
* as the client rendered children. This will avoid content layout
* shift which is disgusting
*/
children(): ReactNode;
fallback?: ReactNode;
/**
* You are encouraged to add a fallback that is the same dimensions
* as the client rendered children. This will avoid content layout
* shift which is disgusting
*/
children(): React.ReactNode;
fallback?: React.ReactNode;
};

@@ -26,3 +26,3 @@ /**

*/
export declare function ClientOnly({ children, fallback }: Props): JSX.Element;
export declare function ClientOnly({ children, fallback }: Props): React.JSX.Element;
export {};

@@ -1,3 +0,3 @@

import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { useHydrated } from "./use-hydrated";
import * as React from "react";
import { useHydrated } from "./use-hydrated.js";
/**

@@ -19,5 +19,3 @@ * Render the children only after the JS has loaded client-side. Use an optional

export function ClientOnly({ children, fallback = null }) {
return useHydrated()
? _jsx(_Fragment, { children: children() })
: _jsx(_Fragment, { children: fallback });
return useHydrated() ? React.createElement(React.Fragment, null, children()) : React.createElement(React.Fragment, null, fallback);
}

@@ -1,29 +0,91 @@

/// <reference types="react" />
import type { AppData } from "@remix-run/server-runtime";
import { HandleConventionArguments } from "./handle-conventions";
type ReferrerPolicy =
| "no-referrer-when-downgrade"
| "no-referrer"
| "origin-when-cross-origin"
| "origin"
| "same-origin"
| "strict-origin-when-cross-origin"
| "strict-origin"
| "unsafe-url";
type CrossOrigin = "anonymous" | "use-credentials";
type ScriptDescriptor = {
async?: boolean;
crossOrigin?: CrossOrigin;
defer?: boolean;
integrity?: string;
noModule?: boolean;
nonce?: string;
referrerPolicy?: ReferrerPolicy;
src: string;
type?: string;
import * as React from "react";
import { HandleConventionArguments } from "./handle-conventions.js";
export type ReferrerPolicy = "no-referrer-when-downgrade" | "no-referrer" | "origin-when-cross-origin" | "origin" | "same-origin" | "strict-origin-when-cross-origin" | "strict-origin" | "unsafe-url";
export type CrossOrigin = "anonymous" | "use-credentials";
export type ScriptType = "module" | "text/javascript";
export type ScriptDescriptor = {
/** Enable preloading of this script on SSR */
preload?: boolean;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
*/
async?: boolean;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin
*/
crossOrigin?: CrossOrigin;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer
*/
defer?: boolean;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity
*/
integrity?: string;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule
*/
noModule?: boolean;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce
*/
nonce?: string;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy
*/
referrerPolicy?: ReferrerPolicy;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src
*/
src: string;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type
*/
type?: ScriptType;
};
export interface ExternalScriptsFunction<Data extends AppData = AppData> {
(args: HandleConventionArguments<Data>): ScriptDescriptor[];
export interface ExternalScriptsFunction<Loader = unknown> {
(args: HandleConventionArguments<Loader>): ScriptDescriptor[];
}
export declare function ExternalScripts(): JSX.Element;
export {};
/**
* Define the shape of the `handle` export if you want to use `scripts`. Combine
* it with your own `handle` type to add `scripts` to your route.
* @description Add a scripts function that access the route's loader data
* @example
* export const handle: ExternalScriptsHandle<SerializeFrom<typeof loader>> = {
* scripts(loaderData) { ... }
* }
* @description Add a static scripts array
* @example
* export const handle: ExternalScriptsHandle = {
* scripts: [...]
* }
* @description Extend it with your own handle type
* @example
* interface Handle<Data = unknown> extends ExternalScriptsHandle<Data> {
* // extra things here
* }
* export const handle: Handle = {
* scripts, // define scripts here
* // and any other handle properties here
* }
*/
export interface ExternalScriptsHandle<Data = unknown> {
scripts?: ExternalScriptsFunction<Data> | ScriptDescriptor[];
}
/**
* Load scripts defined by each route in a single place, often in `root`.
* @example
* // Defines a `scripts` function in a route `handle`
* export const handle: ExternalScriptsHandle<SerializeFrom<typeof loader>> = {
* scripts(loaderData) { ... }
* }
* // Or define a scripts array directly
* export const handle: ExternalScriptsHandle = {
* scripts: [...]
* }
* // Then render ExternalScripts in your root
* return <ExternalScripts />
*/
export declare function ExternalScripts(): React.JSX.Element;
export declare function useExternalScripts(): any[];
export declare function ExternalScript({ src, preload, async, defer, crossOrigin, integrity, type, referrerPolicy, noModule, nonce, }: ScriptDescriptor): React.JSX.Element | null;

@@ -1,49 +0,106 @@

import { createElement as _createElement } from "react";
import {
jsx as _jsx,
Fragment as _Fragment,
jsxs as _jsxs,
} from "react/jsx-runtime";
import * as React from "react";
import { useLocation, useMatches } from "@remix-run/react";
import { useHydrated } from "./use-hydrated.js";
/**
* Load scripts defined by each route in a single place, often in `root`.
* @example
* // Defines a `scripts` function in a route `handle`
* export const handle: ExternalScriptsHandle<SerializeFrom<typeof loader>> = {
* scripts(loaderData) { ... }
* }
* // Or define a scripts array directly
* export const handle: ExternalScriptsHandle = {
* scripts: [...]
* }
* // Then render ExternalScripts in your root
* return <ExternalScripts />
*/
export function ExternalScripts() {
let location = useLocation();
let scripts = useMatches().flatMap((match, index, matches) => {
var _a;
let scripts =
(_a = match.handle) === null || _a === void 0 ? void 0 : _a.scripts;
if (typeof scripts !== "function") return [];
let result = scripts({
id: match.id,
data: match.data,
params: match.params,
location,
parentsData: matches.slice(0, index).map((match) => match.data),
matches,
});
if (Array.isArray(result)) return result;
return [];
});
return _jsxs(_Fragment, {
children: [
scripts.map((props) => {
let rel = props.noModule ? "modulepreload" : "preload";
let as = !props.noModule ? "script" : undefined;
return _jsx(
"link",
{
rel: rel,
href: props.src,
as: as,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
referrerPolicy: props.referrerPolicy,
},
props.src,
);
}),
scripts.map((props) => {
return _createElement("script", { ...props, key: props.src });
}),
],
});
let scripts = useExternalScripts();
return (React.createElement(React.Fragment, null, scripts.map((props) => {
return React.createElement(ExternalScript, { key: props.src, ...props });
})));
}
export function useExternalScripts() {
let location = useLocation();
let matches = useMatches();
return React.useMemo(() => {
let scripts = matches.flatMap((match, index, matches) => {
if (!match.handle)
return []; // ignore no-handle routes
if (match.handle === null)
return []; // ignore null handles
if (typeof match.handle !== "object")
return []; // and non error handles
if (!("scripts" in match.handle))
return []; // and without scripts
let scripts = match.handle.scripts;
// if scripts is an array, suppose it's an array of script descriptors
// and return it
if (Array.isArray(scripts))
return scripts;
// if it's not a function (and not an array), ignore it
if (typeof scripts !== "function")
return [];
let result = scripts({
id: match.id,
data: match.data,
params: match.params,
location,
parentsData: matches.slice(0, index).map((match) => match.data),
matches,
});
if (Array.isArray(result))
return result;
return [];
});
let uniqueScripts = new Map();
for (let script of scripts)
uniqueScripts.set(script.src, script);
return [...uniqueScripts.values()];
}, [matches, location]);
}
export function ExternalScript({ src, preload = false, async = true, defer = true, crossOrigin, integrity, type, referrerPolicy, noModule, nonce, }) {
let isHydrated = useHydrated();
let startsHydrated = React.useRef(isHydrated);
React.useEffect(() => {
if (!startsHydrated.current && isHydrated)
return;
let $script = document.createElement("script");
$script.src = src;
let attributes = {
async,
defer,
crossOrigin,
integrity,
type,
referrerPolicy,
noModule,
nonce,
};
for (let [key, value] of Object.entries(attributes)) {
if (value)
$script.setAttribute(key, value.toString());
}
document.body.append($script);
return () => $script.remove();
}, [
async,
crossOrigin,
defer,
integrity,
isHydrated,
noModule,
nonce,
referrerPolicy,
src,
type,
]);
if (startsHydrated.current && isHydrated)
return null;
let rel = noModule ? "modulepreload" : "preload";
let as = noModule ? undefined : "script";
return (React.createElement(React.Fragment, null,
preload && (React.createElement("link", { rel: rel, href: src, as: as, crossOrigin: crossOrigin, integrity: integrity, referrerPolicy: referrerPolicy })),
React.createElement("script", { src: src, defer: defer, async: async, type: type, noModule: noModule, nonce: nonce, crossOrigin: crossOrigin, integrity: integrity, referrerPolicy: referrerPolicy })));
}

@@ -6,10 +6,3 @@ import { type FetcherWithComponents } from "@remix-run/react";

*/
export type FetcherType =
| "init"
| "done"
| "actionSubmission"
| "actionReload"
| "actionRedirect"
| "loaderSubmission"
| "normalLoad";
export type FetcherType = "init" | "done" | "actionSubmission" | "actionReload" | "actionRedirect" | "loaderSubmission" | "normalLoad";
/**

@@ -25,5 +18,3 @@ * Derive the deprecated `fetcher.type` from the current state of a fetcher.

*/
export declare function useFetcherType(
fetcher: FetcherWithComponents<unknown>,
): FetcherType;
export declare function useFetcherType(fetcher: FetcherWithComponents<unknown>): FetcherType;
/**

@@ -42,8 +33,2 @@ * Derive the deprecated `fetcher.type` from the current state of a fetcher

*/
export declare function getFetcherType(
fetcher: Pick<
FetcherWithComponents<unknown>,
"state" | "data" | "formMethod"
>,
navigation: Pick<Navigation, "formMethod" | "state">,
): FetcherType;
export declare function getFetcherType(fetcher: Pick<FetcherWithComponents<unknown>, "state" | "data" | "formMethod">, navigation: Pick<Navigation, "formMethod" | "state">): FetcherType;

@@ -13,4 +13,4 @@ import { useNavigation } from "@remix-run/react";

export function useFetcherType(fetcher) {
let navigation = useNavigation();
return getFetcherType(fetcher, navigation);
let navigation = useNavigation();
return getFetcherType(fetcher, navigation);
}

@@ -31,27 +31,25 @@ /**

export function getFetcherType(fetcher, navigation) {
if (fetcher.state === "idle" && fetcher.data != null) return "done";
if (fetcher.state === "submitting") return "actionSubmission";
if (
fetcher.state === "loading" &&
fetcher.formMethod != null &&
navigation.formMethod !== "GET" &&
fetcher.data != null
) {
return "actionReload";
}
if (
fetcher.state === "loading" &&
fetcher.formMethod != null &&
navigation.formMethod !== "GET" &&
fetcher.data == null
) {
return "actionRedirect";
}
if (navigation.state === "loading" && navigation.formMethod === "GET") {
return "loaderSubmission";
}
if (navigation.state === "loading" && navigation.formMethod == null) {
return "normalLoad";
}
return "init";
if (fetcher.state === "idle" && fetcher.data != null)
return "done";
if (fetcher.state === "submitting")
return "actionSubmission";
if (fetcher.state === "loading" &&
fetcher.formMethod != null &&
navigation.formMethod !== "GET" &&
fetcher.data != null) {
return "actionReload";
}
if (fetcher.state === "loading" &&
fetcher.formMethod != null &&
navigation.formMethod !== "GET" &&
fetcher.data == null) {
return "actionRedirect";
}
if (navigation.state === "loading" && navigation.formMethod === "GET") {
return "loaderSubmission";
}
if (navigation.state === "loading" && navigation.formMethod == null) {
return "normalLoad";
}
return "init";
}
import type { RouterState } from "@remix-run/router";
import type { AppData } from "@remix-run/server-runtime";
import type { Location, Params } from "@remix-run/react";
import { Matches } from "./matches-type";
export type HandleConventionArguments<Data extends AppData = AppData> = {
id: string;
data: Data;
params: Params;
location: Location;
parentsData: RouterState["loaderData"];
matches: Matches;
import type { Location, Params, useMatches } from "@remix-run/react";
export type HandleConventionArguments<Data = unknown> = {
id: string;
data: Data;
params: Params;
matches: ReturnType<typeof useMatches>;
location: Location;
parentsData: RouterState["loaderData"];
};

@@ -1,10 +0,10 @@

import { ReactNode } from "react";
import * as React from "react";
type Props = {
/**
* You are encouraged to add a fallback that is the same dimensions
* as the server rendered children. This will avoid content layout
* shift which is disgusting
*/
children(): ReactNode;
fallback?: ReactNode;
/**
* You are encouraged to add a fallback that is the same dimensions
* as the server rendered children. This will avoid content layout
* shift which is disgusting
*/
children(): React.ReactNode;
fallback?: React.ReactNode;
};

@@ -24,3 +24,3 @@ /**

*/
export declare function ServerOnly({ children, fallback }: Props): JSX.Element;
export declare function ServerOnly({ children, fallback }: Props): React.JSX.Element;
export {};

@@ -1,3 +0,3 @@

import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { useHydrated } from "./use-hydrated";
import * as React from "react";
import { useHydrated } from "./use-hydrated.js";
/**

@@ -17,5 +17,3 @@ * Render the children only before the JS has loaded client-side. Use an

export function ServerOnly({ children, fallback = null }) {
return useHydrated()
? _jsx(_Fragment, { children: fallback })
: _jsx(_Fragment, { children: children() });
return useHydrated() ? React.createElement(React.Fragment, null, fallback) : React.createElement(React.Fragment, null, children());
}

@@ -1,12 +0,6 @@

import { ReactNode, RefObject } from "react";
export declare function isLinkEvent(
event: MouseEvent,
): HTMLAnchorElement | undefined;
export declare function useDelegatedAnchors(
nodeRef: RefObject<HTMLElement>,
): void;
export declare function PrefetchPageAnchors({
children,
}: {
children: ReactNode;
}): JSX.Element;
import * as React from "react";
export declare function isLinkEvent(event: MouseEvent): HTMLAnchorElement | undefined;
export declare function useDelegatedAnchors(nodeRef: React.RefObject<HTMLElement>): void;
export declare function PrefetchPageAnchors({ children, }: {
children: React.ReactNode;
}): React.JSX.Element;

@@ -1,74 +0,69 @@

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { PrefetchPageLinks, useNavigate } from "@remix-run/react";
import { createContext, useContext, useEffect, useRef, useState } from "react";
const context = createContext(false);
import * as React from "react";
const context = React.createContext(false);
export function isLinkEvent(event) {
if (!(event.target instanceof HTMLElement)) return;
let a = event.target.closest("a");
if (a && a.hasAttribute("href") && a.host === window.location.host) return a;
return;
if (!(event.target instanceof HTMLElement))
return;
let a = event.target.closest("a");
if (a && a.hasAttribute("href") && a.host === window.location.host)
return a;
return;
}
export function useDelegatedAnchors(nodeRef) {
let navigate = useNavigate();
let hasParentPrefetch = useContext(context);
useEffect(() => {
// if you call useDelegatedAnchors as a children of a PrefetchPageAnchors
// then do nothing
if (hasParentPrefetch) return;
let node = nodeRef.current;
node === null || node === void 0
? void 0
: node.addEventListener("click", handleClick);
return () =>
node === null || node === void 0
? void 0
: node.removeEventListener("click", handleClick);
function handleClick(event) {
if (!node) return;
let anchor = isLinkEvent(event);
if (!anchor) return;
if (event.button !== 0) return;
if (anchor.target && anchor.target !== "_self") return;
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
return;
}
if (anchor.hasAttribute("download")) return;
let { pathname, search, hash } = anchor;
navigate({ pathname, search, hash });
event.preventDefault();
}
}, [hasParentPrefetch, navigate, nodeRef]);
let navigate = useNavigate();
let hasParentPrefetch = React.useContext(context);
React.useEffect(() => {
// if you call useDelegatedAnchors as a children of a PrefetchPageAnchors
// then do nothing
if (hasParentPrefetch)
return;
let node = nodeRef.current;
node?.addEventListener("click", handleClick);
return () => node?.removeEventListener("click", handleClick);
function handleClick(event) {
if (!node)
return;
let anchor = isLinkEvent(event);
if (!anchor)
return;
if (event.button !== 0)
return;
if (anchor.target && anchor.target !== "_self")
return;
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
return;
}
if (anchor.hasAttribute("download"))
return;
let { pathname, search, hash } = anchor;
navigate({ pathname, search, hash });
event.preventDefault();
}
}, [hasParentPrefetch, navigate, nodeRef]);
}
export function PrefetchPageAnchors({ children }) {
let nodeRef = useRef(null);
let [page, setPage] = useState(null);
let hasParentPrefetch = useContext(context);
// prefetch is useless without delegated anchors, so we enable it
useDelegatedAnchors(nodeRef);
useEffect(() => {
if (hasParentPrefetch) return;
let node = nodeRef.current;
node === null || node === void 0
? void 0
: node.addEventListener("mouseenter", handleMouseEnter, true);
return () =>
node === null || node === void 0
? void 0
: node.removeEventListener("mouseenter", handleMouseEnter);
function handleMouseEnter(event) {
if (!nodeRef.current) return;
let anchor = isLinkEvent(event);
if (!anchor) return;
let { pathname, search } = anchor;
setPage(pathname + search);
}
}, [hasParentPrefetch]);
return _jsxs("div", {
ref: nodeRef,
style: { display: "contents" },
children: [
_jsx(context.Provider, { value: true, children: children }),
page && !hasParentPrefetch && _jsx(PrefetchPageLinks, { page: page }),
],
});
export function PrefetchPageAnchors({ children, }) {
let nodeRef = React.useRef(null);
let [page, setPage] = React.useState(null);
let hasParentPrefetch = React.useContext(context);
// prefetch is useless without delegated anchors, so we enable it
useDelegatedAnchors(nodeRef);
React.useEffect(() => {
if (hasParentPrefetch)
return;
let node = nodeRef.current;
node?.addEventListener("mouseenter", handleMouseEnter, true);
return () => node?.removeEventListener("mouseenter", handleMouseEnter);
function handleMouseEnter(event) {
if (!nodeRef.current)
return;
let anchor = isLinkEvent(event);
if (!anchor)
return;
let { pathname, search } = anchor;
setPage(pathname + search);
}
}, [hasParentPrefetch]);
return (React.createElement("div", { ref: nodeRef, style: { display: "contents" } },
React.createElement(context.Provider, { value: true }, children),
page && !hasParentPrefetch && React.createElement(PrefetchPageLinks, { page: page })));
}
/// <reference types="react" />
export interface EventSourceOptions {
init?: EventSourceInit;
event?: string;
init?: EventSourceInit;
event?: string;
}
export type EventSourceMap = Map<
string,
{
export type EventSourceMap = Map<string, {
count: number;
source: EventSource;
}
>;
}>;
export declare const EventSourceProvider: import("react").Provider<EventSourceMap>;

@@ -20,5 +17,2 @@ /**

*/
export declare function useEventSource(
url: string | URL,
{ event, init }?: EventSourceOptions,
): string | null;
export declare function useEventSource(url: string | URL, { event, init }?: EventSourceOptions): string | null;

@@ -11,36 +11,28 @@ import { useEffect, useState, createContext, useContext } from "react";

export function useEventSource(url, { event = "message", init } = {}) {
let map = useContext(context);
let [data, setData] = useState(null);
useEffect(() => {
var _a;
let key = [
url.toString(),
event,
init === null || init === void 0 ? void 0 : init.withCredentials,
].join("::");
let value =
(_a = map.get(key)) !== null && _a !== void 0
? _a
: {
let map = useContext(context);
let [data, setData] = useState(null);
useEffect(() => {
let key = [url.toString(), event, init?.withCredentials].join("::");
let value = map.get(key) ?? {
count: 0,
source: new EventSource(url, init),
};
++value.count;
map.set(key, value);
value.source.addEventListener(event, handler);
// rest data if dependencies change
setData(null);
function handler(event) {
setData(event.data || "UNKNOWN_EVENT_DATA");
}
return () => {
value.source.removeEventListener(event, handler);
--value.count;
if (value.count <= 0) {
value.source.close();
map.delete(key);
}
};
}, [url, event, init, map]);
return data;
};
++value.count;
map.set(key, value);
value.source.addEventListener(event, handler);
// rest data if dependencies change
setData(null);
function handler(event) {
setData(event.data || "UNKNOWN_EVENT_DATA");
}
return () => {
value.source.removeEventListener(event, handler);
--value.count;
if (value.count <= 0) {
value.source.close();
map.delete(key);
}
};
}, [url, event, init, map]);
return data;
}

@@ -21,8 +21,8 @@ import { useEffect, useState } from "react";

export function useHydrated() {
let [hydrated, setHydrated] = useState(() => !hydrating);
useEffect(function hydrate() {
hydrating = false;
setHydrated(true);
}, []);
return hydrated;
let [hydrated, setHydrated] = useState(() => !hydrating);
useEffect(function hydrate() {
hydrating = false;
setHydrated(true);
}, []);
return hydrated;
}

@@ -1,2 +0,2 @@

import type { Locales } from "../server/get-client-locales";
import type { Locales } from "../server/get-client-locales.js";
/**

@@ -3,0 +3,0 @@ * Get the locales returned by the loader of the root route.

@@ -25,27 +25,33 @@ import { useMatches } from "@remix-run/react";

export function useLocales() {
let matches = useMatches();
if (!matches) return undefined;
if (matches.length === 0) return undefined;
let [rootMatch] = matches;
// check if rootMatch exists and has data
if (!rootMatch) return undefined;
if (!rootMatch.data) return undefined;
let { data } = rootMatch;
// check if data is an object and has locales
if (typeof data !== "object") return undefined;
if (data === null) return undefined;
if (Array.isArray(data)) return undefined;
if (!("locales" in data)) return undefined;
let { locales } = data;
// check the type of value of locales
// it could be a string
// or it could be an array of strings
if (
Array.isArray(locales) &&
locales.every((value) => typeof value === "string")
) {
return locales;
}
// finally, return undefined
return undefined;
let matches = useMatches();
if (!matches)
return undefined;
if (matches.length === 0)
return undefined;
let [rootMatch] = matches;
// check if rootMatch exists and has data
if (!rootMatch)
return undefined;
if (!rootMatch.data)
return undefined;
let { data } = rootMatch;
// check if data is an object and has locales
if (typeof data !== "object")
return undefined;
if (data === null)
return undefined;
if (Array.isArray(data))
return undefined;
if (!("locales" in data))
return undefined;
let { locales } = data;
// check the type of value of locales
// it could be a string
// or it could be an array of strings
if (Array.isArray(locales) &&
locales.every((value) => typeof value === "string")) {
return locales;
}
// finally, return undefined
return undefined;
}

@@ -23,15 +23,23 @@ import { useMatches } from "@remix-run/react";

export function useShouldHydrate() {
return useMatches().some((match) => {
if (!match.handle) return false;
let { handle, data } = match;
// handle must be an object to continue
if (typeof handle !== "object") return false;
if (handle === null) return false;
if (Array.isArray(handle)) return false;
// get hydrate from handle (it may not exists)
let hydrate = handle.hydrate;
if (!hydrate) return false;
if (typeof hydrate === "function") return hydrate(data);
return hydrate;
});
return useMatches().some((match) => {
if (!match.handle)
return false;
let { handle, data } = match;
// handle must be an object to continue
if (typeof handle !== "object")
return false;
if (handle === null)
return false;
if (Array.isArray(handle))
return false;
if (!("hydrate" in handle))
return false;
// get hydrate from handle (it may not exists)
let hydrate = handle.hydrate;
if (!hydrate)
return false;
if (typeof hydrate === "function")
return hydrate(data);
return hydrate;
});
}

@@ -1,44 +0,44 @@

import { Promisable } from "type-fest";
import { type Promisable } from "type-fest";
type Origin = boolean | string | RegExp | Array<string | RegExp>;
interface CORSOptions {
/**
* Configures the **Access-Control-Allow-Origin** CORS header.
*
* Possible values:
* - true: Enable CORS for any origin (same as "*")
* - false: Don't setup CORS
* - string: Set to a specific origin, if set to "*" it will allow any origin
* - RegExp: Set to a RegExp to match against the origin
* - Array<string | RegExp>: Set to an array of origins to match against the
* string or RegExp
* - Function: Set to a function that will be called with the request origin
* and should return a boolean indicating if the origin is allowed or not.
* @default true
*/
origin?: Origin | ((origin: string) => Promisable<Origin>);
/**
* Configures the **Access-Control-Allow-Methods** CORS header.
* @default ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
*/
methods?: Array<string>;
/**
* Configures the **Access-Control-Allow-Headers** CORS header.
* @default []
*/
allowedHeaders?: string[];
/**
* Configures the **Access-Control-Expose-Headers** CORS header.
* @default []
*/
exposedHeaders?: string[];
/**
* Configures the **Access-Control-Allow-Credentials** CORS header.
* @default false
*/
credentials?: boolean;
/**
* Configures the **Access-Control-Max-Age** CORS header.
* @default 0
*/
maxAge?: number;
/**
* Configures the **Access-Control-Allow-Origin** CORS header.
*
* Possible values:
* - true: Enable CORS for any origin (same as "*")
* - false: Don't setup CORS
* - string: Set to a specific origin, if set to "*" it will allow any origin
* - RegExp: Set to a RegExp to match against the origin
* - Array<string | RegExp>: Set to an array of origins to match against the
* string or RegExp
* - Function: Set to a function that will be called with the request origin
* and should return a boolean indicating if the origin is allowed or not.
* @default true
*/
origin?: Origin | ((origin: string) => Promisable<Origin>);
/**
* Configures the **Access-Control-Allow-Methods** CORS header.
* @default ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
*/
methods?: Array<string>;
/**
* Configures the **Access-Control-Allow-Headers** CORS header.
* @default []
*/
allowedHeaders?: string[];
/**
* Configures the **Access-Control-Expose-Headers** CORS header.
* @default []
*/
exposedHeaders?: string[];
/**
* Configures the **Access-Control-Allow-Credentials** CORS header.
* @default false
*/
credentials?: boolean;
/**
* Configures the **Access-Control-Max-Age** CORS header.
* @default 0
*/
maxAge?: number;
}

@@ -100,7 +100,3 @@ /**

*/
export declare function cors(
request: Request,
response: Response,
options?: CORSOptions,
): Promise<Response>;
export declare function cors(request: Request, response: Response, options?: CORSOptions): Promise<Response>;
export {};
const DEFAULT_OPTIONS = {
origin: true,
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
allowedHeaders: [],
exposedHeaders: [],
origin: true,
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
allowedHeaders: [],
exposedHeaders: [],
};
class CORS {
constructor(options) {
// Merge user options with default options
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
}
async exec(request, response) {
let isPreflight = request.method.toLowerCase() === "options";
await this.configureOrigin(response.headers, request);
this.configureCredentials(response.headers);
this.configureExposedHeaders(response.headers);
if (isPreflight) {
this.configureMethods(response.headers);
this.configureAllowedHeaders(response.headers, request);
this.configureMaxAge(response.headers);
// waiting for the body
if (response.status === 204) response.headers.set("Content-Length", "0");
options;
constructor(options) {
// Merge user options with default options
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
}
return response;
}
async resolveOrigin(request) {
var _a;
let { origin } = this.options;
if (typeof origin === "function") {
return await origin(
(_a = request.headers.get("origin")) !== null && _a !== void 0
? _a
: "",
);
async exec(request, response) {
let isPreflight = request.method.toLowerCase() === "options";
await this.configureOrigin(response.headers, request);
this.configureCredentials(response.headers);
this.configureExposedHeaders(response.headers);
if (isPreflight) {
this.configureMethods(response.headers);
this.configureAllowedHeaders(response.headers, request);
this.configureMaxAge(response.headers);
// waiting for the body
if (response.status === 204)
response.headers.set("Content-Length", "0");
}
return response;
}
return origin;
}
configureMaxAge(headers) {
var { maxAge } = this.options;
if (!this.isNumber(maxAge)) return headers;
headers.append("Access-Control-Max-Age", maxAge.toString());
return headers;
}
configureExposedHeaders(headers) {
var _a;
let exposedHeaders =
(_a = this.options.exposedHeaders) === null || _a === void 0
? void 0
: _a.join(",");
if (!this.isString(exposedHeaders) || exposedHeaders === "") return headers;
headers.append("Access-Control-Expose-Headers", exposedHeaders);
return null;
}
configureAllowedHeaders(headers, request) {
var _a;
let allowedHeaders =
(_a = this.options.allowedHeaders) === null || _a === void 0
? void 0
: _a.join(",");
if (!allowedHeaders) {
// headers wasn't specified, so reflect the request headers
let requestHeaders = request.headers.get(
"Access-Control-Request-Headers",
);
if (this.isString(requestHeaders)) allowedHeaders = requestHeaders;
headers.append("Vary", "Access-Control-Request-Headers");
async resolveOrigin(request) {
let { origin } = this.options;
if (typeof origin === "function") {
return await origin(request.headers.get("origin") ?? "");
}
return origin;
}
if (allowedHeaders && allowedHeaders !== "") {
headers.append("Access-Control-Allow-Headers", allowedHeaders);
configureMaxAge(headers) {
var { maxAge } = this.options;
if (!this.isNumber(maxAge))
return headers;
headers.append("Access-Control-Max-Age", maxAge.toString());
return headers;
}
return headers;
}
configureCredentials(headers) {
if (this.options.credentials === true) {
headers.append("Access-Control-Allow-Credentials", "true");
configureExposedHeaders(headers) {
let exposedHeaders = this.options.exposedHeaders?.join(",");
if (!this.isString(exposedHeaders) || exposedHeaders === "")
return headers;
headers.append("Access-Control-Expose-Headers", exposedHeaders);
return null;
}
return headers;
}
configureMethods(headers) {
var _a;
let methods =
(_a = this.options.methods) === null || _a === void 0
? void 0
: _a.join(",");
if (!this.isString(methods)) return headers;
headers.append("Access-Control-Allow-Methods", methods);
return headers;
}
async configureOrigin(headers, request) {
let origin = await this.resolveOrigin(request);
let requestOrigin = request.headers.get("origin");
if (!requestOrigin || origin === false) return headers;
if (origin === undefined || origin === "*") {
// allow any origin
headers.append("Access-Control-Allow-Origin", "*");
return headers;
configureAllowedHeaders(headers, request) {
let allowedHeaders = this.options.allowedHeaders?.join(",");
if (!allowedHeaders) {
// headers wasn't specified, so reflect the request headers
let requestHeaders = request.headers.get("Access-Control-Request-Headers");
if (this.isString(requestHeaders))
allowedHeaders = requestHeaders;
headers.append("Vary", "Access-Control-Request-Headers");
}
if (allowedHeaders && allowedHeaders !== "") {
headers.append("Access-Control-Allow-Headers", allowedHeaders);
}
return headers;
}
if (this.isString(origin)) {
// fixed origin
headers.append("Access-Control-Allow-Origin", origin);
headers.append("Vary", "Origin");
return headers;
configureCredentials(headers) {
if (this.options.credentials === true) {
headers.append("Access-Control-Allow-Credentials", "true");
}
return headers;
}
if (!this.isOriginAllowed(requestOrigin, origin)) return headers;
// reflect origin
headers.append("Access-Control-Allow-Origin", requestOrigin);
headers.append("Vary", "Origin");
return headers;
}
isOriginAllowed(origin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (let element of allowedOrigin) {
if (this.isOriginAllowed(origin, element)) return true;
}
return false;
configureMethods(headers) {
let methods = this.options.methods?.join(",");
if (!this.isString(methods))
return headers;
headers.append("Access-Control-Allow-Methods", methods);
return headers;
}
if (this.isString(allowedOrigin)) {
return origin === allowedOrigin;
async configureOrigin(headers, request) {
let origin = await this.resolveOrigin(request);
let requestOrigin = request.headers.get("origin");
if (!requestOrigin || origin === false)
return headers;
if (origin === undefined || origin === "*") {
// allow any origin
headers.append("Access-Control-Allow-Origin", "*");
return headers;
}
if (this.isString(origin)) {
// fixed origin
headers.append("Access-Control-Allow-Origin", origin);
headers.append("Vary", "Origin");
return headers;
}
if (!this.isOriginAllowed(requestOrigin, origin))
return headers;
// reflect origin
headers.append("Access-Control-Allow-Origin", requestOrigin);
headers.append("Vary", "Origin");
return headers;
}
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
isOriginAllowed(origin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (let element of allowedOrigin) {
if (this.isOriginAllowed(origin, element))
return true;
}
return false;
}
if (this.isString(allowedOrigin)) {
return origin === allowedOrigin;
}
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
}
return !!allowedOrigin;
}
return !!allowedOrigin;
}
isString(value) {
return typeof value === "string" || value instanceof String;
}
isNumber(value) {
return typeof value === "number" || value instanceof Number;
}
isString(value) {
return typeof value === "string" || value instanceof String;
}
isNumber(value) {
return typeof value === "number" || value instanceof Number;
}
}

@@ -188,3 +178,3 @@ /**

export async function cors(request, response, options = DEFAULT_OPTIONS) {
return new CORS(options).exec(request, response);
return new CORS(options).exec(request, response);
}
import type { Cookie } from "@remix-run/server-runtime";
export type CSRFErrorCode =
| "missing_token_in_cookie"
| "invalid_token_in_cookie"
| "tampered_token_in_cookie"
| "missing_token_in_body"
| "mismatched_token";
export type CSRFErrorCode = "missing_token_in_cookie" | "invalid_token_in_cookie" | "tampered_token_in_cookie" | "missing_token_in_body" | "mismatched_token";
export declare class CSRFError extends Error {
code: CSRFErrorCode;
constructor(code: CSRFErrorCode, message: string);
code: CSRFErrorCode;
constructor(code: CSRFErrorCode, message: string);
}
interface CSRFOptions {
/**
* The cookie object to use for serializing and parsing the CSRF token.
*/
cookie: Cookie;
/**
* The name of the form data key to use for the CSRF token.
*/
formDataKey?: string;
/**
* A secret to use for signing the CSRF token.
*/
secret?: string;
/**
* The cookie object to use for serializing and parsing the CSRF token.
*/
cookie: Cookie;
/**
* The name of the form data key to use for the CSRF token.
*/
formDataKey?: string;
/**
* A secret to use for signing the CSRF token.
*/
secret?: string;
}
export declare class CSRF {
private cookie;
private formDataKey;
private secret?;
constructor(options: CSRFOptions);
/**
* Generates a random string in Base64URL to be used as an authenticity token
* for CSRF protection.
* @param bytes The number of bytes used to generate the token
* @returns A random string in Base64URL
*/
generate(bytes?: number): string;
/**
* Generates a token and serialize it into the cookie.
* @param bytes The number of bytes used to generate the token
* @returns A tuple with the token and the string to send in Set-Cookie
* @example
* let [token, cookie] = await csrf.commitToken();
* return json({ token }, {
* headers: { "set-cookie": cookie }
* })
*/
commitToken(bytes?: number): Promise<readonly [string, string]>;
/**
* Verify if a request and cookie has a valid CSRF token.
* @example
* export async function action({ request }: ActionArgs) {
* await csrf.validate(request);
* // the request is authenticated and you can do anything here
* }
* @example
* export async function action({ request }: ActionArgs) {
* let formData = await request.formData()
* await csrf.validate(formData, request.headers);
* // the request is authenticated and you can do anything here
* }
* @example
* export async function action({ request }: ActionArgs) {
* let formData = await parseMultipartFormData(request);
* await csrf.validate(formData, request.headers);
* // the request is authenticated and you can do anything here
* }
*/
validate(data: Request): Promise<void>;
validate(data: FormData, headers: Headers): Promise<void>;
private readBody;
private parseCookie;
private sign;
private verifySignature;
private cookie;
private formDataKey;
private secret?;
constructor(options: CSRFOptions);
/**
* Generates a random string in Base64URL to be used as an authenticity token
* for CSRF protection.
* @param bytes The number of bytes used to generate the token
* @returns A random string in Base64URL
*/
generate(bytes?: number): string;
/**
* Generates a token and serialize it into the cookie.
* @param requestOrHeaders A request or headers object from which we can
* get the cookie to get the existing token.
* @param bytes The number of bytes used to generate the token
* @returns A tuple with the token and the string to send in Set-Cookie
* If there's already a csrf value in the cookie then the token will
* be the same and the cookie will be null.
* @example
* let [token, cookie] = await csrf.commitToken(request);
* return json({ token }, {
* headers: { "set-cookie": cookie }
* })
*/
commitToken(requestOrHeaders?: Request | Headers, bytes?: number): Promise<readonly [string, string | null]>;
/**
* Verify if a request and cookie has a valid CSRF token.
* @example
* export async function action({ request }: ActionArgs) {
* await csrf.validate(request);
* // the request is authenticated and you can do anything here
* }
* @example
* export async function action({ request }: ActionArgs) {
* let formData = await request.formData()
* await csrf.validate(formData, request.headers);
* // the request is authenticated and you can do anything here
* }
* @example
* export async function action({ request }: ActionArgs) {
* let formData = await parseMultipartFormData(request);
* await csrf.validate(formData, request.headers);
* // the request is authenticated and you can do anything here
* }
*/
validate(data: Request): Promise<void>;
validate(data: FormData, headers: Headers): Promise<void>;
private readBody;
private parseCookie;
private sign;
private verifySignature;
}
export {};
import cryptoJS from "crypto-js";
import { getHeaders } from "./get-headers.js";
export class CSRFError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.name = "CSRFError";
}
code;
constructor(code, message) {
super(message);
this.code = code;
this.name = "CSRFError";
}
}
export class CSRF {
constructor(options) {
var _a;
this.formDataKey = "csrf";
this.cookie = options.cookie;
this.formDataKey =
(_a = options.formDataKey) !== null && _a !== void 0 ? _a : "csrf";
this.secret = options.secret;
}
/**
* Generates a random string in Base64URL to be used as an authenticity token
* for CSRF protection.
* @param bytes The number of bytes used to generate the token
* @returns A random string in Base64URL
*/
generate(bytes = 32) {
let token = cryptoJS.lib.WordArray.random(bytes).toString(
cryptoJS.enc.Base64url,
);
if (!this.secret) return token;
let signature = this.sign(token);
return [token, signature].join(".");
}
/**
* Generates a token and serialize it into the cookie.
* @param bytes The number of bytes used to generate the token
* @returns A tuple with the token and the string to send in Set-Cookie
* @example
* let [token, cookie] = await csrf.commitToken();
* return json({ token }, {
* headers: { "set-cookie": cookie }
* })
*/
async commitToken(bytes = 32) {
let token = this.generate(bytes);
let cookie = await this.cookie.serialize(token);
return [token, cookie];
}
async validate(data, headers) {
if (data instanceof Request && data.bodyUsed) {
throw new Error(
"The body of the request was read before calling CSRF#verify. Ensure you clone it before reading it.",
);
cookie;
formDataKey = "csrf";
secret;
constructor(options) {
this.cookie = options.cookie;
this.formDataKey = options.formDataKey ?? "csrf";
this.secret = options.secret;
}
let formData = await this.readBody(data);
let cookie = await this.parseCookie(data, headers);
// if the session doesn't have a csrf token, throw an error
if (cookie === null) {
throw new CSRFError(
"missing_token_in_cookie",
"Can't find CSRF token in cookie.",
);
/**
* Generates a random string in Base64URL to be used as an authenticity token
* for CSRF protection.
* @param bytes The number of bytes used to generate the token
* @returns A random string in Base64URL
*/
generate(bytes = 32) {
let token = cryptoJS.lib.WordArray.random(bytes).toString(cryptoJS.enc.Base64url);
if (!this.secret)
return token;
let signature = this.sign(token);
return [token, signature].join(".");
}
if (typeof cookie !== "string") {
throw new CSRFError(
"invalid_token_in_cookie",
"Invalid CSRF token in cookie.",
);
/**
* Generates a token and serialize it into the cookie.
* @param requestOrHeaders A request or headers object from which we can
* get the cookie to get the existing token.
* @param bytes The number of bytes used to generate the token
* @returns A tuple with the token and the string to send in Set-Cookie
* If there's already a csrf value in the cookie then the token will
* be the same and the cookie will be null.
* @example
* let [token, cookie] = await csrf.commitToken(request);
* return json({ token }, {
* headers: { "set-cookie": cookie }
* })
*/
async commitToken(requestOrHeaders = new Headers(), bytes = 32) {
let headers = getHeaders(requestOrHeaders);
let existingToken = await this.cookie.parse(headers.get("cookie"));
let token = typeof existingToken === "string" ? existingToken : this.generate(bytes);
let cookie = existingToken ? null : await this.cookie.serialize(token);
return [token, cookie];
}
if (this.verifySignature(cookie) === false) {
throw new CSRFError(
"tampered_token_in_cookie",
"Tampered CSRF token in cookie.",
);
async validate(data, headers) {
if (data instanceof Request && data.bodyUsed) {
throw new Error("The body of the request was read before calling CSRF#verify. Ensure you clone it before reading it.");
}
let formData = await this.readBody(data);
let cookie = await this.parseCookie(data, headers);
// if the session doesn't have a csrf token, throw an error
if (cookie === null) {
throw new CSRFError("missing_token_in_cookie", "Can't find CSRF token in cookie.");
}
if (typeof cookie !== "string") {
throw new CSRFError("invalid_token_in_cookie", "Invalid CSRF token in cookie.");
}
if (this.verifySignature(cookie) === false) {
throw new CSRFError("tampered_token_in_cookie", "Tampered CSRF token in cookie.");
}
// if the body doesn't have a csrf token, throw an error
if (!formData.get(this.formDataKey)) {
throw new CSRFError("missing_token_in_body", "Can't find CSRF token in body.");
}
// if the body csrf token doesn't match the session csrf token, throw an
// error
if (formData.get(this.formDataKey) !== cookie) {
throw new CSRFError("mismatched_token", "Can't verify CSRF token authenticity.");
}
}
// if the body doesn't have a csrf token, throw an error
if (!formData.get(this.formDataKey)) {
throw new CSRFError(
"missing_token_in_body",
"Can't find CSRF token in body.",
);
async readBody(data) {
if (data instanceof FormData)
return data;
return await data.clone().formData();
}
// if the body csrf token doesn't match the session csrf token, throw an
// error
if (formData.get(this.formDataKey) !== cookie) {
throw new CSRFError(
"mismatched_token",
"Can't verify CSRF token authenticity.",
);
parseCookie(data, headers) {
if (data instanceof Request)
headers = data.headers;
if (!headers)
return null;
return this.cookie.parse(headers.get("cookie"));
}
}
async readBody(data) {
if (data instanceof FormData) return data;
return await data.clone().formData();
}
parseCookie(data, headers) {
if (data instanceof Request) headers = data.headers;
if (!headers) return null;
return this.cookie.parse(headers.get("cookie"));
}
sign(token) {
if (!this.secret) return token;
return cryptoJS
.HmacSHA256(token, this.secret)
.toString(cryptoJS.enc.Base64url);
}
verifySignature(token) {
if (!this.secret) return true;
let [value, signature] = token.split(".");
let expectedSignature = this.sign(value);
return signature === expectedSignature;
}
sign(token) {
if (!this.secret)
return token;
return cryptoJS
.HmacSHA256(token, this.secret)
.toString(cryptoJS.enc.Base64url);
}
verifySignature(token) {
if (!this.secret)
return true;
let [value, signature] = token.split(".");
let expectedSignature = this.sign(value);
return signature === expectedSignature;
}
}
interface SendFunctionArgs {
/**
* @default "message"
*/
event?: string;
data: string;
/**
* @default "message"
*/
event?: string;
data: string;
}
interface SendFunction {
(args: SendFunctionArgs): void;
(args: SendFunctionArgs): void;
}
interface CleanupFunction {
(): void;
(): void;
}
interface AbortFunction {
(): void;
(): void;
}
interface InitFunction {
(send: SendFunction, abort: AbortFunction): CleanupFunction;
(send: SendFunction, abort: AbortFunction): CleanupFunction;
}

@@ -26,7 +26,3 @@ /**

*/
export declare function eventStream(
signal: AbortSignal,
init: InitFunction,
options?: ResponseInit,
): Response;
export declare function eventStream(signal: AbortSignal, init: InitFunction, options?: ResponseInit): Response;
export {};

@@ -8,36 +8,38 @@ /**

export function eventStream(signal, init, options = {}) {
let stream = new ReadableStream({
start(controller) {
let encoder = new TextEncoder();
function send({ event = "message", data }) {
controller.enqueue(encoder.encode(`event: ${event}\n`));
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
}
let cleanup = init(send, close);
let closed = false;
function close() {
if (closed) return;
cleanup();
closed = true;
signal.removeEventListener("abort", close);
controller.close();
}
signal.addEventListener("abort", close);
if (signal.aborted) return close();
},
});
let headers = new Headers(options.headers);
if (headers.has("Content-Type")) {
console.warn("Overriding Content-Type header to `text/event-stream`");
}
if (headers.has("Cache-Control")) {
console.warn("Overriding Cache-Control header to `no-cache`");
}
if (headers.has("Connection")) {
console.warn("Overriding Connection header to `keep-alive`");
}
headers.set("Content-Type", "text/event-stream");
headers.set("Cache-Control", "no-cache");
headers.set("Connection", "keep-alive");
return new Response(stream, { headers });
let stream = new ReadableStream({
start(controller) {
let encoder = new TextEncoder();
function send({ event = "message", data }) {
controller.enqueue(encoder.encode(`event: ${event}\n`));
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
}
let cleanup = init(send, close);
let closed = false;
function close() {
if (closed)
return;
cleanup();
closed = true;
signal.removeEventListener("abort", close);
controller.close();
}
signal.addEventListener("abort", close);
if (signal.aborted)
return close();
},
});
let headers = new Headers(options.headers);
if (headers.has("Content-Type")) {
console.warn("Overriding Content-Type header to `text/event-stream`");
}
if (headers.has("Cache-Control")) {
console.warn("Overriding Cache-Control header to `no-cache`");
}
if (headers.has("Connection")) {
console.warn("Overriding Connection header to `keep-alive`");
}
headers.set("Content-Type", "text/event-stream");
headers.set("Cache-Control", "no-cache");
headers.set("Connection", "keep-alive");
return new Response(stream, { headers });
}

@@ -1,3 +0,3 @@

import isIP from "is-ip";
import { getHeaders } from "./get-headers";
import { isIP } from "is-ip";
import { getHeaders } from "./get-headers.js";
/**

@@ -8,42 +8,45 @@ * This is the list of headers, in order of preference, that will be used to

const headerNames = Object.freeze([
"X-Client-IP",
"X-Forwarded-For",
"HTTP-X-Forwarded-For",
"Fly-Client-IP",
"CF-Connecting-IP",
"Fastly-Client-Ip",
"True-Client-Ip",
"X-Real-IP",
"X-Cluster-Client-IP",
"X-Forwarded",
"Forwarded-For",
"Forwarded",
"DO-Connecting-IP" /** Digital ocean app platform */,
"oxygen-buyer-ip" /** Shopify oxygen platform */,
"X-Client-IP",
"X-Forwarded-For",
"HTTP-X-Forwarded-For",
"Fly-Client-IP",
"CF-Connecting-IP",
"Fastly-Client-Ip",
"True-Client-Ip",
"X-Real-IP",
"X-Cluster-Client-IP",
"X-Forwarded",
"Forwarded-For",
"Forwarded",
"DO-Connecting-IP" /** Digital ocean app platform */,
"oxygen-buyer-ip" /** Shopify oxygen platform */,
]);
export function getClientIPAddress(requestOrHeaders) {
let headers = getHeaders(requestOrHeaders);
let ipAddress = headerNames
.flatMap((headerName) => {
let value = headers.get(headerName);
if (headerName === "Forwarded") {
return parseForwardedHeader(value);
}
if (!(value === null || value === void 0 ? void 0 : value.includes(",")))
return value;
return value.split(",").map((ip) => ip.trim());
let headers = getHeaders(requestOrHeaders);
let ipAddress = headerNames
.flatMap((headerName) => {
let value = headers.get(headerName);
if (headerName === "Forwarded") {
return parseForwardedHeader(value);
}
if (!value?.includes(","))
return value;
return value.split(",").map((ip) => ip.trim());
})
.find((ip) => {
if (ip === null) return false;
return isIP(ip);
.find((ip) => {
if (ip === null)
return false;
return isIP(ip);
});
return ipAddress !== null && ipAddress !== void 0 ? ipAddress : null;
return ipAddress ?? null;
}
function parseForwardedHeader(value) {
if (!value) return null;
for (let part of value.split(";")) {
if (part.startsWith("for=")) return part.slice(4);
continue;
}
return null;
if (!value)
return null;
for (let part of value.split(";")) {
if (part.startsWith("for="))
return part.slice(4);
continue;
}
return null;
}
import { parseAcceptLanguage } from "intl-parse-accept-language";
import { getHeaders } from "./get-headers";
import { getHeaders } from "./get-headers.js";
export function getClientLocales(requestOrHeaders) {
let headers = getHeaders(requestOrHeaders);
let acceptLanguage = headers.get("Accept-Language");
// if the header is not defined, return undefined
if (!acceptLanguage) return undefined;
let locales = parseAcceptLanguage(acceptLanguage, {
validate: Intl.DateTimeFormat.supportedLocalesOf,
ignoreWildcard: true,
});
// if there are no locales found, return undefined
if (locales.length === 0) return undefined;
// if there are multiple locales, return the array
return locales;
let headers = getHeaders(requestOrHeaders);
let acceptLanguage = headers.get("Accept-Language");
// if the header is not defined, return undefined
if (!acceptLanguage)
return undefined;
let locales = parseAcceptLanguage(acceptLanguage, {
validate: Intl.DateTimeFormat.supportedLocalesOf,
ignoreWildcard: true,
});
// if there are no locales found, return undefined
if (locales.length === 0)
return undefined;
// if there are multiple locales, return the array
return locales;
}

@@ -6,4 +6,2 @@ /**

*/
export declare function getHeaders(
requestOrHeaders: Request | Headers,
): Headers;
export declare function getHeaders(requestOrHeaders: Request | Headers): Headers;

@@ -7,6 +7,6 @@ /**

export function getHeaders(requestOrHeaders) {
if (requestOrHeaders instanceof Request) {
return requestOrHeaders.headers;
}
return requestOrHeaders;
if (requestOrHeaders instanceof Request) {
return requestOrHeaders.headers;
}
return requestOrHeaders;
}
export interface HoneypotInputProps {
nameFieldName: string;
validFromFieldName: string;
encryptedValidFrom: string;
nameFieldName: string;
validFromFieldName: string | null;
encryptedValidFrom: string;
}
export interface HonetpotConfig {
randomizeNameFieldName?: boolean;
nameFieldName?: string;
validFromFieldName?: string;
validFromTimestamp?: number;
encryptionSeed?: string;
randomizeNameFieldName?: boolean;
nameFieldName?: string;
validFromFieldName?: string | null;
encryptionSeed?: string;
}
export declare class SpamError extends Error {}
export declare class SpamError extends Error {
}
export declare class Honeypot {
protected config: HonetpotConfig;
private generatedEncryptionSeed;
constructor(config?: HonetpotConfig);
getInputProps(): HoneypotInputProps;
check(formData: FormData): void;
protected get nameFieldName(): string;
protected get validFromFieldName(): string;
protected get validFromTimestamp(): number;
protected get encryptionSeed(): string;
protected getRandomizedNameFieldName(
nameFieldName: string,
formData: FormData,
): string | undefined;
protected shouldCheckHoneypot(
formData: FormData,
nameFieldName: string,
): boolean;
protected randomValue(): string;
protected encrypt(value: string): string;
protected decrypt(value: string): string;
protected isFuture(timestamp: number): boolean;
protected isValidTimeStamp(timestampp: number): boolean;
protected config: HonetpotConfig;
private generatedEncryptionSeed;
constructor(config?: HonetpotConfig);
getInputProps({ validFromTimestamp, }?: {
validFromTimestamp?: number | undefined;
}): HoneypotInputProps;
check(formData: FormData): void;
protected get nameFieldName(): string;
protected get validFromFieldName(): string | null;
protected get encryptionSeed(): string;
protected getRandomizedNameFieldName(nameFieldName: string, formData: FormData): string | undefined;
protected shouldCheckHoneypot(formData: FormData, nameFieldName: string): boolean;
protected randomValue(): string;
protected encrypt(value: string): string;
protected decrypt(value: string): string;
protected isFuture(timestamp: number): boolean;
protected isValidTimeStamp(timestampp: number): boolean;
}
import CryptoJS from "crypto-js";
export class SpamError extends Error {}
export class SpamError extends Error {
}
const DEFAULT_NAME_FIELD_NAME = "name__confirm";
const DEFAULT_VALID_FROM_FIELD_NAME = "from__confirm";
export class Honeypot {
constructor(config = {}) {
this.config = config;
this.generatedEncryptionSeed = this.randomValue();
}
getInputProps() {
return {
nameFieldName: this.nameFieldName,
validFromFieldName: this.validFromFieldName,
encryptedValidFrom: this.encrypt(this.validFromTimestamp.toString()),
};
}
check(formData) {
var _a;
let nameFieldName =
(_a = this.config.nameFieldName) !== null && _a !== void 0
? _a
: "honeypot";
if (this.config.randomizeNameFieldName) {
let actualName = this.getRandomizedNameFieldName(nameFieldName, formData);
if (actualName) nameFieldName = actualName;
config;
generatedEncryptionSeed = this.randomValue();
constructor(config = {}) {
this.config = config;
}
if (!this.shouldCheckHoneypot(formData, nameFieldName)) return;
if (!formData.has(nameFieldName)) {
throw new SpamError("Missing honeypot input");
getInputProps({ validFromTimestamp = Date.now(), } = {}) {
return {
nameFieldName: this.nameFieldName,
validFromFieldName: this.validFromFieldName,
encryptedValidFrom: this.encrypt(validFromTimestamp.toString()),
};
}
let honeypotValue = formData.get(nameFieldName);
if (honeypotValue !== "") throw new SpamError("Honeypot input not empty");
if (!this.config.validFromTimestamp) return;
let validFrom = formData.get(this.validFromFieldName);
if (!validFrom) throw new SpamError("Missing honeypot valid from input");
let time = this.decrypt(validFrom);
if (!time) throw new SpamError("Invalid honeypot valid from input");
if (!this.isValidTimeStamp(Number(time))) {
throw new SpamError("Invalid honeypot valid from input");
check(formData) {
let nameFieldName = this.config.nameFieldName ?? DEFAULT_NAME_FIELD_NAME;
if (this.config.randomizeNameFieldName) {
let actualName = this.getRandomizedNameFieldName(nameFieldName, formData);
if (actualName)
nameFieldName = actualName;
}
if (!this.shouldCheckHoneypot(formData, nameFieldName))
return;
if (!formData.has(nameFieldName)) {
throw new SpamError("Missing honeypot input");
}
let honeypotValue = formData.get(nameFieldName);
if (honeypotValue !== "")
throw new SpamError("Honeypot input not empty");
if (!this.validFromFieldName)
return;
let validFrom = formData.get(this.validFromFieldName);
if (!validFrom)
throw new SpamError("Missing honeypot valid from input");
let time = this.decrypt(validFrom);
if (!time)
throw new SpamError("Invalid honeypot valid from input");
if (!this.isValidTimeStamp(Number(time))) {
throw new SpamError("Invalid honeypot valid from input");
}
if (this.isFuture(Number(time))) {
throw new SpamError("Honeypot valid from is in future");
}
}
if (this.isFuture(Number(time))) {
throw new SpamError("Honeypot valid from is in future");
get nameFieldName() {
let fieldName = this.config.nameFieldName ?? DEFAULT_NAME_FIELD_NAME;
if (!this.config.randomizeNameFieldName)
return fieldName;
return `${fieldName}_${this.randomValue()}`;
}
}
get nameFieldName() {
var _a;
let fieldName =
(_a = this.config.nameFieldName) !== null && _a !== void 0
? _a
: "honeypot";
if (!this.config.randomizeNameFieldName) return fieldName;
return `${fieldName}_${this.randomValue()}`;
}
get validFromFieldName() {
var _a;
return (_a = this.config.validFromFieldName) !== null && _a !== void 0
? _a
: "honeypot_from";
}
get validFromTimestamp() {
var _a;
return (_a = this.config.validFromTimestamp) !== null && _a !== void 0
? _a
: Date.now();
}
get encryptionSeed() {
var _a;
return (_a = this.config.encryptionSeed) !== null && _a !== void 0
? _a
: this.generatedEncryptionSeed;
}
getRandomizedNameFieldName(nameFieldName, formData) {
for (let key of formData.keys()) {
if (!key.startsWith(nameFieldName)) continue;
return key;
get validFromFieldName() {
if (this.config.validFromFieldName === undefined) {
return DEFAULT_VALID_FROM_FIELD_NAME;
}
return this.config.validFromFieldName;
}
}
shouldCheckHoneypot(formData, nameFieldName) {
return formData.has(nameFieldName) || formData.has(this.validFromFieldName);
}
randomValue() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
encrypt(value) {
return CryptoJS.AES.encrypt(value, this.encryptionSeed).toString();
}
decrypt(value) {
return CryptoJS.AES.decrypt(value, this.encryptionSeed).toString(
CryptoJS.enc.Utf8,
);
}
isFuture(timestamp) {
return timestamp > Date.now();
}
isValidTimeStamp(timestampp) {
if (Number.isNaN(timestampp)) return false;
if (timestampp <= 0) return false;
if (timestampp >= Number.MAX_SAFE_INTEGER) return false;
return true;
}
get encryptionSeed() {
return this.config.encryptionSeed ?? this.generatedEncryptionSeed;
}
getRandomizedNameFieldName(nameFieldName, formData) {
for (let key of formData.keys()) {
if (!key.startsWith(nameFieldName))
continue;
return key;
}
}
shouldCheckHoneypot(formData, nameFieldName) {
return (formData.has(nameFieldName) ||
Boolean(this.validFromFieldName && formData.has(this.validFromFieldName)));
}
randomValue() {
return CryptoJS.lib.WordArray.random(128 / 8).toString();
}
encrypt(value) {
return CryptoJS.AES.encrypt(value, this.encryptionSeed).toString();
}
decrypt(value) {
return CryptoJS.AES.decrypt(value, this.encryptionSeed).toString(CryptoJS.enc.Utf8);
}
isFuture(timestamp) {
return timestamp > Date.now();
}
isValidTimeStamp(timestampp) {
if (Number.isNaN(timestampp))
return false;
if (timestampp <= 0)
return false;
if (timestampp >= Number.MAX_SAFE_INTEGER)
return false;
return true;
}
}

@@ -1,16 +0,11 @@

import { getHeaders } from "./get-headers";
import { getHeaders } from "./get-headers.js";
export function isPrefetch(requestOrHeaders) {
let headers = getHeaders(requestOrHeaders);
let purpose =
headers.get("Purpose") ||
headers.get("X-Purpose") ||
headers.get("Sec-Purpose") ||
headers.get("Sec-Fetch-Purpose") ||
headers.get("Moz-Purpose") ||
headers.get("X-Moz");
return (
(purpose === null || purpose === void 0
? void 0
: purpose.toLowerCase()) === "prefetch"
);
let headers = getHeaders(requestOrHeaders);
let purpose = headers.get("Purpose") ||
headers.get("X-Purpose") ||
headers.get("Sec-Purpose") ||
headers.get("Sec-Fetch-Purpose") ||
headers.get("Moz-Purpose") ||
headers.get("X-Moz");
return purpose?.toLowerCase() === "prefetch";
}
import type { TypedResponse } from "@remix-run/server-runtime";
type ResponseResult<LoaderData> = {
[Key in keyof LoaderData]: LoaderData[Key] extends () => infer ReturnValue
? ReturnValue extends PromiseLike<infer Value>
? Value
: ReturnValue
: LoaderData[Key] extends PromiseLike<infer Value>
? Value
: LoaderData[Key];
[Key in keyof LoaderData]: LoaderData[Key] extends () => infer ReturnValue ? ReturnValue extends PromiseLike<infer Value> ? Value : ReturnValue : LoaderData[Key] extends PromiseLike<infer Value> ? Value : LoaderData[Key];
};
export declare function jsonHash<LoaderData extends Record<string, unknown>>(
input: LoaderData,
init?: ResponseInit | number,
): Promise<TypedResponse<ResponseResult<LoaderData>>>;
export declare function jsonHash<LoaderData extends Record<string, unknown>>(input: LoaderData, init?: ResponseInit | number): Promise<TypedResponse<ResponseResult<LoaderData>>>;
export {};
import { json as remixJson } from "@remix-run/server-runtime";
export async function jsonHash(input, init) {
let result = {};
let resolvedResults = await Promise.all(
Object.entries(input).map(async ([key, value]) => {
if (value instanceof Function) value = value();
if (value instanceof Promise) value = await value;
return [key, value];
}),
);
for (let [key, value] of resolvedResults) {
result[key] = value;
}
return remixJson(result, init);
let result = {};
let resolvedResults = await Promise.all(Object.entries(input).map(async ([key, value]) => {
if (value instanceof Function)
value = value();
if (value instanceof Promise)
value = await value;
return [key, value];
}));
for (let [key, value] of resolvedResults) {
result[key] =
value;
}
return remixJson(result, init);
}
import type { TypedResponse } from "@remix-run/server-runtime";
type ActionsRecord = Record<string, () => Promise<TypedResponse<unknown>>>;
type ResponsesRecord<Actions extends ActionsRecord> = {
[Action in keyof Actions]: Actions[Action] extends () => Promise<
TypedResponse<infer Result>
>
? Result
: never;
[Action in keyof Actions]: Actions[Action] extends () => Promise<TypedResponse<infer Result>> ? Result : never;
};
type ResponsesUnion<Actions extends ActionsRecord> =
ResponsesRecord<Actions>[keyof Actions];
type ResponsesUnion<Actions extends ActionsRecord> = ResponsesRecord<Actions>[keyof Actions];
/**

@@ -20,18 +15,6 @@ * Runs an action based on the request's action name

*/
export declare function namedAction<Actions extends ActionsRecord>(
request: Request,
actions: Actions,
): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(
url: URL,
actions: Actions,
): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(
searchParams: URLSearchParams,
actions: Actions,
): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(
formData: FormData,
actions: Actions,
): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(request: Request, actions: Actions): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(url: URL, actions: Actions): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(searchParams: URLSearchParams, actions: Actions): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export declare function namedAction<Actions extends ActionsRecord>(formData: FormData, actions: Actions): Promise<TypedResponse<ResponsesUnion<Actions>>>;
export {};
export async function namedAction(input, actions) {
let name = await getActionName(input);
if (name && name in actions) {
return actions[name]();
}
if (name === null && "default" in actions) {
return actions["default"]();
}
if (name === null) throw new ReferenceError("Action name not found");
throw new ReferenceError(`Action "${name}" not found`);
let name = await getActionName(input);
if (name && name in actions) {
return actions[name]();
}
if (name === null && "default" in actions) {
return actions["default"]();
}
if (name === null)
throw new ReferenceError("Action name not found");
throw new ReferenceError(`Action "${name}" not found`);
}
async function getActionName(input) {
if (input instanceof Request) {
let actionName = findNameInURL(new URL(input.url).searchParams);
if (actionName) return actionName;
return findNameInFormData(await input.clone().formData());
}
if (input instanceof URL) {
return findNameInURL(input.searchParams);
}
if (input instanceof URLSearchParams) {
return findNameInURL(input);
}
if (input instanceof FormData) {
return findNameInFormData(input);
}
return null;
if (input instanceof Request) {
let actionName = findNameInURL(new URL(input.url).searchParams);
if (actionName)
return actionName;
return findNameInFormData(await input.clone().formData());
}
if (input instanceof URL) {
return findNameInURL(input.searchParams);
}
if (input instanceof URLSearchParams) {
return findNameInURL(input);
}
if (input instanceof FormData) {
return findNameInFormData(input);
}
return null;
}
function findNameInURL(searchParams) {
for (let key of searchParams.keys()) {
if (key.startsWith("/")) return key.slice(1);
}
let actionName = searchParams.get("intent");
if (typeof actionName === "string") return actionName;
actionName = searchParams.get("action");
if (typeof actionName === "string") return actionName;
actionName = searchParams.get("_action");
if (typeof actionName === "string") return actionName;
return null;
for (let key of searchParams.keys()) {
if (key.startsWith("/"))
return key.slice(1);
}
let actionName = searchParams.get("intent");
if (typeof actionName === "string")
return actionName;
actionName = searchParams.get("action");
if (typeof actionName === "string")
return actionName;
actionName = searchParams.get("_action");
if (typeof actionName === "string")
return actionName;
return null;
}
function findNameInFormData(formData) {
for (let key of formData.keys()) {
if (key.startsWith("/")) return key.slice(1);
}
let actionName = formData.get("intent");
if (typeof actionName === "string") return actionName;
actionName = formData.get("action");
if (typeof actionName === "string") return actionName;
actionName = formData.get("_action");
if (typeof actionName === "string") return actionName;
return null;
for (let key of formData.keys()) {
if (key.startsWith("/"))
return key.slice(1);
}
let actionName = formData.get("intent");
if (typeof actionName === "string")
return actionName;
actionName = formData.get("action");
if (typeof actionName === "string")
return actionName;
actionName = formData.get("_action");
if (typeof actionName === "string")
return actionName;
return null;
}

@@ -9,5 +9,5 @@ /**

export declare function parseAcceptHeader(header: string): {
type: string;
subtype: string;
params: any;
type: string;
subtype: string;
params: any;
}[];

@@ -9,12 +9,10 @@ /**

export function parseAcceptHeader(header) {
let types = header.split(",").map((type) => type.trim());
let parsedTypes = types.map((value) => {
let [mediaType, ...params] = value.split(";");
let [type, subtype] = mediaType.split("/").map((part) => part.trim());
let parsedParams = Object.fromEntries(
params.map((param) => param.split("=")),
);
return { type, subtype, params: parsedParams };
});
return parsedTypes;
let types = header.split(",").map((type) => type.trim());
let parsedTypes = types.map((value) => {
let [mediaType, ...params] = value.split(";");
let [type, subtype] = mediaType.split("/").map((part) => part.trim());
let parsedParams = Object.fromEntries(params.map((param) => param.split("=")));
return { type, subtype, params: parsedParams };
});
return parsedTypes;
}

@@ -27,6 +27,3 @@ import { EntryContext } from "@remix-run/server-runtime";

*/
export declare function preloadRouteAssets(
context: EntryContext,
headers: Headers,
): void;
export declare function preloadRouteAssets(context: EntryContext, headers: Headers): void;
/**

@@ -58,6 +55,3 @@ * Preload the assets linked in the routes matching the current request.

*/
export declare function preloadLinkedAssets(
context: EntryContext,
headers: Headers,
): void;
export declare function preloadLinkedAssets(context: EntryContext, headers: Headers): void;
/**

@@ -88,5 +82,2 @@ * Add Link headers to preload the JS modules in the route matching the current

*/
export declare function preloadModuleAssets(
context: EntryContext,
headers: Headers,
): void;
export declare function preloadModuleAssets(context: EntryContext, headers: Headers): void;

@@ -27,4 +27,4 @@ /**

export function preloadRouteAssets(context, headers) {
preloadLinkedAssets(context, headers); // preload links
preloadModuleAssets(context, headers); // preload JS modules
preloadLinkedAssets(context, headers); // preload links
preloadModuleAssets(context, headers); // preload JS modules
}

@@ -58,25 +58,26 @@ /**

export function preloadLinkedAssets(context, headers) {
let links = context.staticHandlerContext.matches
.flatMap((match) => {
let route = context.routeModules[match.route.id];
if (route.links instanceof Function) return route.links();
return [];
let links = context.staticHandlerContext.matches
.flatMap((match) => {
let route = context.routeModules[match.route.id];
if (route.links instanceof Function)
return route.links();
return [];
})
.map((link) => {
if ("as" in link && "href" in link) {
return { href: link.href, as: link.as };
}
if ("rel" in link && "href" in link && link.rel === "stylesheet")
return { href: link.href, as: "style" };
return null;
.map((link) => {
if ("as" in link && "href" in link) {
return { href: link.href, as: link.as };
}
if ("rel" in link && "href" in link && link.rel === "stylesheet")
return { href: link.href, as: "style" };
return null;
})
.filter((link) => {
return link !== null && "href" in link;
.filter((link) => {
return link !== null && "href" in link;
})
.filter((item, index, list) => {
return index === list.findIndex((link) => link.href === item.href);
.filter((item, index, list) => {
return index === list.findIndex((link) => link.href === item.href);
});
for (let link of links) {
headers.append("Link", `<${link.href}>; rel=preload; as=${link.as}`);
}
for (let link of links) {
headers.append("Link", `<${link.href}>; rel=preload; as=${link.as}`);
}
}

@@ -109,21 +110,14 @@ /**

export function preloadModuleAssets(context, headers) {
var _a;
let urls = [
context.manifest.url,
context.manifest.entry.module,
...context.manifest.entry.imports,
];
for (let match of context.staticHandlerContext.matches) {
let route = context.manifest.routes[match.route.id];
urls.push(
route.module,
...((_a = route.imports) !== null && _a !== void 0 ? _a : []),
);
}
for (let url of urls) {
headers.append(
"Link",
`<${url}>; rel=preload; as=script; crossorigin=anonymous`,
);
}
let urls = [
context.manifest.url,
context.manifest.entry.module,
...context.manifest.entry.imports,
];
for (let match of context.staticHandlerContext.matches) {
let route = context.manifest.routes[match.route.id];
urls.push(route.module, ...(route.imports ?? []));
}
for (let url of urls) {
headers.append("Link", `<${url}>; rel=preload; as=script; crossorigin=anonymous`);
}
}
export interface RespondToHandlers {
default(): Response | Promise<Response>;
[key: string]: () => Response | Promise<Response>;
default(): Response | Promise<Response>;
[key: string]: () => Response | Promise<Response>;
}

@@ -39,9 +39,3 @@ /**

*/
export declare function respondTo(
request: Request,
handlers: RespondToHandlers,
): Promise<Response> | Response;
export declare function respondTo(
headers: Headers,
handlers: RespondToHandlers,
): Promise<Response> | Response;
export declare function respondTo(request: Request, handlers: RespondToHandlers): Promise<Response> | Response;
export declare function respondTo(headers: Headers, handlers: RespondToHandlers): Promise<Response> | Response;

@@ -1,18 +0,22 @@

import { getHeaders } from "./get-headers";
import { parseAcceptHeader } from "./parse-accept-header";
import { getHeaders } from "./get-headers.js";
import { parseAcceptHeader } from "./parse-accept-header.js";
export function respondTo(requestOrHeaders, handlers) {
let headers = getHeaders(requestOrHeaders);
let accept = headers.get("accept");
if (!accept) return handlers.default();
let types = parseAcceptHeader(accept);
for (let { type, subtype } of types) {
let handler = handlers[`${type}/${subtype}`];
if (handler) return handler();
handler = handlers[subtype];
if (handler) return handler();
handler = handlers[type];
if (handler) return handler();
continue;
}
return handlers.default();
let headers = getHeaders(requestOrHeaders);
let accept = headers.get("accept");
if (!accept)
return handlers.default();
let types = parseAcceptHeader(accept);
for (let { type, subtype } of types) {
let handler = handlers[`${type}/${subtype}`];
if (handler)
return handler();
handler = handlers[subtype];
if (handler)
return handler();
handler = handlers[type];
if (handler)
return handler();
continue;
}
return handlers.default();
}

@@ -1,110 +0,3 @@

/// <reference types="node" />
/// <reference types="node" resolution-mode="require"/>
/**
* Create a response receiving a JSON object with the status code 201.
* @example
* export async function action({ request }: ActionArgs) {
* let result = await doSomething(request);
* return created(result);
* }
*/
export declare function created<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a new Response with a redirect set to the URL the user was before.
* It uses the Referer header to detect the previous URL. It asks for a fallback
* URL in case the Referer couldn't be found, this fallback should be a URL you
* may be ok the user to land to after an action even if it's not the same.
* @example
* export async function action({ request }: ActionArgs) {
* await doSomething(request);
* // If the user was on `/search?query=something` we redirect to that URL
* // but if we couldn't we redirect to `/search`, which is an good enough
* // fallback
* return redirectBack(request, { fallback: "/search" });
* }
*/
export declare function redirectBack(
request: Request,
{
fallback,
...init
}: ResponseInit & {
fallback: string;
},
): Response;
/**
* Create a response receiving a JSON object with the status code 400.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* throw badRequest<BoundaryData>({ user });
* }
*/
export declare function badRequest<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a response receiving a JSON object with the status code 401.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* throw unauthorized<BoundaryData>({ user });
* }
*/
export declare function unauthorized<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a response receiving a JSON object with the status code 403.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* if (!user.idAdmin) throw forbidden<BoundaryData>({ user });
* }
*/
export declare function forbidden<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a response receiving a JSON object with the status code 404.
* @example
* export async function loader({ request, params }: LoaderArgs) {
* let user = await getUser(request);
* if (!db.exists(params.id)) throw notFound<BoundaryData>({ user });
* }
*/
export declare function notFound<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a response receiving a JSON object with the status code 422.
* @example
* export async function loader({ request, params }: LoaderArgs) {
* let user = await getUser(request);
* throw unprocessableEntity<BoundaryData>({ user });
* }
*/
export declare function unprocessableEntity<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a response receiving a JSON object with the status code 500.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* throw serverError<BoundaryData>({ user });
* }
*/
export declare function serverError<Data = unknown>(
data: Data,
init?: Omit<ResponseInit, "status">,
): import("@remix-run/server-runtime").TypedResponse<Data>;
/**
* Create a response with only the status 304 and optional headers.

@@ -117,5 +10,3 @@ * This is useful when trying to implement conditional responses based on Etags.

*/
export declare function notModified(
init?: Omit<ResponseInit, "status">,
): Response;
export declare function notModified(init?: Omit<ResponseInit, "status">): Response;
/**

@@ -132,6 +23,3 @@ * Create a response with a JavaScript file response.

*/
export declare function javascript(
content: string,
init?: number | ResponseInit,
): Response;
export declare function javascript(content: string, init?: number | ResponseInit): Response;
/**

@@ -148,6 +36,3 @@ * Create a response with a CSS file response.

*/
export declare function stylesheet(
content: string,
init?: number | ResponseInit,
): Response;
export declare function stylesheet(content: string, init?: number | ResponseInit): Response;
/**

@@ -164,6 +49,3 @@ * Create a response with a PDF file response.

*/
export declare function pdf(
content: Blob | Buffer | ArrayBuffer,
init?: number | ResponseInit,
): Response;
export declare function pdf(content: Blob | Buffer | ArrayBuffer, init?: number | ResponseInit): Response;
/**

@@ -180,6 +62,3 @@ * Create a response with a HTML file response.

*/
export declare function html(
content: string,
init?: number | ResponseInit,
): Response;
export declare function html(content: string, init?: number | ResponseInit): Response;
/**

@@ -196,6 +75,3 @@ * Create a response with a XML file response.

*/
export declare function xml(
content: string,
init?: number | ResponseInit,
): Response;
export declare function xml(content: string, init?: number | ResponseInit): Response;
/**

@@ -215,14 +91,4 @@ * Create a response with a TXT file response.

*/
export declare function txt(
content: string,
init?: number | ResponseInit,
): Response;
export type ImageType =
| "image/jpeg"
| "image/png"
| "image/gif"
| "image/svg+xml"
| "image/webp"
| "image/bmp"
| "image/avif";
export declare function txt(content: string, init?: number | ResponseInit): Response;
export type ImageType = "image/jpeg" | "image/png" | "image/gif" | "image/svg+xml" | "image/webp" | "image/bmp" | "image/avif";
/**

@@ -239,10 +105,4 @@ * Create a response with a image file response.

*/
export declare function image(
content: Buffer | ArrayBuffer | ReadableStream,
{
type,
...init
}: ResponseInit & {
export declare function image(content: Buffer | ArrayBuffer | ReadableStream, { type, ...init }: ResponseInit & {
type: ImageType;
},
): Response;
}): Response;

@@ -1,103 +0,2 @@

import { json, redirect } from "@remix-run/server-runtime";
/**
* Create a response receiving a JSON object with the status code 201.
* @example
* export async function action({ request }: ActionArgs) {
* let result = await doSomething(request);
* return created(result);
* }
*/
export function created(data, init) {
return json(data, { ...init, status: 201 });
}
/**
* Create a new Response with a redirect set to the URL the user was before.
* It uses the Referer header to detect the previous URL. It asks for a fallback
* URL in case the Referer couldn't be found, this fallback should be a URL you
* may be ok the user to land to after an action even if it's not the same.
* @example
* export async function action({ request }: ActionArgs) {
* await doSomething(request);
* // If the user was on `/search?query=something` we redirect to that URL
* // but if we couldn't we redirect to `/search`, which is an good enough
* // fallback
* return redirectBack(request, { fallback: "/search" });
* }
*/
export function redirectBack(request, { fallback, ...init }) {
var _a;
return redirect(
(_a = request.headers.get("Referer")) !== null && _a !== void 0
? _a
: fallback,
init,
);
}
/**
* Create a response receiving a JSON object with the status code 400.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* throw badRequest<BoundaryData>({ user });
* }
*/
export function badRequest(data, init) {
return json(data, { ...init, status: 400 });
}
/**
* Create a response receiving a JSON object with the status code 401.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* throw unauthorized<BoundaryData>({ user });
* }
*/
export function unauthorized(data, init) {
return json(data, { ...init, status: 401 });
}
/**
* Create a response receiving a JSON object with the status code 403.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* if (!user.idAdmin) throw forbidden<BoundaryData>({ user });
* }
*/
export function forbidden(data, init) {
return json(data, { ...init, status: 403 });
}
/**
* Create a response receiving a JSON object with the status code 404.
* @example
* export async function loader({ request, params }: LoaderArgs) {
* let user = await getUser(request);
* if (!db.exists(params.id)) throw notFound<BoundaryData>({ user });
* }
*/
export function notFound(data, init) {
return json(data, { ...init, status: 404 });
}
/**
* Create a response receiving a JSON object with the status code 422.
* @example
* export async function loader({ request, params }: LoaderArgs) {
* let user = await getUser(request);
* throw unprocessableEntity<BoundaryData>({ user });
* }
*/
export function unprocessableEntity(data, init) {
return json(data, { ...init, status: 422 });
}
/**
* Create a response receiving a JSON object with the status code 500.
* @example
* export async function loader({ request }: LoaderArgs) {
* let user = await getUser(request);
* throw serverError<BoundaryData>({ user });
* }
*/
export function serverError(data, init) {
return json(data, { ...init, status: 500 });
}
/**
* Create a response with only the status 304 and optional headers.

@@ -111,3 +10,3 @@ * This is useful when trying to implement conditional responses based on Etags.

export function notModified(init) {
return new Response("", { ...init, status: 304 });
return new Response("", { ...init, status: 304 });
}

@@ -126,11 +25,11 @@ /**

export function javascript(content, init = {}) {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/javascript; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/javascript; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
}

@@ -149,11 +48,11 @@ /**

export function stylesheet(content, init = {}) {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/css; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/css; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
}

@@ -172,11 +71,11 @@ /**

export function pdf(content, init = {}) {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/pdf");
}
return new Response(content, {
...responseInit,
headers,
});
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/pdf");
}
return new Response(content, {
...responseInit,
headers,
});
}

@@ -195,11 +94,11 @@ /**

export function html(content, init = {}) {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/html; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/html; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
}

@@ -218,11 +117,11 @@ /**

export function xml(content, init = {}) {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/xml; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/xml; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
}

@@ -244,11 +143,11 @@ /**

export function txt(content, init = {}) {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/plain; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "text/plain; charset=utf-8");
}
return new Response(content, {
...responseInit,
headers,
});
}

@@ -267,10 +166,10 @@ /**

export function image(content, { type, ...init }) {
let headers = new Headers(init.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", type);
}
return new Response(content, {
...init,
headers,
});
let headers = new Headers(init.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", type);
}
return new Response(content, {
...init,
headers,
});
}
import type { Cookie } from "@remix-run/server-runtime";
import { z } from "zod";
import { TypedCookie } from "./typed-cookie";
export declare function rollingCookie<Schema extends z.ZodTypeAny>(
cookie: Cookie | TypedCookie<Schema>,
request: Request,
responseHeaders: Headers,
): Promise<void>;
import { TypedCookie } from "./typed-cookie.js";
export declare function rollingCookie<Schema extends z.ZodTypeAny>(cookie: Cookie | TypedCookie<Schema>, request: Request, responseHeaders: Headers): Promise<void>;
export async function rollingCookie(cookie, request, responseHeaders) {
let value = await cookie.parse(responseHeaders.get("Set-Cookie"));
if (value !== null) return;
value = await cookie.parse(request.headers.get("Cookie"));
if (!value) return;
responseHeaders.append("Set-Cookie", await cookie.serialize(value));
let value = await cookie.parse(responseHeaders.get("Set-Cookie"));
if (value !== null)
return;
value = await cookie.parse(request.headers.get("Cookie"));
if (!value)
return;
responseHeaders.append("Set-Cookie", await cookie.serialize(value));
}

@@ -10,5 +10,2 @@ /**

*/
export declare function safeRedirect(
to: FormDataEntryValue | string | null | undefined,
defaultRedirect?: string,
): string;
export declare function safeRedirect(to: FormDataEntryValue | string | null | undefined, defaultRedirect?: string): string;

@@ -12,13 +12,12 @@ const DEFAULT_REDIRECT = "/";

export function safeRedirect(to, defaultRedirect = DEFAULT_REDIRECT) {
if (!to || typeof to !== "string") return defaultRedirect;
to = to.trim();
if (
!to.startsWith("/") ||
to.startsWith("//") ||
to.startsWith("/\\") ||
to.includes("..")
) {
return defaultRedirect;
}
return to;
if (!to || typeof to !== "string")
return defaultRedirect;
to = to.trim();
if (!to.startsWith("/") ||
to.startsWith("//") ||
to.startsWith("/\\") ||
to.includes("..")) {
return defaultRedirect;
}
return to;
}

@@ -1,24 +0,11 @@

import {
Cookie,
CookieParseOptions,
CookieSerializeOptions,
} from "@remix-run/server-runtime";
import { Cookie, CookieParseOptions, CookieSerializeOptions } from "@remix-run/server-runtime";
import type { z } from "zod";
export interface TypedCookie<Schema extends z.ZodTypeAny> extends Cookie {
isTyped: true;
parse(
cookieHeader: string | null,
options?: CookieParseOptions,
): Promise<z.infer<Schema> | null>;
serialize(
value: z.infer<Schema>,
options?: CookieSerializeOptions,
): Promise<string>;
isTyped: true;
parse(cookieHeader: string | null, options?: CookieParseOptions): Promise<z.infer<Schema> | null>;
serialize(value: z.infer<Schema>, options?: CookieSerializeOptions): Promise<string>;
}
export declare function createTypedCookie<Schema extends z.ZodTypeAny>({
cookie,
schema,
}: {
cookie: Cookie;
schema: Schema;
export declare function createTypedCookie<Schema extends z.ZodTypeAny>({ cookie, schema, }: {
cookie: Cookie;
schema: Schema;
}): TypedCookie<Schema>;

@@ -30,4 +17,2 @@ /**

*/
export declare function isTypedCookie<Schema extends z.ZodTypeAny>(
value: unknown,
): value is TypedCookie<Schema>;
export declare function isTypedCookie<Schema extends z.ZodTypeAny>(value: unknown): value is TypedCookie<Schema>;

@@ -1,30 +0,31 @@

import { isCookie } from "@remix-run/server-runtime";
export function createTypedCookie({ cookie, schema }) {
if (schema._def.typeName === "ZodObject") {
let flashSchema = {};
for (let key in schema.shape) {
flashSchema[flash(key)] = schema.shape[key].optional();
import { isCookie, } from "@remix-run/server-runtime";
export function createTypedCookie({ cookie, schema, }) {
if (schema._def.typeName === "ZodObject") {
let flashSchema = {};
for (let key in schema.shape) {
flashSchema[flash(key)] = schema.shape[key].optional();
}
}
}
return {
isTyped: true,
get name() {
return cookie.name;
},
get isSigned() {
return cookie.isSigned;
},
get expires() {
return cookie.expires;
},
async parse(cookieHeader, options) {
if (!cookieHeader) return null;
let value = await cookie.parse(cookieHeader, options);
return await parseSchemaWithFlashKeys(schema, value);
},
async serialize(value, options) {
let parsedValue = await parseSchemaWithFlashKeys(schema, value);
return cookie.serialize(parsedValue, options);
},
};
return {
isTyped: true,
get name() {
return cookie.name;
},
get isSigned() {
return cookie.isSigned;
},
get expires() {
return cookie.expires;
},
async parse(cookieHeader, options) {
if (!cookieHeader)
return null;
let value = await cookie.parse(cookieHeader, options);
return await parseSchemaWithFlashKeys(schema, value);
},
async serialize(value, options) {
let parsedValue = await parseSchemaWithFlashKeys(schema, value);
return cookie.serialize(parsedValue, options);
},
};
}

@@ -37,21 +38,22 @@ /**

export function isTypedCookie(value) {
return isCookie(value) && value.isTyped === true;
return (isCookie(value) &&
value.isTyped === true);
}
function flash(name) {
return `__flash_${name}__`;
return `__flash_${name}__`;
}
function parseSchemaWithFlashKeys(schema, value) {
// if the Schema is not a ZodObject, we use it directly
if (schema._def.typeName !== "ZodObject") {
return schema.nullable().parseAsync(value);
}
// but if it's a ZodObject, we need to add support for flash keys, so we
// get the shape of the schema, create a flash key for each key, and then we
// extend the original schema with the flash schema and parse the value
let objectSchema = schema;
let flashSchema = {};
for (let key in objectSchema.shape) {
flashSchema[flash(key)] = objectSchema.shape[key].optional();
}
return objectSchema.extend(flashSchema).parseAsync(value);
// if the Schema is not a ZodObject, we use it directly
if (schema._def.typeName !== "ZodObject") {
return schema.nullable().parseAsync(value);
}
// but if it's a ZodObject, we need to add support for flash keys, so we
// get the shape of the schema, create a flash key for each key, and then we
// extend the original schema with the flash schema and parse the value
let objectSchema = schema;
let flashSchema = {};
for (let key in objectSchema.shape) {
flashSchema[flash(key)] = objectSchema.shape[key].optional();
}
return objectSchema.extend(flashSchema).parseAsync(value);
}

@@ -1,77 +0,53 @@

import {
CookieParseOptions,
CookieSerializeOptions,
SessionStorage,
} from "@remix-run/server-runtime";
import { CookieParseOptions, CookieSerializeOptions, SessionStorage } from "@remix-run/server-runtime";
import { z } from "zod";
export interface TypedSession<Schema extends z.ZodTypeAny> {
/**
* Marks a session as a typed session.
*/
readonly isTyped: boolean;
/**
* A unique identifier for this session.
*
* Note: This will be the empty string for newly created sessions and
* sessions that are not backed by a database (i.e. cookie-based sessions).
*/
readonly id: string;
/**
* The raw data contained in this session.
*
* This is useful mostly for SessionStorage internally to access the raw
* session data to persist.
*/
readonly data: z.infer<Schema>;
/**
* Returns `true` if the session has a value for the given `name`, `false`
* otherwise.
*/
has<Key extends keyof z.infer<Schema>>(name: Key): boolean;
/**
* Returns the value for the given `name` in this session.
*/
get<Key extends keyof z.infer<Schema>>(key: Key): z.infer<Schema>[Key] | null;
/**
* Sets a value in the session for the given `name`.
*/
set<Key extends keyof z.infer<Schema>>(
name: Key,
value: z.infer<Schema>[Key],
): void;
/**
* Sets a value in the session that is only valid until the next `get()`.
* This can be useful for temporary values, like error messages.
*/
flash<Key extends keyof z.infer<Schema>>(
name: Key,
value: z.infer<Schema>[Key],
): void;
/**
* Removes a value from the session.
*/
unset<Key extends keyof z.infer<Schema>>(name: Key): void;
/**
* Marks a session as a typed session.
*/
readonly isTyped: boolean;
/**
* A unique identifier for this session.
*
* Note: This will be the empty string for newly created sessions and
* sessions that are not backed by a database (i.e. cookie-based sessions).
*/
readonly id: string;
/**
* The raw data contained in this session.
*
* This is useful mostly for SessionStorage internally to access the raw
* session data to persist.
*/
readonly data: z.infer<Schema>;
/**
* Returns `true` if the session has a value for the given `name`, `false`
* otherwise.
*/
has<Key extends keyof z.infer<Schema>>(name: Key): boolean;
/**
* Returns the value for the given `name` in this session.
*/
get<Key extends keyof z.infer<Schema>>(key: Key): z.infer<Schema>[Key] | null;
/**
* Sets a value in the session for the given `name`.
*/
set<Key extends keyof z.infer<Schema>>(name: Key, value: z.infer<Schema>[Key]): void;
/**
* Sets a value in the session that is only valid until the next `get()`.
* This can be useful for temporary values, like error messages.
*/
flash<Key extends keyof z.infer<Schema>>(name: Key, value: z.infer<Schema>[Key]): void;
/**
* Removes a value from the session.
*/
unset<Key extends keyof z.infer<Schema>>(name: Key): void;
}
export interface TypedSessionStorage<Schema extends z.ZodTypeAny> {
getSession(
cookieHeader?: string | null | undefined,
options?: CookieParseOptions | undefined,
): Promise<TypedSession<Schema>>;
commitSession(
session: TypedSession<Schema>,
options?: CookieSerializeOptions | undefined,
): Promise<string>;
destroySession(
session: TypedSession<Schema>,
options?: CookieSerializeOptions | undefined,
): Promise<string>;
getSession(cookieHeader?: string | null | undefined, options?: CookieParseOptions | undefined): Promise<TypedSession<Schema>>;
commitSession(session: TypedSession<Schema>, options?: CookieSerializeOptions | undefined): Promise<string>;
destroySession(session: TypedSession<Schema>, options?: CookieSerializeOptions | undefined): Promise<string>;
}
export declare function createTypedSessionStorage<
Schema extends z.AnyZodObject,
>({
sessionStorage,
schema,
}: {
sessionStorage: SessionStorage;
schema: Schema;
export declare function createTypedSessionStorage<Schema extends z.AnyZodObject>({ sessionStorage, schema, }: {
sessionStorage: SessionStorage;
schema: Schema;
}): TypedSessionStorage<Schema>;

@@ -83,4 +59,2 @@ /**

*/
export declare function isTypedSession<Schema extends z.AnyZodObject>(
value: unknown,
): value is TypedSession<Schema>;
export declare function isTypedSession<Schema extends z.AnyZodObject>(value: unknown): value is TypedSession<Schema>;

@@ -1,71 +0,72 @@

import { isSession } from "@remix-run/server-runtime";
export function createTypedSessionStorage({ sessionStorage, schema }) {
return {
async getSession(cookieHeader, options) {
let session = await sessionStorage.getSession(cookieHeader, options);
return await createTypedSession({ session, schema });
},
async commitSession(session, options) {
// check if session.data is valid
await schema.parseAsync(session.data);
return await sessionStorage.commitSession(session, options);
},
async destroySession(session) {
// check if session.data is valid
await schema.parseAsync(session.data);
return await sessionStorage.destroySession(session);
},
};
import { isSession, } from "@remix-run/server-runtime";
export function createTypedSessionStorage({ sessionStorage, schema, }) {
return {
async getSession(cookieHeader, options) {
let session = await sessionStorage.getSession(cookieHeader, options);
return await createTypedSession({ session, schema });
},
async commitSession(session, options) {
// check if session.data is valid
await schema.parseAsync(session.data);
return await sessionStorage.commitSession(session, options);
},
async destroySession(session) {
// check if session.data is valid
await schema.parseAsync(session.data);
return await sessionStorage.destroySession(session);
},
};
}
async function createTypedSession({ session, schema }) {
// get a raw shape version of the schema but converting all the keys to their
// flash versions.
let flashSchema = {};
for (let key in schema.shape) {
flashSchema[flash(key)] = schema.shape[key].optional();
}
// parse session.data to add default values and remove invalid ones
// we use strict mode here so we can throw an error if the session data
// contains any invalid key, which is a sign that the session data is
// corrupted.
let data = await schema.extend(flashSchema).strict().parseAsync(session.data);
return {
get isTyped() {
return true;
},
get id() {
return session.id;
},
get data() {
return data;
},
has(name) {
let key = String(safeKey(schema, name));
return key in data || flash(key) in data;
},
get(name) {
let key = String(safeKey(schema, name));
if (key in data) return data[key];
let flashKey = flash(key);
if (flashKey in data) {
let value = data[flashKey];
delete data[flashKey];
return value;
}
return;
},
set(name, value) {
let key = String(safeKey(schema, name));
data[key] = value;
},
flash(name, value) {
let key = String(safeKey(schema, name));
let flashKey = flash(key);
data[flashKey] = value;
},
unset(name) {
let key = String(safeKey(schema, name));
delete data[key];
},
};
async function createTypedSession({ session, schema, }) {
// get a raw shape version of the schema but converting all the keys to their
// flash versions.
let flashSchema = {};
for (let key in schema.shape) {
flashSchema[flash(key)] = schema.shape[key].optional();
}
// parse session.data to add default values and remove invalid ones
// we use strict mode here so we can throw an error if the session data
// contains any invalid key, which is a sign that the session data is
// corrupted.
let data = await schema.extend(flashSchema).strict().parseAsync(session.data);
return {
get isTyped() {
return true;
},
get id() {
return session.id;
},
get data() {
return data;
},
has(name) {
let key = String(safeKey(schema, name));
return key in data || flash(key) in data;
},
get(name) {
let key = String(safeKey(schema, name));
if (key in data)
return data[key];
let flashKey = flash(key);
if (flashKey in data) {
let value = data[flashKey];
delete data[flashKey];
return value;
}
return;
},
set(name, value) {
let key = String(safeKey(schema, name));
data[key] = value;
},
flash(name, value) {
let key = String(safeKey(schema, name));
let flashKey = flash(key);
data[flashKey] = value;
},
unset(name) {
let key = String(safeKey(schema, name));
delete data[key];
},
};
}

@@ -78,10 +79,11 @@ /**

export function isTypedSession(value) {
return isSession(value) && value.isTyped === true;
return (isSession(value) &&
value.isTyped === true);
}
function flash(name) {
return `__flash_${name}__`;
return `__flash_${name}__`;
}
// checks that the key is a valid key of the schema
function safeKey(schema, key) {
return schema.keyof().parse(key);
return schema.keyof().parse(key);
}
{
"name": "remix-utils",
"version": "7.0.0",
"version": "7.0.1",
"license": "MIT",

@@ -8,7 +8,43 @@ "engines": {

},
"main": "./build/index.js",
"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 --module ESNext --outDir ./build",
"build": "tsc --project tsconfig.json --outDir ./build",
"postbuild": "prettier --write \"build/**/*.js\" \"build/**/*.d.ts\"",

@@ -40,61 +76,103 @@ "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"test/**/*.ts\" \"test/**/*.tsx\"",

"redirect-back",
"outlet",
"client-only",
"hydrated",
"body parser"
"server-only",
"cors",
"rolling cookie",
"safe redirect",
"typed cookie",
"typed session",
"client IP address",
"client locale",
"json hash",
"prefetch",
"named action"
],
"peerDependencies": {
"@remix-run/react": "^1.19.1",
"@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",
"@remix-run/server-runtime": "^1.19.1",
"crypto-js": "^4.1.1",
"intl-parse-accept-language": "^1.0.0",
"is-ip": "^5.0.1",
"react": "^18.0.0",
"zod": "^3.19.1"
"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": "^1.19.2",
"@remix-run/react": "^1.19.2",
"@remix-run/node": "^2.0.0",
"@remix-run/react": "^2.0.0",
"@remix-run/router": "^1.7.2",
"@remix-run/server-runtime": "^1.19.2",
"@remix-run/testing": "^1.19.3",
"@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^12.1.2",
"@types/crypto-js": "^4.1.1",
"@types/react": "^17.0.14",
"@types/uuid": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"@vitejs/plugin-react": "^4.0.4",
"@vitest/coverage-v8": "^0.34.1",
"@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": "^8.3.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest-dom": "^4.0.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-testing-library": "^5.1.0",
"eslint-plugin-unicorn": "^41.0.1",
"happy-dom": "^10.9.0",
"msw": "^1.2.3",
"prettier": "^2.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-node": "^10.4.0",
"typescript": "^5.1.6",
"vite": "^4.4.9",
"vitest": "^0.34.1",
"zod": "^3.19.1"
"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": {
"crypto-js": "^4.1.1",
"intl-parse-accept-language": "^1.0.0",
"is-ip": "^3.1.0",
"schema-dts": "^1.1.0",
"type-fest": "^2.5.2",
"uuid": "^8.3.2"
"type-fest": "^4.3.3"
}
}
/* eslint-disable unicorn/prefer-module */
/** @type {import("prettier").Config} */
module.exports = {
export default {
trailingComma: "all",
useTabs: true,
};

@@ -11,2 +11,78 @@ # Remix Utils

Additional optional dependencies may be needed, all optional dependencies are:
- `@remix-run/react` (also `@remix-run/router` but you should be using the React one)
- `@remix-run/node` or `@remix-run/cloudflare` or `@remix-run/deno` (actually it's `@remix-run/server-runtime` but you should use one of the others)
- `crypto-js`
- `is-ip`
- `intl-parse-accept-language`
- `react`
- `zod`
The utils that require an extra optional dependency mention it in their documentation.
If you want to install them all run:
```sh
npm add crypto-js is-ip intl-parse-accept-language zod
```
React and the `@remix-run/*` packages should be already installed in your project.
## Upgrade from Remix Utils v6
If you used Remix Utils before, you will notice some changes.
1. The package is published as ESM only
2. Every util now has a specific path in the package, so you need to import them from `remix-utils/<util-name>`.
3. All dependencies are now optional, so you need to install them yourself.
#### Usage with CJS
Since v7 of Remix Utils the package is published as ESM-only (plus type definitions). This means if you're using Remix with CJS you need to configure it to bundle Remix Utils.
In your `remix.config.js` file, add this:
```js
module.exports = {
serverDependenciesToBundle: ["remix-utils"],
};
```
If you're not sure if your app uses ESM or CJS, check if you have `serverModuleFormat` in your `remix.config.js` file to know.
In case you don't have one, if you're using Remix v1 it will be CJS and if you're using Remix v2 it will be ESM.
> **Note**
> Some of the optional dependencies in Remix Utils may still be published as CJS, so you may need to add them to `serverDependenciesToBundle` too.
Another thing to consider if you did the upgrade from Remix v1 to Remix v2 is that in your `tsconfig.json` you will need to set `"moduleResolution": "Bundler"`, otherwise TS will not resolve the new import paths.
#### Updated Import Paths
You will need to change your imports to use the correct one. So instead of doing:
```ts
import { eventStream, useEventSource } from "remix-utils";
```
You need to change it to:
```ts
import { eventStream } from "remix-utils/sse/server";
import { useEventSource } from "remix-utils/sse/react";
```
This adds more lines but enables the next change.
#### Optional Dependencies
Before, Remix Utils installed some dependencies like Zod that you may never used.
Current version marks every dependency as optional, so you need to install them yourself.
While this is more works it means the package can keep using more dependencies as needed without increasing the bundle size for everyone. Only if you use the dependency that depends on Zod you will install and include Zod in your bundle.
Every util mentions what dependencies it requires and the [Installation](#installation) section mentions the whole list in case you want to install them all upfront.
## API Reference

@@ -21,2 +97,4 @@

```ts
import { promiseHash } from "remix-utils/promise";
export async function loader({ request }: LoaderArgs) {

@@ -27,3 +105,3 @@ return json(

posts: getPosts(request),
})
}),
);

@@ -36,2 +114,4 @@ }

```ts
import { promiseHash } from "remix-utils/promise";
export async function loader({ request }: LoaderArgs) {

@@ -48,3 +128,3 @@ return json(

}),
})
}),
);

@@ -59,2 +139,4 @@ }

```ts
import { timeout } from "remix-utils/promise";
try {

@@ -74,2 +156,4 @@ let result = await timeout(fetch("https://example.com"), { ms: 100 });

```ts
import { timeout } from "remix-utils/promise";
try {

@@ -79,3 +163,3 @@ let controller = new AbortController();

fetch("https://example.com", { signal: controller.signal }),
{ ms: 100, controller }
{ ms: 100, controller },
);

@@ -101,3 +185,3 @@ } catch (error) {

```ts
import { cacheAssets } from "remix-utils";
import { cacheAssets } from "remix-utils/cache-assets";

@@ -119,2 +203,4 @@ cacheAssets().catch((error) => {

```ts
import { cacheAssets } from "remix-utils/cache-assets";
cacheAssests({ cacheName: "assets", buildPath: "/build/" }).catch((error) => {

@@ -127,23 +213,10 @@ // do something with the error, or not

> **Note** This depends on `react`.
The ClientOnly component lets you render the children element only on the client-side, avoiding rendering it the server-side.
> **Note**
> If you're using React 18 and a [streaming server rendering API](https://beta.reactjs.org/reference/react-dom/server) (eg. [`renderToPipeableStream`](https://beta.reactjs.org/reference/react-dom/server/renderToPipeableStream)) you probably want to use a `<Suspense>` boundary instead.
>
> ```tsx
> export default function Component() {
> return (
> <Suspense fallback={<SimplerStaticVersion />}>
> <ComplexComponentNeedingBrowserEnvironment />
> </Suspense>
> );
> }
> ```
>
> See ["Providing a fallback for server errors and server-only content" in the React Suspense docs](https://beta.reactjs.org/reference/react/Suspense#providing-a-fallback-for-server-errors-and-server-only-content).
You can provide a fallback component to be used on SSR, and while optional, it's highly recommended to provide one to avoid content layout shift issues.
```tsx
import { ClientOnly } from "remix-utils";
import { ClientOnly } from "remix-utils/client-only";

@@ -172,2 +245,4 @@ export default function Component() {

> **Note** This depends on `react`.
The ServerOnly component is the opposite of the ClientOnly component, it lets you render the children element only on the server-side, avoiding rendering it the client-side.

@@ -178,3 +253,3 @@

```tsx
import { ServerOnly } from "remix-utils";
import { ServerOnly } from "remix-utils/server-only";

@@ -215,2 +290,4 @@ export default function Component() {

```ts
import { cors } from "remix-utils/cors";
export async function loader({ request }: LoaderArgs) {

@@ -226,2 +303,4 @@ let data = await getData(request);

```ts
import { cors } from "remix-utils/cors";
export async function loader({ request }: LoaderArgs) {

@@ -236,2 +315,4 @@ let data = await getData(request);

```ts
import { cors } from "remix-utils/cors";
export async function loader({ request }: LoaderArgs) {

@@ -248,2 +329,4 @@ let data = await getData(request);

```tsx
import { cors } from "remix-utils/cors";
const ABORT_DELAY = 5000;

@@ -255,3 +338,3 @@

responseHeaders: Headers,
remixContext: EntryContext
remixContext: EntryContext,
) {

@@ -278,3 +361,3 @@ let callbackName = isbot(request.headers.get("user-agent"))

status: didError ? 500 : responseStatusCode,
})
}),
).then((response) => {

@@ -294,3 +377,3 @@ resolve(response);

},
}
},
);

@@ -304,3 +387,3 @@

response,
{ request }
{ request },
) => {

@@ -335,2 +418,4 @@ return await cors(request, response);

> **Note**: This depends on `react`, `crypto-js`, and a Remix server runtime.
The CSRF related functions let you implement CSRF protection on your application.

@@ -344,4 +429,4 @@

// app/utils/csrf.server.ts
import { CSRF } from "remix-utils";
import { createCookie } from "@remix-run/node"; // or /cloudflare
import { CSRF } from "remix-utils/csrf";
import { createCookie } from "@remix-run/node"; // or cloudflare/deno

@@ -397,2 +482,4 @@ export const cookie = createCookie("csrf", {

```tsx
import { AuthenticityTokenProvider } from "remix-utils/authenticity-token";
let { csrf } = useLoaderData<LoaderData>();

@@ -412,3 +499,3 @@ return (

import { Form } from "@remix-run/react";
import { AuthenticityTokenInput } from "remix-utils";
import { AuthenticityTokenInput } from "remix-utils/authenticity-token";

@@ -439,3 +526,3 @@ export default function Component() {

import { useFetcher } from "@remix-run/react";
import { useAuthenticityToken } from "remix-utils";
import { useAuthenticityToken } from "remix-utils/authenticity-token";

@@ -448,3 +535,3 @@ export function useMarkAsRead() {

{ csrf, ...data },
{ action: "/api/mark-as-read", method: "post" }
{ action: "/api/mark-as-read", method: "post" },
);

@@ -458,3 +545,4 @@ };

```ts
import { CSRFError, redirectBack } from "remix-utils";
import { CSRFError } from "remix-utils/csrf";
import { redirectBack } from "remix-utils/redirect-back";
import { csrf } from "~/utils/csrf.server";

@@ -504,38 +592,88 @@

### ExternalScripts
### External Scripts
If you need to load different external scripts on certain routes, you can use the `ExternalScripts` component together with the `ExternalScriptsFunction` type.
> **Note**: This depends on `react`, `@remix-run/react`, and a Remix server runtime.
In the route you want to load the script add a `handle` export with a `scripts` method, this method should implement the `ExternalScriptsFunction` type.
If you need to load different external scripts on certain routes, you can use the `ExternalScripts` component together with the `ExternalScriptsFunction` and `ScriptDescriptor` types.
In the route you want to load the script add a `handle` export with a `scripts` method, type the `handle` to be `ExternalScriptsHandle`. This interface is let's you define `scripts` as either a function or an array.
If you want to define what scripts to load based on the loader data, you can use `scripts` as a function:
```ts
// create the scripts function with the correct type
// note: loader type is optional
let scripts: ExternalScriptsFunction<SerializeFrom<typeof loader>> = ({
id,
data,
params,
matches,
location,
parentsData,
}) => {
return [
import { ExternalScriptsHandle } from "remix-utils/external-scripts";
type LoaderData = SerializeFrom<typeof loader>;
export let handle: ExternalScriptsHandle<LoaderData> = {
scripts({ id, data, params, matches, location, parentsData }) {
return [
{
src: "https://unpkg.com/htmx.org@1.9.6",
integrity: "sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni",
crossOrigin: 'anonymous"
}
];
},
};
```
If the list of scripts to load is static you can define `scripts` as an array directly.
```ts
import { ExternalScriptsHandle } from "remix-utils/external-scripts";
export let handle: ExternalScriptsHandle = {
scripts: [
{
src: "https://code.jquery.com/jquery-3.6.0.min.js",
integrity: "sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=",
crossOrigin: "anonymous",
},
];
src: "https://unpkg.com/htmx.org@1.9.6",
integrity: "sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni",
crossOrigin: 'anonymous",
preload: true, // use it to render a <link rel="preload"> for this script
}
],
};
```
// and export it through the handle, you could also create it inline here
// if you don't care about the type
export let handle = { scripts };
You can also import `ExternalScriptsFunction` and `ScriptDescriptor` interfaces yourself to build a custom handle type.
```ts
import {
ExternalScriptsFunction,
ScriptDescriptor,
} from "remix-utils/external-scripts";
interface AppHandle<LoaderData = unknown> {
scripts?: ExternalScriptsFunction<LoaderData> | ScriptDescriptor[];
}
export let handle: AppHandle<LoaderData> = {
scripts, // define scripts as a function or array here
};
```
Then, in the root route, add the `ExternalScripts` component together with the Remix's Scripts component, usually inside a Document component.
Or you can extend the `ExternalScriptsHandle` interface.
```ts
import { ExternalScriptsHandle } from "remix-utils/external-scripts";
interface AppHandle<LoaderData = unknown>
extends ExternalScriptsHandle<LoaderData> {
// more handle properties here
}
export let handle: AppHandle<LoaderData> = {
scripts, // define scripts as a function or array here
};
```
---
Then, in the root route, add the `ExternalScripts` component somewhere, usually you want to load it either inside `<head>` or at the bottom of `<body>`, either before or after the Remix's `<Scripts>` component.
Where exactly to place `<ExternalScripts />` will depend on your app, but a safe place is at the end of `<body>`.
```tsx
import { Links, LiveReload, Meta, Scripts, ScrollRestoration } from "remix";
import { ExternalScripts } from "remix-utils";
import { ExternalScripts } from "remix-utils/external-scripts";

@@ -566,12 +704,36 @@ type Props = { children: React.ReactNode; title?: string };

Now, any script you defined in the ScriptsFunction will be added to the HTML together with a `<link rel="preload">` before it.
Now, any script you defined in the ScriptsFunction will be added to the HTML.
> Tip: You could use it together with useShouldHydrate to disable Remix scripts in certain routes but still load scripts for analytics or small features that need JS but don't need the full app JS to be enabled.
You could use this util together with `useShouldHydrate` to disable Remix scripts in certain routes but still load scripts for analytics or small features that need JS but don't need the full app JS to be enabled.
```tsx
let shouldHydrate = useShouldHydrate();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{title ? <title>{title}</title> : null}
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
{shouldHydrate ? <Scripts /> : <ExternalScripts />}
<LiveReload />
</body>
</html>
);
```
### useGlobalNavigationState
> **Note**: This depends on `react`, and `@remix-run/react`.
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";
import { useGlobalNavigationState } from "remix-utils/use-global-navigation-state";

@@ -599,6 +761,8 @@ export function GlobalPendingUI() {

> **Note**: This depends on `react`, and `@remix-run/react`.
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";
import { useGlobalPendingState } from "remix-utils/use-global-navigation-state";

@@ -621,6 +785,8 @@ export function GlobalPendingUI() {

> **Note**: This depends on `react`, and `@remix-run/react`.
This hook lets you know if the global transition or if one of any active fetchers is submitting.
```ts
import { useGlobalSubmittingState } from "remix-utils";
import { useGlobalSubmittingState } from "remix-utils/use-global-navigation-state";

@@ -639,6 +805,8 @@ export function GlobalPendingUI() {

> **Note**: This depends on `react`, and `@remix-run/react`.
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";
import { useGlobalLoadingState } from "remix-utils/use-global-navigation-state";

@@ -657,2 +825,4 @@ export function GlobalPendingUI() {

> **Note**: This depends on `react`.
This hook lets you detect if your component is already hydrated. This means the JS for the element loaded client-side and React is running.

@@ -663,3 +833,3 @@

```ts
import { useHydrated } from "remix-utils";
import { useHydrated } from "remix-utils/use-hydrated";

@@ -683,2 +853,4 @@ export function Component() {

> **Note**: This depends on `react`.
This hooks lets you get the locales returned by the root loader. It follows a simple convention, your root loader return value should be an objet with the key `locales`.

@@ -689,2 +861,5 @@

```ts
import { useLocales } from "remix-utils/use-locales";
import { getClientLocales } from "remix-utils/get-client-locales";
// in the root loader

@@ -710,2 +885,4 @@ export async function loader({ request }: LoaderArgs) {

> **Note**: This depends on `@remix-run/react`.
If you are building a Remix application where most routes are static, and you want to avoid loading client-side JS, you can use this hook, plus some conventions, to detect if one or more active routes needs JS and only render the Scripts component in that case.

@@ -718,3 +895,3 @@

import { Links, LiveReload, Meta, Scripts } from "@remix-run/react";
import { useShouldHydrate } from "remix-utils";
import { useShouldHydrate } from "remix-utils/use-should-hydrate";

@@ -769,5 +946,9 @@ interface DocumentProps {

> **Note**: This depends on `is-ip`.
This function receives a Request or Headers objects and will try to get the IP address of the client (the user) who originated the request.
```ts
import { getClientIPAddress } from "remix-utils/get-client-ip-address";
export async function loader({ request }: LoaderArgs) {

@@ -781,3 +962,3 @@ // using the request

If it can't find he ipAddress the return value will be `null`. Remember to check if it was able to find it before using it.
If it can't find he IP address the return value will be `null`. Remember to check if it was able to find it before using it.

@@ -804,5 +985,9 @@ The function uses the following list of headers, in order of preference:

> **Note**: This depends on `intl-parse-accept-language`.
This function let you get the locales of the client (the user) who originated the request.
```ts
import { getClientLocales } from "remix-utils/get-client-locales";
export async function loader({ request }: LoaderArgs) {

@@ -821,3 +1006,3 @@ // using the request

```ts
import { getClientLocales } from "remix-utils";
import { getClientLocales } from "remix-utils/get-client-locales";
export async function loader({ request }: LoaderArgs) {

@@ -844,2 +1029,4 @@ let locales = getClientLocales(request);

```ts
import { isPrefetch } from "remix-utils/is-prefetch";
export async function loader({ request }: LoaderArgs) {

@@ -866,6 +1053,6 @@ let data = await getData(request);

```ts
import { redirectBack } from "remix-utils";
import { redirectBack } from "remix-utils/redirect-back";
export async function action({ request }: ActionArgs) {
return redirectBack(request, { fallback: "/" });
throw redirectBack(request, { fallback: "/" });
}

@@ -876,90 +1063,2 @@ ```

#### Created
Helper function to create a Created (201) response with a JSON body.
```ts
import { created } from "remix-utils";
export async function action({ request }: ActionArgs) {
let result = await doSomething(request);
return created(result);
}
```
#### Bad Request
Helper function to create a Bad Request (400) response with a JSON body.
```ts
import { badRequest } from "remix-utils";
export async function action() {
throw badRequest({ message: "You forgot something in the form." });
}
```
#### Unauthorized
Helper function to create an Unauthorized (401) response with a JSON body.
```ts
import { unauthorized } from "remix-utils";
export async function loader() {
// usually what you really want is to throw a redirect to the login page
throw unauthorized({ message: "You need to login." });
}
```
#### Forbidden
Helper function to create a Forbidden (403) response with a JSON body.
```ts
import { forbidden } from "remix-utils";
export async function loader() {
throw forbidden({ message: "You don't have access for this." });
}
```
#### Not Found
Helper function to create a Not Found (404) response with a JSON body.
```ts
import { notFound } from "remix-utils";
export async function loader() {
throw notFound({ message: "This doesn't exist." });
}
```
#### Unprocessable Entity
Helper function to create an Unprocessable Entity (422) response with a JSON body.
```ts
import { unprocessableEntity } from "remix-utils";
export async function loader() {
throw unprocessableEntity({ message: "This doesn't exists." });
}
```
This is used by the CSRF validation. You probably don't want to use it directly.
#### Server Error
Helper function to create a Server Error (500) response with a JSON body.
```ts
import { serverError } from "remix-utils";
export async function loader() {
throw serverError({ message: "Something unexpected happened." });
}
```
#### Not Modified

@@ -970,2 +1069,4 @@

```ts
import { notModified } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -983,2 +1084,4 @@ return notModified();

```ts
import { javascript } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -996,2 +1099,4 @@ return javascript("console.log('Hello World')");

```ts
import { stylesheet } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -1009,2 +1114,4 @@ return stylesheet("body { color: red; }");

```ts
import { pdf } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -1022,2 +1129,4 @@ return pdf(await generatePDF(request.formData()));

```ts
import { html } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -1035,2 +1144,4 @@ return html("<h1>Hello World</h1>");

```ts
import { xml } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -1041,3 +1152,3 @@ return xml("<?xml version='1.0'?><catalog></catalog>");

#### TXT
#### Plain Text

@@ -1049,2 +1160,4 @@ Helper function to create a TXT file response with any header.

```ts
import { txt } from "remix-utils/responses";
export async function loader({ request }: LoaderArgs) {

@@ -1060,2 +1173,4 @@ return txt(`

> **Note**: This depends on `zod`, and a Remix server runtime.
Cookie objects in Remix allows any type, the typed cookies from Remix Utils lets you use Zod to parse the cookie values and ensure they conform to a schema.

@@ -1065,3 +1180,3 @@

import { createCookie } from "@remix-run/node";
import { createTypedCookie } from "remix-utils";
import { createTypedCookie } from "remix-utils/typed-cookie";
import { z } from "zod";

@@ -1154,2 +1269,4 @@

> **Note**: This depends on `zod`, and a Remix server runtime.
Session objects in Remix allows any type, the typed sessions from Remix Utils lets you use Zod to parse the session data and ensure they conform to a schema.

@@ -1159,3 +1276,3 @@

import { createCookieSessionStorage } from "@remix-run/node";
import { createTypedSessionStorage } from "remix-utils";
import { createTypedSessionStorage } from "remix-utils/typed-session";
import { z } from "zod";

@@ -1219,2 +1336,4 @@

> **Note**: This depends on `react`.
Server-Sent Events are a way to send data from the server to the client without the need for the client to request it. This is useful for things like chat applications, live updates, and more.

@@ -1231,3 +1350,3 @@

// app/routes/sse.time.ts
import { eventStream } from "remix-utils";
import { eventStream } from "remix-utils/event-stream";

@@ -1251,3 +1370,3 @@ export async function loader({ request }: LoaderArgs) {

// app/components/counter.ts
import { useEventSource } from "remix-utils";
import { useEventSource } from "remix-utils/use-event-source";

@@ -1295,2 +1414,4 @@ function Counter() {

> **Note**: This depends on `zod`, and a Remix server runtime.
Rolling cookies allows you to prolong the expiration of a cookie by updating the expiration date of every cookie.

@@ -1303,3 +1424,3 @@

```ts
import { rollingCookie } from "remix-utils";
import { rollingCookie } from "remix-utils/rolling-cookie";

@@ -1312,3 +1433,3 @@ import { sessionCookie } from "~/session.server";

responseHeaders: Headers,
remixContext: EntryContext
remixContext: EntryContext,
) {

@@ -1322,3 +1443,3 @@ await rollingCookie(sessionCookie, request, responseHeaders);

responseHeaders,
remixContext
remixContext,
)

@@ -1329,3 +1450,3 @@ : handleBrowserRequest(

responseHeaders,
remixContext
remixContext,
);

@@ -1338,8 +1459,10 @@ }

```ts
import { rollingCookie } from "remix-utils/rolling-cookie";
export let handleDataRequest: HandleDataRequestFunction = async (
response: Response,
{ request }
{ request },
) => {
let cookieValue = await sessionCookie.parse(
responseHeaders.get("set-cookie")
responseHeaders.get("set-cookie"),
);

@@ -1350,3 +1473,3 @@ if (!cookieValue) {

"Set-Cookie",
await sessionCookie.serialize(cookieValue)
await sessionCookie.serialize(cookieValue),
);

@@ -1363,6 +1486,8 @@ }

> **Note**: This depends on a Remix server runtime.
It's common to need to handle more than one action in the same route, there are many options here like [sending the form to a resource route](https://sergiodxa.com/articles/multiple-forms-per-route-in-remix#using-resource-routes) or using an [action reducer](https://sergiodxa.com/articles/multiple-forms-per-route-in-remix#the-action-reducer-pattern), the `namedAction` function uses some conventions to implement the action reducer pattern.
```tsx
import { namedAction } from "remix-utils";
import { namedAction } from "remix-utils/named-action";

@@ -1447,6 +1572,6 @@ export async function action({ request }: ActionArgs) {

headers: Headers,
context: EntryContext
context: EntryContext,
) {
let markup = renderToString(
<RemixServer context={context} url={request.url} />
<RemixServer context={context} url={request.url} />,
);

@@ -1573,3 +1698,3 @@ headers.set("Content-Type", "text/html");

```tsx
import { useDelegatedAnchors } from "remix-utils";
import { useDelegatedAnchors } from "remix-utils/use-delegated-anchors";

@@ -1598,3 +1723,3 @@ export async function loader() {

```tsx
import { PrefetchPageAnchors } from "remix-utils";
import { PrefetchPageAnchors } from "remix-utils/use-delegated-anchors";

@@ -1621,5 +1746,7 @@ export async function loader() {

> **Note**: This depends on `react`, and `@remix-run/react`.
The `useDebounceFetcher` is a wrapper of `useFetcher` that adds debounce support to `fetcher.submit`.
The hook is based on @JacobParis [article](https://www.jacobparis.com/content/use-debounce-fetcher).
The hook is based on [@JacobParis](https://github.com/JacobParis)' [article](https://www.jacobparis.com/content/use-debounce-fetcher).

@@ -1629,3 +1756,3 @@ The main difference with Jacob's version is that Remix Utils' version overwrites `fetcher.submit` instead of appending a `fetcher.debounceSubmit` method.

```tsx
import { useDebounceFetcher } from "remix-utils";
import { useDebounceFetcher } from "remix-utils/use-debounce-fetcher";

@@ -1649,6 +1776,8 @@ export function Component({ data }) {

> **Note**: This depends on `@remix-route/react`.
Derive the value of the deprecated `fetcher.type` from the fetcher and navigation data.
```ts
import { getFetcherType } from "remix-utils";
import { getFetcherType } from "remix-utils/fetcher-type";

@@ -1670,3 +1799,3 @@ function Component() {

```ts
import { useFetcherType } from "remix-utils";
import { useFetcherType } from "remix-utils/fetcher-type";

@@ -1687,3 +1816,3 @@ function Component() {

```ts
import { type FetcherType } from "remix-utils";
import { type FetcherType } from "remix-utils/fetcher-type";

@@ -1702,3 +1831,3 @@ function useCallbackOnDone(type: FetcherType, cb) {

```ts
import { respondTo } from "remix-utils";
import { respondTo } from "remix-utils/respond-to";

@@ -1750,6 +1879,6 @@ export async function loader({ request }: LoaderArgs) {

```ts
import { parseAcceptHeader } from "remix-utils";
import { parseAcceptHeader } from "remix-utils/parse-accept-header";
let parsed = parseAcceptHeader(
"text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/*, */*;q=0.8"
"text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/*, */*;q=0.8",
);

@@ -1762,2 +1891,92 @@ ```

### Form Honeypot
> **Note**: This depends on `react` and `crypto-js`.
Honeypot is a simple technic to prevent spam bots from submitting forms, it works by adding a hidden field to the form that bots will fill, but humans won't.
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";
// Create a new Honeypot instance, the values here are the defaults, you can
// customize them
export const honeypot = new Honeypot({
randomizeNameFieldName: false,
nameFieldName: "name__confirm",
validFromFieldName: "from__confirm", // null to disable it
encryptionSeed: undefined, // Ideally it should be unique even between processes
});
```
Then, in your `app/root` loader, call `honeypot.getInputProps()` and return it.
```ts
// app/root.tsx
import { honeypot } from "~/honeypot.server";
export async function loader() {
// more code here
return json({ honeypot: honeypot.getInputProps() });
}
```
And in the `app/root` component render the `HoneypotProvider` component wrapping the rest of the UI.
```tsx
import { HoneypotProvider } from "remix-utils/honeypot-inputs";
export default function Component() {
// more code here
return (
// some JSX
<HoneypotProvider>
<Outlet />
</HoneypotProvider>
// end that JSX
);
}
```
Now, in every public form you want protect against spam (like a login form), render the `HoneypotInputs` component.
```tsx
import { HoneypotInputs } from "remix-utils/honeypot-inputs";
function SomePublicForm() {
return (
<Form method="post">
<HoneypotInputs label="Please leave this field blank" />
{/* more inputs and some buttons */}
</Form>
);
}
```
> **Note**: The label value above is the default one, use it to allow the label to be localized, or remove it if you don't want to change it.
Finally, in the action the form submits to, you can call `honeypot.check`.
```ts
import { SpamError } from "remix-utils/honeypot";
import { honeypot } from "~/honeypot.server";
export async function action({ request }) {
let formData = await request.formData();
try {
honeypot.check(formData);
} catch (error) {
if (error instanceof SpamError) {
// handle spam requests here
}
// handle any other possible error here, e.g. re-throw since nothing else
// should be thrown
}
// the rest of your action
}
```
## Author

@@ -1764,0 +1983,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc