Skip to content

Conversation

@juliusmarminge
Copy link
Member

@juliusmarminge juliusmarminge commented Apr 5, 2025

Summary by CodeRabbit

  • New Features

    • Introduced an experimental, advanced client-side file upload API with typed support, pause, resume, abort, and progress tracking.
    • Added React components and pages showcasing multiple upload workflows, including controlled and asynchronous uploads.
    • Provided a custom React hook for drag-and-drop file uploads with preview and state management.
    • Expanded backend upload routes with distinct private, public, and image upload configurations enforcing session validation.
  • Enhancements

    • Refactored button and file card components to use a scalable variant and size styling system via class-variance-authority.
    • Extended global styles with comprehensive CSS variables for colors, radii, theming, and animations.
    • Added utility exports for file size formatting and content labeling.
  • Dependency Updates

    • Replaced "clsx" with "class-variance-authority" and added "lucide-react" and "tailwind-merge" for enhanced styling and icons.
    • Updated "class-variance-authority" dependency to version ^0.7.1 in multiple packages.
  • Bug Fixes

    • Corrected upload button to target the updated backend upload route.
  • Chores

    • Increased Node.js memory allocation for build and dev scripts.
    • Updated package build output configurations to include new directories.

@vercel
Copy link

vercel bot commented Apr 5, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
docs-uploadthing ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 1, 2025 9:30am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
legacy-docs-uploadthing ⬜️ Ignored (Inspect) Visit Preview May 1, 2025 9:30am

@changeset-bot
Copy link

changeset-bot bot commented Apr 5, 2025

🦋 Changeset detected

Latest commit: f8c7cd2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
uploadthing Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 5, 2025

Walkthrough

This update introduces a new, fully-typed, and controllable file upload client ("future" uploader) for the UploadThing SDK, including its core implementation, public API, and supporting React hooks and UI examples. It adds new internal and public modules for client-side upload management with pause, resume, and abort capabilities, and event-driven progress tracking. The playground app is extended with new upload pages and components demonstrating both asynchronous and controlled upload flows, as well as comprehensive UI examples using Origin UI. Styling across the playground is refactored to use the class-variance-authority library, and design tokens are added via CSS variables. Several dependency updates and configuration changes support these enhancements.

Changes

