Skip to content

Commit

Permalink
[feat] File router overhaul (#94)
Browse files Browse the repository at this point in the history
* getting started

* cleannup

* Make it work

* mark as breaking 4.0 change

* cleanup

* fix build and types

* update docs

* guarantee limit and file size always exist with backfill/upscale/I need a better name for this

* wire up file limit path, still haven't implemented lol

* parseAndExpandInputConfig rename

* file limits working with mime-types as our first dep :(

* Fix copy for button labels, add /sink

* maybe this will fix test builds

* versioning is annoying

* clean up stale comments

* More pluralizations

* Typed counts
  • Loading branch information
t3dotgg authored May 28, 2023
1 parent b4857e4 commit 91fb166
Show file tree
Hide file tree
Showing 16 changed files with 399 additions and 112 deletions.
6 changes: 6 additions & 0 deletions .changeset/olive-donkeys-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"uploadthing": major
"@uploadthing/react": major
---

Overhauled file router syntax
4 changes: 1 addition & 3 deletions docs/src/pages/nextjs/appdir.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ const auth = (req: Request) => ({ id: "fakeId" }); // Fake auth function
// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
imageUploader: f
imageUploader: f({ image: { maxFileSize: "4MB" } })
// Set permissions and file types for this FileRoute
.fileTypes(["image", "video"])
.maxSize("1GB")
.middleware(async (req) => {
// This code runs on your server before upload
const user = await auth(req);
Expand Down
4 changes: 1 addition & 3 deletions docs/src/pages/nextjs/pagedir.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ const auth = (req: NextApiRequest, res: NextApiResponse) => ({ id: "fakeId" });
// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
imageUploader: f
imageUploader: f({ image: { maxFileSize: "4MB" } })
// Set permissions and file types for this FileRoute
.fileTypes(["image", "video"])
.maxSize("1GB")
.middleware(async (req, res) => {
// This code runs on your server before upload
const user = await auth(req, res);
Expand Down
32 changes: 26 additions & 6 deletions examples/appdir/src/app/_uploadthing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@ import { createUploadthing, type FileRouter } from "uploadthing/next";
const f = createUploadthing();

export const uploadRouter = {
withMdwr: f
.fileTypes(["image"])
.maxSize("16MB")
videoAndImage: f({
image: {
maxFileSize: "4MB",
maxFileCount: 4,
},
video: {
maxFileSize: "16MB",
},
})
.middleware(() => ({}))
.onUploadComplete((data) => {
console.log("upload completed", data);
}),

withMdwr: f({
image: {
maxFileCount: 2,
maxFileSize: "1MB",
},
})
.middleware((req) => {
const h = req.headers.get("someProperty");

Expand All @@ -28,9 +45,12 @@ export const uploadRouter = {
// ^?
}),

withoutMdwr: f
.maxSize("64MB")
.fileTypes(["image"])
withoutMdwr: f({
image: {
maxFileCount: 2,
maxFileSize: "16MB",
},
})
.middleware(() => {
return { testMetadata: "lol" };
})
Expand Down
55 changes: 55 additions & 0 deletions examples/appdir/src/app/sink/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { Inter } from "next/font/google";
import { UploadButton, UploadDropzone } from "@uploadthing/react";
import type { OurFileRouter } from "../_uploadthing";

const inter = Inter({ subsets: ["latin"] });

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center gap-16 p-24">
<div className="flex flex-col items-center justify-center gap-4">
<div className="flex gap-4">
<UploadButton<OurFileRouter>
endpoint="withoutMdwr"
onClientUploadComplete={(res) => {
console.log("Files: ", res);
alert("Upload Completed");
}}
onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`);
}}
/>

<UploadButton<OurFileRouter>
endpoint="videoAndImage"
onClientUploadComplete={(res) => {
console.log("Files: ", res);
alert("Upload Completed");
}}
onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`);
}}
/>
</div>
</div>
<div className="flex flex-col items-center justify-center gap-4">
<span className="text-4xl font-bold text-center">
{`...or using a dropzone:`}
</span>
<UploadDropzone<OurFileRouter>
endpoint="withoutMdwr"
onClientUploadComplete={(res) => {
// Do something with the response
console.log("Files: ", res);
alert("Upload Completed");
}}
onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`);
}}
/>
</div>
</main>
);
}
6 changes: 2 additions & 4 deletions examples/pagedir/src/server/uploadthing/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { options } from "~/pages/api/auth/[...nextauth]";
const f = createUploadthing();

export const uploadRouter = {
withMdwr: f
.fileTypes(["image"])
.maxSize("16MB")
withMdwr: f(["image"])
.middleware(async (req, res) => {
const auth = await getServerSession(req, res, options);

Expand All @@ -29,7 +27,7 @@ export const uploadRouter = {
// ^?
}),

withoutMdwr: f
withoutMdwr: f(["image"])
.middleware(() => {
return { testMetadata: "lol" };
})
Expand Down
8 changes: 5 additions & 3 deletions packages/react/src/client-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ const badReqMock = {
},
} as unknown as Request;

it("typeerrors for invalid input", async () => {
it("typeerrors for invalid input", () => {
const f = createUploadthing();

const exampleRoute = f
const exampleRoute = f(["image"])
.middleware(() => ({ foo: "bar" }))
.onUploadComplete(({ metadata }) => {});
.onUploadComplete(({ metadata }) => {
console.log(metadata);
});

const router = { exampleRoute };

Expand Down
76 changes: 58 additions & 18 deletions packages/react/src/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,60 @@ import {
generateMimeTypes,
} from "uploadthing/client";
import { useUploadThing } from "./useUploadThing";
import type { FileRouter } from "uploadthing/server";
import type { FileRouter, ExpandedRouteConfig } from "uploadthing/server";
import type { DANGEROUS__uploadFiles } from "uploadthing/client";

type EndpointHelper<TRouter extends void | FileRouter> = void extends TRouter
? "YOU FORGOT TO PASS THE GENERIC"
: keyof TRouter;

const generatePermittedFileTypes = (config?: ExpandedRouteConfig) => {
const fileTypes = config ? Object.keys(config) : [];

const maxFileCount = config
? Object.values(config).map((v) => v.maxFileCount)
: [];

return { fileTypes, multiple: maxFileCount.some((v) => v && v > 1) };
};

const capitalizeStart = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};

const INTERNAL_doFormatting = (config?: ExpandedRouteConfig): string => {
if (!config) return "";

const allowedTypes = Object.keys(config) as (keyof ExpandedRouteConfig)[];

const formattedTypes = allowedTypes.map((f) => (f === "blob" ? "file" : f));

// Format multi-type uploader label as "Supports videos, images and files";
if (formattedTypes.length > 1) {
const lastType = formattedTypes.pop();
return `${formattedTypes.join("s, ")} and ${lastType}s`;
}

// Single type uploader label
const key = allowedTypes[0];
const formattedKey = formattedTypes[0];

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { maxFileSize, maxFileCount } = config[key]!;

if (maxFileCount && maxFileCount > 1) {
return `${formattedKey}s up to ${maxFileSize}, max ${maxFileCount}`;
} else {
return `${formattedKey} (${maxFileSize})`;
}
};

const allowedContentTextLabelGenerator = (
config?: ExpandedRouteConfig
): string => {
return capitalizeStart(INTERNAL_doFormatting(config));
};

/**
* @example
* <UploadButton<OurFileRouter>
Expand All @@ -23,7 +70,6 @@ type EndpointHelper<TRouter extends void | FileRouter> = void extends TRouter
*/
export function UploadButton<TRouter extends void | FileRouter = void>(props: {
endpoint: EndpointHelper<TRouter>;
multiple?: boolean;
onClientUploadComplete?: (
res?: Awaited<ReturnType<typeof DANGEROUS__uploadFiles>>
) => void;
Expand All @@ -36,34 +82,31 @@ export function UploadButton<TRouter extends void | FileRouter = void>(props: {
onUploadError: props.onUploadError,
});

const { maxSize, fileTypes } = permittedFileInfo ?? {};
const { fileTypes, multiple } = generatePermittedFileTypes(
permittedFileInfo?.config
);

return (
<div className="ut-flex ut-flex-col ut-gap-1 ut-items-center ut-justify-center">
<label className="ut-bg-blue-600 ut-rounded-md ut-w-36 ut-h-10 ut-flex ut-items-center ut-justify-center ut-cursor-pointer">
<input
className="ut-hidden"
type="file"
multiple={props.multiple}
multiple={multiple}
accept={generateMimeTypes(fileTypes ?? [])?.join(", ")}
onChange={(e) => {
if (!e.target.files) return;
void startUpload(Array.from(e.target.files));
}}
/>
<span className="ut-px-3 ut-py-2 ut-text-white">
{isUploading ? (
<Spinner />
) : (
`Choose File${props.multiple ? `(s)` : ``}`
)}
{isUploading ? <Spinner /> : `Choose File${multiple ? `(s)` : ``}`}
</span>
</label>
<div className="ut-h-[1.25rem]">
{fileTypes && (
<p className="ut-text-xs ut-leading-5 ut-text-gray-600">
{`${fileTypes.includes("blob") ? "File" : fileTypes.join(", ")}`}{" "}
{maxSize && `up to ${maxSize}`}
{allowedContentTextLabelGenerator(permittedFileInfo?.config)}
</p>
)}
</div>
Expand Down Expand Up @@ -108,7 +151,7 @@ export const UploadDropzone = <
setFiles(acceptedFiles);
}, []);

const { maxSize, fileTypes } = permittedFileInfo ?? {};
const { fileTypes } = generatePermittedFileTypes(permittedFileInfo?.config);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
Expand Down Expand Up @@ -146,12 +189,9 @@ export const UploadDropzone = <
<p className="ut-pl-1">{`or drag and drop`}</p>
</div>
<div className="ut-h-[1.25rem]">
{fileTypes && (
<p className="ut-text-xs ut-leading-5 ut-text-gray-600">
{`${fileTypes.includes("blob") ? "File" : fileTypes.join(", ")}`}{" "}
{maxSize && `up to ${maxSize}`}
</p>
)}
<p className="ut-text-xs ut-leading-5 ut-text-gray-600">
{allowedContentTextLabelGenerator(permittedFileInfo?.config)}
</p>
</div>
{files.length > 0 && (
<div className="ut-mt-4 ut-flex ut-items-center ut-justify-center">
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/useUploadThing.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useState } from "react";
import type { FileRouter } from "uploadthing/server";
import type { FileRouter, ExpandedRouteConfig } from "uploadthing/server";
import { DANGEROUS__uploadFiles } from "uploadthing/client";

import { useEvent } from "./utils/useEvent";
import useFetch from "./utils/useFetch";

type EndpointMetadata = {
slug: string;
maxSize: string;
fileTypes: string[];
config: ExpandedRouteConfig;
}[];
const useEndpointMetadata = (endpoint: string) => {
const { data } = useFetch<EndpointMetadata>("/api/uploadthing");
Expand Down
4 changes: 4 additions & 0 deletions packages/uploadthing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"mime-types": "^2.1.35"
},
"devDependencies": {
"@types/mime-types": "2.1.1",
"@types/node": "18.16.0",
"@uploadthing/eslint-config": "0.1.0",
"eslint": "^8.40.0",
Expand Down
Loading

1 comment on commit 91fb166

@vercel
Copy link

@vercel vercel bot commented on 91fb166 May 28, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.