Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jobs:

- name: Build
run: pnpm turbo build --filter "./packages/*"

- name: Install Playwright
run: pnpm exec playwright install chromium

- name: Test
run: pnpm run test
env:
Expand Down
2 changes: 1 addition & 1 deletion examples/backend-adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test": "playwright test"
},
"dependencies": {
"@playwright/test": "1.45.0",
"@playwright/test": "1.49.1",
"@uploadthing/react": "7.1.5",
"concurrently": "^8.2.2",
"typescript": "^5.5.2",
Expand Down
15 changes: 5 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,24 @@
"@effect/vitest": "0.13.15",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@manypkg/cli": "^0.21.3",
"@playwright/test": "1.45.0",
"@prettier/sync": "^0.5.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@types/bun": "^1.1.5",
"@types/node": "^20.14.0",
"@uploadthing/eslint-config": "workspace:*",
"@vitest/coverage-v8": "^2.1.2",
"happy-dom": "^13.6.2",
"msw": "2.2.13",
"@vitest/browser": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8",
"msw": "2.7.0",
"playwright": "1.49.1",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"turbo": "2.3.3",
"typescript": "^5.5.2",
"uploadthing": "workspace:*",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.2"
"vitest": "^2.1.8"
},
"pnpm": {
"patchedDependencies": {
"[email protected]": "patches/[email protected]",
"@mswjs/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]"
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/expo/src/document-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import * as DocumentPicker from "expo-document-picker";
import type { UseUploadthingProps } from "@uploadthing/react";
import { __useUploadThingInternal } from "@uploadthing/react/native";
import { generatePermittedFileTypes } from "@uploadthing/shared";
import type { ExpandedRouteConfig, ExtendObjectIf } from "@uploadthing/shared";
import type {
ExpandedRouteConfig,
ExtendObjectIf,
FetchEsque,
} from "@uploadthing/shared";
import type { FileRouter } from "uploadthing/server";
import type { inferEndpointInput } from "uploadthing/types";

Expand Down Expand Up @@ -33,6 +37,7 @@ export const GENERATE_useDocumentUploader = <
TRouter extends FileRouter,
>(initOpts: {
url: URL;
fetch: FetchEsque;
}) => {
const useDocumentUploader = <TEndpoint extends keyof TRouter>(
endpoint: TEndpoint,
Expand All @@ -41,6 +46,7 @@ export const GENERATE_useDocumentUploader = <
const { routeConfig, startUpload, isUploading } = __useUploadThingInternal(
initOpts.url,
endpoint,
initOpts.fetch,
opts,
);
const { mimeTypes, multiple } = useMemo(
Expand Down
8 changes: 7 additions & 1 deletion packages/expo/src/image-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import * as ImagePicker from "expo-image-picker";
import type { UseUploadthingProps } from "@uploadthing/react";
import { __useUploadThingInternal } from "@uploadthing/react/native";
import { generatePermittedFileTypes } from "@uploadthing/shared";
import type { ExpandedRouteConfig, ExtendObjectIf } from "@uploadthing/shared";
import type {
ExpandedRouteConfig,
ExtendObjectIf,
FetchEsque,
} from "@uploadthing/shared";
import type { FileRouter } from "uploadthing/server";
import type { inferEndpointInput } from "uploadthing/types";

Expand All @@ -25,6 +29,7 @@ export const GENERATE_useImageUploader = <
TRouter extends FileRouter,
>(initOpts: {
url: URL;
fetch: FetchEsque;
}) => {
const useImageUploader = <TEndpoint extends keyof TRouter>(
endpoint: TEndpoint,
Expand All @@ -33,6 +38,7 @@ export const GENERATE_useImageUploader = <
const { routeConfig, startUpload, isUploading } = __useUploadThingInternal(
initOpts.url,
endpoint,
initOpts.fetch,
opts,
);
const { mediaTypes, multiple } = useMemo(
Expand Down
33 changes: 30 additions & 3 deletions packages/expo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Constants from "expo-constants";

import { generateReactHelpers } from "@uploadthing/react/native";
import type { FetchEsque } from "@uploadthing/shared";
import { warnIfInvalidPeerDependency } from "@uploadthing/shared";
import { version as uploadthingClientVersion } from "uploadthing/client";
import type { FileRouter } from "uploadthing/types";
Expand All @@ -20,6 +21,25 @@ export interface GenerateTypedHelpersOptions {
* @default (process.env.EXPO_PUBLIC_SERVER_ORIGIN ?? ExpoConstants.debuggerHost) + "/api/uploadthing"
*/
url?: URL | string;
/**
* Provide a custom fetch implementation.
* @default `globalThis.fetch`
* @example
* ```ts
* fetch: (input, init) => {
* if (input.toString().startsWith(MY_SERVER_URL)) {
* // Include cookies in the request to your API
* return fetch(input, {
* ...init,
* credentials: "include",
* });
* }
*
* return fetch(input, init);
* }
* ```
*/
fetch?: FetchEsque | undefined;
}

export const generateReactNativeHelpers = <TRouter extends FileRouter>(
Expand Down Expand Up @@ -48,9 +68,16 @@ export const generateReactNativeHelpers = <TRouter extends FileRouter>(
);
}

const vanillaHelpers = generateReactHelpers<TRouter>({ ...initOpts, url });
const useImageUploader = GENERATE_useImageUploader<TRouter>({ url });
const useDocumentUploader = GENERATE_useDocumentUploader<TRouter>({ url });
const fetch = initOpts?.fetch ?? globalThis.fetch;
const opts = {
...initOpts,
url,
fetch,
};

const vanillaHelpers = generateReactHelpers<TRouter>(opts);
const useImageUploader = GENERATE_useImageUploader<TRouter>(opts);
const useDocumentUploader = GENERATE_useDocumentUploader<TRouter>(opts);

return { ...vanillaHelpers, useImageUploader, useDocumentUploader };
};
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@
"@types/react-dom": "18.3.0",
"@uploadthing/eslint-config": "workspace:*",
"@uploadthing/tsconfig": "workspace:*",
"@vitest/browser": "2.1.2",
"bunchee": "^6.1.2",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
Expand All @@ -90,6 +89,7 @@
"tailwindcss": "^3.4.16",
"typescript": "^5.5.2",
"uploadthing": "workspace:*",
"vitest-browser-react": "0.0.4",
"wait-on": "^7.2.0",
"zod": "^3.23.8"
},
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export function UploadButton<
const { startUpload, isUploading, routeConfig } = __useUploadThingInternal(
resolveMaybeUrlArg($props.url),
$props.endpoint,
$props.fetch ?? globalThis.fetch,
{
signal: acRef.current.signal,
headers: $props.headers,
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export function UploadDropzone<
const { startUpload, isUploading, routeConfig } = __useUploadThingInternal(
resolveMaybeUrlArg($props.url),
$props.endpoint,
$props.fetch ?? globalThis.fetch,
{
signal: acRef.current.signal,
headers: $props.headers,
Expand Down
21 changes: 18 additions & 3 deletions packages/react/src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@ export const generateUploadButton = <TRouter extends FileRouter>(
);

const url = resolveMaybeUrlArg(opts?.url);
const fetch = opts?.fetch ?? globalThis.fetch;

const TypedButton = <TEndpoint extends keyof TRouter>(
props: Omit<
UploadButtonProps<TRouter, TEndpoint>,
keyof GenerateTypedHelpersOptions
>,
) => <UploadButton<TRouter, TEndpoint> {...(props as any)} url={url} />;
) => (
<UploadButton<TRouter, TEndpoint>
{...(props as any)}
url={url}
fetch={fetch}
/>
);
return TypedButton;
};

Expand All @@ -54,7 +61,13 @@ export const generateUploadDropzone = <TRouter extends FileRouter>(
UploadDropzoneProps<TRouter, TEndpoint>,
keyof GenerateTypedHelpersOptions
>,
) => <UploadDropzone<TRouter, TEndpoint> {...(props as any)} url={url} />;
) => (
<UploadDropzone<TRouter, TEndpoint>
{...(props as any)}
url={url}
fetch={fetch}
/>
);
return TypedDropzone;
};

Expand All @@ -74,6 +87,8 @@ export const generateUploader = <TRouter extends FileRouter>(
UploadthingComponentProps<TRouter, TEndpoint>,
keyof GenerateTypedHelpersOptions
>,
) => <Uploader<TRouter, TEndpoint> {...(props as any)} url={url} />;
) => (
<Uploader<TRouter, TEndpoint> {...(props as any)} url={url} fetch={fetch} />
);
return TypedUploader;
};
39 changes: 39 additions & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
ClassListMerger,
ErrorMessage,
ExtendObjectIf,
FetchEsque,
MaybePromise,
UploadThingError,
} from "@uploadthing/shared";
Expand All @@ -26,6 +27,25 @@ export interface GenerateTypedHelpersOptions {
* @default (VERCEL_URL ?? window.location.origin) + "/api/uploadthing"
*/
url?: string | URL;
/**
* Provide a custom fetch implementation.
* @default `globalThis.fetch`
* @example
* ```ts
* fetch: (input, init) => {
* if (input.toString().startsWith(MY_SERVER_URL)) {
* // Include cookies in the request to your API
* return fetch(input, {
* ...init,
* credentials: "include",
* });
* }
*
* return fetch(input, init);
* }
* ```
*/
fetch?: FetchEsque | undefined;
}

export type UseUploadthingProps<
Expand Down Expand Up @@ -122,6 +142,25 @@ export type UploadthingComponentProps<
* @default (VERCEL_URL ?? window.location.origin) + "/api/uploadthing"
*/
url?: string | URL;
/**
* Provide a custom fetch implementation.
* @default `globalThis.fetch`
* @example
* ```ts
* fetch: (input, init) => {
* if (input.toString().startsWith(MY_SERVER_URL)) {
* // Include cookies in the request to your API
* return fetch(input, {
* ...init,
* credentials: "include",
* });
* }
*
* return fetch(input, init);
* }
* ```
*/
fetch?: FetchEsque | undefined;
config?: {
mode?: "auto" | "manual";
appendOnPaste?: boolean;
Expand Down
12 changes: 10 additions & 2 deletions packages/react/src/useUploadThing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRef, useState } from "react";
import type {
EndpointMetadata,
ExpandedRouteConfig,
FetchEsque,
} from "@uploadthing/shared";
import {
INTERNAL_DO_NOT_USE__fatalClientError,
Expand Down Expand Up @@ -30,14 +31,17 @@ import useFetch from "./utils/useFetch";

declare const globalThis: {
__UPLOADTHING?: EndpointMetadata;
fetch: FetchEsque;
};

const useRouteConfig = (
fetch: FetchEsque,
url: URL,
endpoint: string,
): ExpandedRouteConfig | undefined => {
const maybeServerData = globalThis.__UPLOADTHING;
const { data } = useFetch<EndpointMetadata>(
fetch,
// Don't fetch if we already have the data
maybeServerData ? undefined : url.href,
);
Expand All @@ -55,9 +59,11 @@ export function __useUploadThingInternal<
>(
url: URL,
endpoint: EndpointArg<TRouter, TEndpoint>,
fetch: FetchEsque,
opts?: UseUploadthingProps<TRouter[TEndpoint]>,
) {
const { uploadFiles, routeRegistry } = genUploader<TRouter>({
fetch,
url,
package: "@uploadthing/react",
});
Expand Down Expand Up @@ -134,7 +140,7 @@ export function __useUploadThingInternal<
});

const _endpoint = unwrap(endpoint, routeRegistry);
const routeConfig = useRouteConfig(url, _endpoint as string);
const routeConfig = useRouteConfig(fetch, url, _endpoint as string);

return {
startUpload,
Expand All @@ -152,18 +158,20 @@ export const generateReactHelpers = <TRouter extends FileRouter>(
uploadthingClientVersion,
);

const fetch = initOpts?.fetch ?? globalThis.fetch;
const url = resolveMaybeUrlArg(initOpts?.url);

const clientHelpers = genUploader<TRouter>({
url,
package: "@uploadthing/react",
fetch,
});

function useUploadThing<TEndpoint extends keyof TRouter>(
endpoint: EndpointArg<TRouter, TEndpoint>,
opts?: UseUploadthingProps<TRouter[TEndpoint]>,
) {
return __useUploadThingInternal(url, endpoint, opts);
return __useUploadThingInternal(url, endpoint, fetch, opts);
}

function getRouteConfig(slug: EndpointArg<TRouter, keyof TRouter>) {
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/utils/useFetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Ripped from https://usehooks-ts.com/react-hook/use-fetch
import { useEffect, useReducer, useRef } from "react";

import type { FetchEsque } from "@uploadthing/shared";
import { safeParseJSON } from "@uploadthing/shared";

interface State<T> {
Expand All @@ -16,7 +17,11 @@ type Action<T> =
| { type: "fetched"; payload: T }
| { type: "error"; payload: Error };

function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
function useFetch<T = unknown>(
fetch: FetchEsque,
url?: string,
options?: RequestInit,
): State<T> {
const cache = useRef<Cache<T>>({});

// Used to prevent state update if the component is unmounted
Expand Down
Loading
Loading