File(s) Change Summary
examples/profile-picture/package.json, examples/with-novel/package.json Updated class-variance-authority dependency from ^0.7.0 to ^0.7.1.
packages/uploadthing/package.json Added export for ./client-future, included client-future in files, increased Node.js memory for build/dev scripts.
packages/uploadthing/src/_internal/client-future.ts Added new internal module implementing a comprehensive, event-driven file upload client with error handling, state management, and concurrency.
packages/uploadthing/src/_internal/ut-reporter.ts Made package property in reporter config optional; header set conditionally.
packages/uploadthing/src/client-future.ts Added new public module: typed uploader generator with pause/resume/abort, event-driven uploads, and route registry.
packages/uploadthing/src/client.ts Exported bytesToFileSize and allowedContentTextLabelGenerator from shared utilities.
packages/uploadthing/turbo.json Added client-future/** to build outputs.
playground/app/api/uploadthing/route.ts Replaced single anything route with anyPrivate, anyPublic, and images routes, each with distinct ACLs and middleware.
playground/app/future/page.tsx Added new page with Async and Controlled uploader components using the future API, supporting event-driven uploads and user controls.
playground/app/global.css Added CSS variables for colors, radii, and chart colors; introduced base and theme layers for design tokens and styling.
playground/app/originui/page.tsx Added new page demonstrating multiple upload UI examples with Origin UI, supporting drag-and-drop, previews, and upload controls.
playground/components/button.tsx Refactored Button to use class-variance-authority for variants and sizes, replacing color prop and clsx usage.
playground/components/fieldset.tsx, playground/components/file-card.tsx, playground/components/skeleton.tsx Switched from clsx to class-variance-authority for cx utility. Adjusted button props and classes in file card.
playground/components/uploader.tsx Changed upload API endpoint from anything to anyPrivate.
playground/lib/uploadthing.ts Added new module: React hook for dropzone file uploads, typed uploader instance, and utility exports for file management.
playground/package.json Replaced clsx with class-variance-authority, added lucide-react and tailwind-merge dependencies.

Sequence Diagram(s)

High-level: Future Uploader Flow

sequenceDiagram
    participant User
    participant Uploader (future_genUploader)
    participant Server (UploadThing API)
    participant Storage (Presigned URL)

    User->>Uploader: Select files & trigger upload
    Uploader->>Server: Request presigned URLs for files
    Server-->>Uploader: Return presigned URLs
    Uploader->>User: Emit presignedReceived event
    loop For each file (concurrent, max 6)
        Uploader->>Storage: PUT file data (with progress)
        Storage-->>Uploader: Respond (success or error)
        Uploader->>User: Emit progress/completed/failed events
    end
    Uploader->>User: All uploads done (done() promise resolves)
Loading

Controlled Upload: Pause/Resume/Abort

sequenceDiagram
    participant User
    participant Uploader
    participant Storage

    User->>Uploader: Start upload
    Uploader->>Storage: PUT file data (upload in progress)
    User->>Uploader: Pause upload
    Uploader--xStorage: Abort request (pause, no rejection)
    User->>Uploader: Resume upload
    Uploader->>Storage: Re-initiate PUT request
    User->>Uploader: Abort upload
    Uploader--xStorage: Abort request (throw UploadAbortedError)
Loading

Suggested labels

examples, @uploadthing/react, sdk

Suggested reviewers

  • markflorkowski
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Member Author

juliusmarminge commented Apr 5, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

@juliusmarminge juliusmarminge changed the title initial impl of new API initial impl of new uploadFiles API Apr 5, 2025
@juliusmarminge juliusmarminge changed the title initial impl of new uploadFiles API feat: initial impl of new uploadFiles API Apr 5, 2025
@juliusmarminge juliusmarminge changed the title feat: initial impl of new uploadFiles API feat: new uploadFiles API Apr 5, 2025
@pkg-pr-new
Copy link

pkg-pr-new bot commented Apr 5, 2025

@github-actions
Copy link
Contributor

github-actions bot commented Apr 5, 2025

📦 Bundle size comparison

Bundle Size (gzip) Visualization
Main 30.53KB See Treemap 📊
PR (16f3699) 30.55KB See Treemap 📊
Diff ↑15.00B

* Upload a file to the storage provider
* Throughout the upload, the file's status and progress will be updated
* @remarks This function never rejects
* @internal
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't implemented the public-facing uploadFiles yet, which would take just File[], transition to PendingFile and request the presigned URLs

Technically we could have this be public and also export a function to manually get the predesigned URLs

@juliusmarminge juliusmarminge force-pushed the future-uploadFiles branch 6 times, most recently from 290fb78 to f1c75b8 Compare May 1, 2025 09:17
@juliusmarminge juliusmarminge marked this pull request as ready for review May 1, 2025 09:20
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
playground/app/api/uploadthing/route.ts (1)

17-19: Naming consideration for anyPrivate route.

The route name anyPrivate follows the naming pattern established in this file, but there was a previous comment suggesting this name could be reconsidered.

playground/app/originui/page.tsx (1)

179-184: Preview URLs leak memory across the entire Origin-UI page

Every component here relies on the preview object URL produced by
useUploadThingDropzone, but none of them ever revokes it. Because users
can add/remove hundreds of files in a single session, un-revoked blob URLs
quickly exhaust memory.

Once you fix the leak inside the hook (see previous comment) these
components will inherit the benefit automatically. Just ensure you upgrade
the hook everywhere.

🧹 Nitpick comments (10)
playground/app/global.css (3)

6-40: Consider fallbacks for OKLCH color values.

Oklch is cutting-edge and not universally supported. To ensure graceful degradation, you might provide fallback formats (e.g. hex or rgb) alongside the oklch() declarations:

--background: oklch(1 0 0); /* OKLCH */
--background: #ffffff;      /* HEX fallback */

This will improve cross-browser compatibility.


42-91: Plan for theming beyond inline defaults.

The @theme inline block maps your root tokens into --color-*. If you intend to support dark mode or multiple themes, consider grouping alternative tokens (e.g. @theme dark { … }) rather than only inline defaults.


93-100: Scope global border and outline resets.

Applying @apply border-border outline-ring/50 to * can bloat your generated CSS and may override third-party resets unexpectedly. Consider limiting this to focusable elements or specific components:

*:focus { @apply outline-ring/50; }

This will reduce CSS size and avoid styling conflicts.

playground/app/api/uploadthing/route.ts (1)

17-34: Significant code duplication between upload routes.

The anyPrivate and anyPublic routes have identical implementation except for the ACL setting. This duplication increases maintenance burden as any changes need to be applied in multiple places.

Consider extracting the common logic into a helper function that takes the ACL value as a parameter:

-  anyPrivate: fileRoute({
-    blob: { maxFileSize: "256MB", maxFileCount: 10, acl: "private" },
-  })
-    .input(z.object({}))
-    .middleware(async (opts) => {
-      const session = await getSession();
-      if (!session) {
-        throw new UploadThingError("Unauthorized");
-      }
-
-      console.log("middleware ::", session.sub, opts.input);
-
-      return {};
-    })
-    .onUploadComplete(async (opts) => {
-      console.log("Upload complete", opts.file);
-      revalidateTag(CACHE_TAGS.LIST_FILES);
-    }),
-
-  anyPublic: fileRoute({
-    blob: { maxFileSize: "256MB", maxFileCount: 10, acl: "public-read" },
-  })
-    .input(z.object({}))
-    .middleware(async (opts) => {
-      const session = await getSession();
-      if (!session) {
-        throw new UploadThingError("Unauthorized");
-      }
-
-      console.log("middleware ::", session.sub, opts.input);
-
-      return {};
-    })
-    .onUploadComplete(async (opts) => {
-      console.log("Upload complete", opts.file);
-      revalidateTag(CACHE_TAGS.LIST_FILES);
-    }),
+  // Helper function to create a blob upload route with the specified ACL
+  const createBlobRoute = (acl: "private" | "public-read") => 
+    fileRoute({
+      blob: { maxFileSize: "256MB", maxFileCount: 10, acl },
+    })
+      .input(z.object({}))
+      .middleware(async (opts) => {
+        const session = await getSession();
+        if (!session) {
+          throw new UploadThingError("Unauthorized");
+        }
+  
+        console.log("middleware ::", session.sub, opts.input);
+  
+        return {};
+      })
+      .onUploadComplete(async (opts) => {
+        console.log("Upload complete", opts.file);
+        revalidateTag(CACHE_TAGS.LIST_FILES);
+      });
+
+  anyPrivate: createBlobRoute("private"),
+  anyPublic: createBlobRoute("public-read"),

Also applies to: 36-53

playground/app/future/page.tsx (2)

70-88: startTransition should wrap state updates, not async work

startTransition is intended for render-only updates. Awaiting an async
function inside the transition blocks the “concurrent” benefit and can
throw unhandled promises if the component unmounts mid-await.

-  const handleSubmit = (e: React.FormEvent) => {
-
-    startTransition(async () => {
-      setIsUploading(true);
-      const controls = await future_createUpload("anyPrivate", { … });
-      setUploadControls(controls);
-    });
-  };
+  const handleSubmit = async (e: React.FormEvent) => {
+
+    setIsUploading(true);
+    const controls = await future_createUpload("anyPrivate", { … });
+    // wrap *only* the state writes
+    startTransition(() => setUploadControls(controls));
+  };

Keeps the async work out of the transition and still batches the state
updates.


160-168: Division by zero guard for progress calculation

If, for any reason, file.size comes back as 0 the progress bar will throw
NaN and collapse the layout.

- style={{ width: `${(file.sent / file.size) * 100}%` }}
+ const pct = file.size ? (file.sent / file.size) * 100 : 0;
+
+ style={{ width: `${pct}%` }}

Minor, but avoids edge-case crashes.

packages/uploadthing/src/client-future.ts (2)

173-176: abortUpload leaks control flow via synchronous throw

abortUpload both aborts the XHR via AbortController and then immediately throws an UploadAbortedError().
Because this throw is synchronous, any caller that forgets to wrap the call in try/ catch will crash the UI thread before the deferred promises settle.

Prefer returning the error (or resolving the done() promise with a failed state) and keep abortUpload side-effect-only:

-      // Abort the upload
-      throw new UploadAbortedError();
+      // Aborted; caller can detect via the deferred(s) rejecting with UploadAbortedError
+      return;

If a hard exception is desired, document it prominently in JSDoc so integrators know they must guard the call.


213-232: Replace void in the union return type with undefined

Biome flags void inside a union as confusing. void widens to undefined | null | void 0 at the type level and makes narrowing harder. The API already treats “no single file specified” as an omitted parameter, where undefined is the natural sentinel.

-const done = async <T extends AnyFile<TRoute[TEndpoint]> | void = void>(
+const done = async <T extends AnyFile<TRoute[TEndpoint]> | undefined = undefined>(

Resulting inference stays identical while satisfying the linter and preventing accidental null acceptance.

🧰 Tools
🪛 Biome (1.9.4)

[error] 213-213: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

packages/uploadthing/src/_internal/client-future.ts (2)

384-388: HEAD probe for every file can be expensive – consider conditional reuse

A HEAD request is fired per file to fetch x-ut-range-start.
For large batches (dozens of files) this doubles the request count and increases latency on high-RTT networks.

Possible optimisations:

  1. Add a feature flag in the presigned payload (supports-resume) and skip the probe when it is 0 (fresh uploads).
  2. Bundle range information together with the presigned URL response to remove the extra RTT entirely.

These changes preserve resumability while halving the number of network trips.


460-468: "uri" in file runtime check lacks type-safety

The in operator is used to detect React-Native File shims.
Because file is mutated in place to gain extra properties (status, sent, …) the structural check might collide in the future.

Suggestion: narrow via a branded interface:

interface RNFileLike extends File {
  uri: string;
}

and cast only when file as unknown as RNFileLike after verifying typeof (file as any).uri === "string".

This avoids false positives when other URIs (e.g., metadata) are added to the enhanced file type.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7ceef0 and f1c75b8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • examples/profile-picture/package.json (1 hunks)
  • examples/with-novel/package.json (1 hunks)
  • packages/uploadthing/package.json (3 hunks)
  • packages/uploadthing/src/_internal/client-future.ts (1 hunks)
  • packages/uploadthing/src/_internal/ut-reporter.ts (2 hunks)
  • packages/uploadthing/src/client-future.ts (1 hunks)
  • packages/uploadthing/src/client.ts (1 hunks)
  • packages/uploadthing/turbo.json (1 hunks)
  • playground/app/api/uploadthing/route.ts (1 hunks)
  • playground/app/future/page.tsx (1 hunks)
  • playground/app/global.css (1 hunks)
  • playground/app/originui/page.tsx (1 hunks)
  • playground/components/button.tsx (1 hunks)
  • playground/components/fieldset.tsx (1 hunks)
  • playground/components/file-card.tsx (2 hunks)
  • playground/components/skeleton.tsx (1 hunks)
  • playground/components/uploader.tsx (1 hunks)
  • playground/lib/uploadthing.ts (1 hunks)
  • playground/package.json (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
playground/components/file-card.tsx (1)
playground/lib/actions.ts (1)
  • deleteFile (91-106)
playground/components/button.tsx (1)
playground-v6/components/button.tsx (1)
  • Button (11-26)
playground/app/api/uploadthing/route.ts (2)
playground-v6/lib/data.ts (1)
  • getSession (27-31)
playground-v6/lib/const.ts (1)
  • CACHE_TAGS (3-5)
packages/uploadthing/src/_internal/client-future.ts (6)
packages/shared/src/tagged-errors.ts (1)
  • FetchError (63-72)
packages/uploadthing/src/_internal/types.ts (1)
  • UploadPutResult (188-202)
packages/shared/src/effect.ts (2)
  • FetchContext (7-11)
  • fetchEff (20-55)
packages/uploadthing/src/client-future.ts (1)
  • version (38-38)
packages/shared/src/types.ts (1)
  • MaybePromise (14-14)
packages/uploadthing/src/_internal/ut-reporter.ts (1)
  • createUTReporter (45-112)
🪛 Biome (1.9.4)
packages/uploadthing/src/client-future.ts

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


[error] 213-213: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

packages/uploadthing/src/_internal/client-future.ts

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

🔇 Additional comments (17)
playground/app/global.css (1)

1-5: Verify CSS import directives and scanning configuration.

You’ve added @import "uploadthing/tw/v4"; and the nonstandard @source "../node_modules/@uploadthing/react/dist";. Please confirm that your PostCSS/Tailwind processor supports @source for scanning CSS classes in node modules. If not, classes used in the React dist may be purged at build time.

examples/with-novel/package.json (1)

14-14: Dependency version bump looks good.

Upgrading "class-variance-authority" to ^0.7.1 aligns with other examples. No conflicting versions of clsx remain in this example’s dependencies.

playground/components/fieldset.tsx (1)

2-2: Import cx from class-variance-authority is correct.

The switch from clsx to the named cx export matches the new styling utility and keeps component logic unchanged. Ensure that your overall build no longer includes the old clsx package.

examples/profile-picture/package.json (1)

19-19: Dependency version bump is consistent.

Updating "class-variance-authority" to ^0.7.1 mirrors other example projects. No additional adjustments needed here.

playground/components/skeleton.tsx (1)

1-1: Import adjustment for cx is applied correctly.

The refactor from clsx to class-variance-authority aligns with the rest of the codebase. Component behavior remains unaffected.

playground/components/uploader.tsx (1)

64-64: Endpoint change matches new upload route structure.

The change from using the anything endpoint to anyPrivate aligns with the updated backend upload routes. This ensures the uploader component correctly targets the private upload route as part of the new uploadFiles API implementation.

packages/uploadthing/turbo.json (1)

8-8: Build configuration updated for new client module.

The addition of "client-future/**" to the build outputs correctly includes the new module implementing the uploadFiles API in the package distribution.

playground/package.json (1)

13-13:

Details

✅ Verification successful

Dependencies updated to support new UI components.

The replacement of clsx with class-variance-authority and addition of lucide-react and tailwind-merge support the new UI components and styling approach used in the playground for demonstrating the new upload functionality.

Also applies to: 15-15, 19-19


🏁 Script executed:

#!/bin/bash
# Verify that components are using the new dependencies instead of the removed clsx
echo "Checking for any remaining clsx imports in the playground:"
rg "from ['\\"]clsx['\\"]" playground/

Length of output: 243


I’ve fixed the regex quoting to reliably catch any remaining clsx imports. Please rerun this check:


🏁 Script executed:

#!/bin/bash
echo "Searching for `clsx` imports in the playground directory:"
rg -n -e "import.*clsx" -e "from ['\\\"]clsx['\\\"]" playground/ || echo "No clsx imports found"

Length of output: 456


Let’s simplify the check to catch any mention of clsx (imports, requires, or usage) without complex regex:


🏁 Script executed:

#!/bin/bash
echo "Searching for any occurrence of 'clsx' in the playground directory:"
rg -nF "clsx" playground/ || echo "No clsx references found"

Length of output: 235


Dependencies Updated for New Playground UI Components

  • playground/package.json
    • Line 13: added "class-variance-authority": "^0.7.1"
    • Line 15: added "lucide-react"
    • Line 19: added "tailwind-merge"

Confirmed that all clsx references have been removed and replaced. No further changes required.

packages/uploadthing/src/_internal/ut-reporter.ts (1)

49-49: Made package property optional for more flexible reporting.

Making the package property optional and conditionally setting the x-uploadthing-package header only when the property is defined provides more flexibility in how the reporter is used. This change supports the new uploadFiles API by allowing the reporter to work with or without a specific package identifier.

Also applies to: 64-66

packages/uploadthing/src/client.ts (1)

44-46: Exported utilities for file size formatting and content label generation.

The addition of these two utility exports from @uploadthing/shared helps provide consistent formatting and label generation across the uploadthing ecosystem.

playground/components/file-card.tsx (2)

4-4: Migration from clsx to class-variance-authority.

This change aligns with the broader styling refactoring across the playground components.


43-53: Refactored delete button styling from props to direct classes.

The button styling has been updated to use explicit Tailwind classes instead of the color prop system, with a proper size="icon" property. This approach gives more direct control over the button's appearance.

packages/uploadthing/package.json (3)

22-31: Added client-future module to package exports.

This properly configures the new client-future module to be accessible to consumers of the package through both ESM and CJS module systems with proper type definitions.


131-131: Added client-future to files array.

This ensures the client-future directory is included in the published package.


149-151: Increased Node memory allocation for build processes.

The memory allocation for build and dev scripts has been doubled from 8GB to 16GB. This suggests the new client-future module may add significant complexity to the build process.

Consider monitoring the actual memory usage during builds to ensure this increase is necessary. If memory usage remains well below the new limit, you might be able to reduce it in the future.

playground/app/api/uploadthing/route.ts (1)

55-58: Images route configured differently from other routes.

The images route uses a different configuration with awaitServerData: false, which means client uploads will complete before server-side processing finishes. This is appropriate for image uploads where immediate UI feedback is desired.

playground/app/originui/page.tsx (1)

748-807: Repeated inline progress-bar logic could be extracted

Comp553 (and siblings) contain identical inline closures that calculate
progress and paint bars. Duplicating the snippet in four places hampers
maintenance and increases bundle size.

Consider a tiny, memoized <ProgressBar progress={sent/size} /> component
shared by all upload UIs.

[ suggest_optional_refactor ]

Comment on lines 30 to 33
const [files, setFiles] = useState<
(AnyFile<UploadRouter["images"]> & { preview?: string })[]
>([]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect generic narrows files state to the images route only

files is currently typed as
AnyFile<UploadRouter["images"]>[], so if the hook is used with
anyPrivate or anyPublic the compiler will (silently) lie – you’ll lose
type-safety and risk accessing props that do not exist on those route
definitions.

-  const [files, setFiles] = useState<
-    (AnyFile<UploadRouter["images"]> & { preview?: string })[]
-  >([]);
+  const [files, setFiles] = useState<
+    (AnyFile<UploadRouter[TFileRoute]> & { preview?: string })[]
+  >([]);

Apply the same substitution to every helper that takes/returns a file
(e.g. removeFile).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [files, setFiles] = useState<
(AnyFile<UploadRouter["images"]> & { preview?: string })[]
>([]);
const [files, setFiles] = useState<
(AnyFile<UploadRouter[TFileRoute]> & { preview?: string })[]
>([]);

Comment on lines +50 to +64
const pendingFiles = acceptedFiles.map((file) => {
const pendingFile = makePendingFile(file);
const preview = file.type.startsWith("image")
? URL.createObjectURL(file)
: undefined;
return Object.assign(pendingFile, { preview });
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Object-URL never revoked – causes memory leaks

URL.createObjectURL allocates a blob URL that must be released with
URL.revokeObjectURL when the preview is no longer needed
(component unmount or file removed). Otherwise each drag-and-drop adds a
permanent entry in the browser’s blob registry.

A minimal fix inside removeFile and a cleanup useEffect is enough:

- const removeFile = (file: AnyFile<AnyFileRoute>) => {
-   setFiles((prev) => prev.filter((f) => f !== file));
- };
+ const removeFile = (file: AnyFile<AnyFileRoute>) => {
+   if ('preview' in file && file.preview) URL.revokeObjectURL(file.preview);
+   setFiles((prev) => prev.filter((f) => f !== file));
+ };
+
+ useEffect(() => {
+   return () => {
+     files.forEach((f) => {
+       if ('preview' in f && f.preview) URL.revokeObjectURL(f.preview);
+     });
+   };
+ }, [files]);

Without this the tab’s memory usage continuously grows while testing the
drop-zone.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +162 to +168
const files = Arr.ensure(file ?? options.files);
for (const file of files) {
const upload = uploads.get(file);
if (!upload) throw "No upload found";

if (upload.deferred.ac.signal.aborted === false) {
// Ensure the upload is paused
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Throw typed errors instead of raw strings

abortUpload, pauseUpload, and resumeUpload use throw "No upload found" when a file key is missing. Throwing a bare string:

  • Loses stack-trace context in some runtimes.
  • Violates the project’s tagged‐error convention.
  • Is inconsistent with the rest of this module, which always throws an UploadThing*Error.
-        if (!upload) throw "No upload found";
+        if (!upload) {
+          throw new Error("uploadthing: no in-flight upload found for the provided File");
+        }

Consider a dedicated UploadNotFoundError (mirroring UploadAbortedError) if callers need to discriminate the failure.

Also applies to: 185-187

Comment on lines +1 to +4
import { Array } from "effect";
import * as Arr from "effect/Array";
import * as Micro from "effect/Micro";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid shadowing the global Array and remove the duplicate import

Importing Array from effect masks the built-in Array constructor and the subsequent explicit effect/Array import is redundant. This can be confusing for anyone reading or debugging the code and may break IDE ref-actors that rely on the global symbol.

-import { Array } from "effect";
-import * as Arr from "effect/Array";
+import * as Arr from "effect/Array";          // gives you Arr.zip & Arr.ensure

Then replace every Array.zip usage in this file with Arr.zip.

This keeps one canonical alias, eliminates the shadowed global, and aligns with the rest of the codebase that already uses Arr.ensure.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Array } from "effect";
import * as Arr from "effect/Array";
import * as Micro from "effect/Micro";
import * as Arr from "effect/Array"; // gives you Arr.zip & Arr.ensure
import * as Micro from "effect/Micro";
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.changeset/happy-houses-hang.md (1)

9-9: Replace bare URL with Markdown link to satisfy MD034 lint rule. Embed the link in Markdown syntax, for example:

- https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx
+ [Example usage here](https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx)
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

9-9: Bare URL used
null

(MD034, no-bare-urls)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1c75b8 and bc9db78.

📒 Files selected for processing (1)
  • .changeset/happy-houses-hang.md (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
.changeset/happy-houses-hang.md

9-9: Bare URL used
null

(MD034, no-bare-urls)

🔇 Additional comments (3)
.changeset/happy-houses-hang.md (3)

1-3: Frontmatter formatting is correct. The changeset header properly specifies the uploadthing package with a minor version bump according to the Changesets spec.


5-5: Commit summary aligns with Conventional Commits. The feat: prefix clearly indicates a new experimental client API.


7-7: Explanatory text is clear. No changes needed.

@juliusmarminge juliusmarminge merged commit bc3d422 into main May 1, 2025
21 of 22 checks passed
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
packages/uploadthing/src/client-future.ts (4)

225-225: Consider consistency in error handling between functions

abortUpload throws an explicit UploadAbortedError after pausing uploads, while pauseUpload simply returns if an upload isn't found without throwing anything. This creates an inconsistent API experience.

Either make both functions no-op when the file isn't found, or make both throw the same style of error:

 const pauseUpload = (file?: File) => {
   const files = Arr.ensure(file ?? options.files);
   for (const file of files) {
     const upload = uploads.get(file);
-    if (!upload) return;
+    if (!upload) {
+      // Either throw consistently like abortUpload:
+      // throw new Error("uploadthing: no in-flight upload found for the provided File");
+      // Or just return silently (current behavior)
+      return;
+    }

1-2: ⚠️ Potential issue

Avoid shadowing the global Array object

Importing Array from effect masks the built-in Array constructor which can cause confusion. This was flagged in a previous review but wasn't addressed.

-import { Array } from "effect";
-import * as Arr from "effect/Array";
+import * as Arr from "effect/Array";

Then update all Array.zip calls in this file to use Arr.zip.

🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


165-165: 🛠️ Refactor suggestion

Replace string literals with proper Error objects

Throwing string literals loses stack trace information and is inconsistent with the project's error handling pattern.

-        if (!upload) throw "No upload found";
+        if (!upload) {
+          throw new Error("uploadthing: no in-flight upload found for the provided File");
+        }

185-185: 🛠️ Refactor suggestion

Replace string literals with proper Error objects

Throwing string literals loses stack trace information and is inconsistent with the project's error handling pattern.

-        if (!upload) throw "No upload found";
+        if (!upload) {
+          throw new Error("uploadthing: no in-flight upload found for the provided File");
+        }
packages/uploadthing/src/_internal/client-future.ts (1)

1-3: ⚠️ Potential issue

Avoid shadowing the global Array object

Importing Array from effect masks the built-in Array constructor which can cause confusion.

-import { Array, Micro, Predicate } from "effect";
+import { Micro, Predicate } from "effect";
+import * as EffArray from "effect/Array";

Then update all Array.zip calls to use EffArray.zip.

🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

🧹 Nitpick comments (4)
.changeset/happy-houses-hang.md (1)

9-9: Use Markdown link syntax instead of bare URL

For better readability and markup compatibility, use proper Markdown link syntax instead of a bare URL.

-https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx
+[Example usage](https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx)
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

9-9: Bare URL used
null

(MD034, no-bare-urls)

packages/uploadthing/src/client-future.ts (2)

213-219: Avoid void in union types

Using void in a union type can be confusing. Consider using undefined instead for better type clarity.

-const done = async <T extends AnyFile<TRouter[TEndpoint]> | void = void>(
+const done = async <T extends AnyFile<TRouter[TEndpoint]> | undefined = undefined>(
   file?: T,
 ): Promise<
   T extends AnyFile<TRouter[TEndpoint]>
     ? UploadedFile<TRouter[TEndpoint]> | FailedFile<TRouter[TEndpoint]>
     : (UploadedFile<TRouter[TEndpoint]> | FailedFile<TRouter[TEndpoint]>)[]
 > => {
🧰 Tools
🪛 Biome (1.9.4)

[error] 213-213: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)


146-147: Consider validating files against upload constraints

The pauseUpload, resumeUpload, and abortUpload functions don't validate that the provided file was part of the original upload set. This might lead to unexpected behavior if consumers pass unrelated files.

Consider validating that any provided file was actually part of the initial upload request:

 const pauseUpload = (file?: File) => {
   const files = Arr.ensure(file ?? options.files);
   for (const file of files) {
+    // Check if file was part of the original upload
+    if (file && !options.files.includes(file)) {
+      throw new Error("uploadthing: file was not part of the original upload request");
+    }
     const upload = uploads.get(file);
     if (!upload) return;
packages/uploadthing/src/_internal/client-future.ts (1)

594-618: Ensure concurrent uploads respect rate limits

The code uses a concurrency limit of 6 for parallel uploads, but it's not clear if this was chosen based on specific API limitations or testing.

The hardcoded concurrency value of 6 may need adjustment based on:

  1. Storage provider rate limits
  2. Client device capabilities (memory, network)
  3. Server-side capacity

Consider making this configurable or dynamically adjustable based on network conditions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc9db78 and f8c7cd2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • .changeset/happy-houses-hang.md (1 hunks)
  • examples/profile-picture/package.json (1 hunks)
  • examples/with-novel/package.json (1 hunks)
  • packages/uploadthing/package.json (3 hunks)
  • packages/uploadthing/src/_internal/client-future.ts (1 hunks)
  • packages/uploadthing/src/_internal/ut-reporter.ts (2 hunks)
  • packages/uploadthing/src/client-future.ts (1 hunks)
  • packages/uploadthing/src/client.ts (1 hunks)
  • packages/uploadthing/turbo.json (1 hunks)
  • playground/app/api/uploadthing/route.ts (1 hunks)
  • playground/app/future/page.tsx (1 hunks)
  • playground/app/global.css (1 hunks)
  • playground/app/originui/page.tsx (1 hunks)
  • playground/components/button.tsx (1 hunks)
  • playground/components/fieldset.tsx (1 hunks)
  • playground/components/file-card.tsx (2 hunks)
  • playground/components/skeleton.tsx (1 hunks)
  • playground/components/uploader.tsx (1 hunks)
  • playground/lib/uploadthing.ts (1 hunks)
  • playground/package.json (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • playground/components/fieldset.tsx
  • examples/with-novel/package.json
🚧 Files skipped from review as they are similar to previous changes (15)
  • examples/profile-picture/package.json
  • playground/components/uploader.tsx
  • playground/components/skeleton.tsx
  • packages/uploadthing/src/_internal/ut-reporter.ts
  • packages/uploadthing/turbo.json
  • playground/package.json
  • packages/uploadthing/package.json
  • playground/components/file-card.tsx
  • packages/uploadthing/src/client.ts
  • playground/app/global.css
  • playground/app/originui/page.tsx
  • playground/components/button.tsx
  • playground/app/future/page.tsx
  • playground/lib/uploadthing.ts
  • playground/app/api/uploadthing/route.ts
🧰 Additional context used
🪛 Biome (1.9.4)
packages/uploadthing/src/_internal/client-future.ts

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

packages/uploadthing/src/client-future.ts

[error] 1-1: Do not shadow the global "Array" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


[error] 213-213: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

🪛 markdownlint-cli2 (0.17.2)
.changeset/happy-houses-hang.md

9-9: Bare URL used
null

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: bundle-analyze-result
  • GitHub Check: e2e-node (backend-adapters)
🔇 Additional comments (8)
.changeset/happy-houses-hang.md (1)

1-9: LGTM! Properly documented experimental feature

The changeset correctly marks this as a minor version change and clearly states that the API is experimental and not covered by semver.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

9-9: Bare URL used
null

(MD034, no-bare-urls)

packages/uploadthing/src/client-future.ts (3)

72-74: LGTM! Strong typing for the endpoint resolution

The implementation properly handles both function and direct string endpoint specifications, providing good type safety.


75-89: LGTM! Robust error handling with effect library

The code properly uses the effect library for error handling and transforms the errors into a more user-friendly format.


97-136: LGTM! Excellent implementation of concurrent uploads

The implementation properly handles concurrent uploads with proper state management and event emission.

packages/uploadthing/src/_internal/client-future.ts (4)

20-67: LGTM! Well-structured error class hierarchy

The error classes follow a consistent pattern with the TaggedError approach, making it easy to distinguish between different error types and providing rich error information.


74-170: LGTM! Comprehensive file state interfaces

The file state interfaces (PendingFile, UploadingFile, FailedFile, UploadedFile) are well designed with clear status discriminants and appropriate properties for each state.


380-489: LGTM! Robust upload implementation with progress tracking

The uploadFile function is well-implemented with proper error handling, progress tracking, and state transitions. The special handling for iOS/React Native FormData is a nice touch.


460-473:

Details

❓ Verification inconclusive

Verify React Native compatibility with real-world devices

The code has special handling for React Native FormData, but it's important to verify this works across various React Native versions and iOS devices.


🏁 Script executed:

#!/bin/bash
# Verify if the React Native special case is documented or tested
echo "Checking for React Native tests or documentation:"
find . -type f -name "*.md" -o -name "*.test.ts" -o -name "*.test.tsx" | xargs grep -l "React Native" || echo "No React Native documentation or tests found"

# Look for existing React Native integration in the codebase
echo -e "\nChecking for React Native integration:"
grep -r --include="*.ts" --include="*.tsx" "React Native" .

Length of output: 1508


Please add automated tests and documentation for React Native FormData handling

I see that we already have React Native–specific handling in client-future.ts (and mirrored in upload-browser.ts) and explanatory comments in the Expo packages, but there aren’t any corresponding tests or markdown docs.

• Add integration or unit tests covering the "uri" in file branch on real devices or emulators (e.g., Jest+React Native Testing Library or E2E tests).
• Update your package README or add a dedicated MD file to document the React Native FormData behavior and any iOS quirks (nil-object crash workaround, supported RN versions, etc.).


This API is not covered under semver. Check out some example usage here:

https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify that the example URL is correct

The URL points to /originui/page.tsx but the PR summary mentions examples in playground/app/future/page.tsx. Please verify this is the correct example location.


🏁 Script executed:

#!/bin/bash
# Check if both paths exist and compare them
echo "Checking if the files exist:"
ls -la playground/app/originui/page.tsx 2>/dev/null
ls -la playground/app/future/page.tsx 2>/dev/null

# Check if they contain examples of the new API
echo -e "\nChecking for usage of future_genUploader in files:"
grep -n "future_genUploader" playground/app/originui/page.tsx 2>/dev/null
grep -n "future_genUploader" playground/app/future/page.tsx 2>/dev/null

Length of output: 550


Update the example URL in the changeset

It looks like the linked example in .changeset/happy-houses-hang.md still points to:

https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx

but according to the PR summary the new API example lives in playground/app/future/page.tsx. Please update the URL on line 9 of that changeset to:

- https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx
+ https://github.com/pingdotgg/uploadthing/blob/main/playground/app/future/page.tsx

and verify the linked file shows the intended example.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
https://github.com/pingdotgg/uploadthing/blob/main/playground/app/originui/page.tsx
https://github.com/pingdotgg/uploadthing/blob/main/playground/app/future/page.tsx
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

9-9: Bare URL used
null

(MD034, no-bare-urls)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants