Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add minimal-tanstack-start example #989

Merged
merged 11 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
7 changes: 7 additions & 0 deletions .changeset/healthy-vans-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@uploadthing/shared": patch
"@uploadthing/react": patch
"@uploadthing/expo": patch
---

chore: refactor internal hook generator to be more friendly for Vite React Refresh
2 changes: 2 additions & 0 deletions examples/minimal-tanstack-start/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Go to https://uploadthing.com/dashboard to get your token
UPLOADTHING_TOKEN='...'
3 changes: 3 additions & 0 deletions examples/minimal-tanstack-start/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "@tanstack/start/config";

export default defineConfig({});
juraj98 marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions examples/minimal-tanstack-start/app/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {
createStartAPIHandler,
defaultAPIFileRouteHandler,
} from "@tanstack/start/api";

export default createStartAPIHandler(defaultAPIFileRouteHandler);
8 changes: 8 additions & 0 deletions examples/minimal-tanstack-start/app/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { StartClient } from "@tanstack/start";
import { hydrateRoot } from "react-dom/client";

import { createRouter } from "./router";

const router = createRouter();

hydrateRoot(document.getElementById("root")!, <StartClient router={router} />);
89 changes: 89 additions & 0 deletions examples/minimal-tanstack-start/app/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* prettier-ignore-start */

/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file is auto-generated by TanStack Router

// Import Routes

import { Route as rootRoute } from "./routes/__root";
import { Route as IndexImport } from "./routes/index";

// Create/Update Routes

const IndexRoute = IndexImport.update({
path: "/",
getParentRoute: () => rootRoute,
} as any);

// Populate the FileRoutesByPath interface

declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
}
}

// Create and export the route tree

export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
}

export interface FileRoutesByTo {
"/": typeof IndexRoute;
}

export interface FileRoutesById {
__root__: typeof rootRoute;
"/": typeof IndexRoute;
}

export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/";
fileRoutesByTo: FileRoutesByTo;
to: "/";
id: "__root__" | "/";
fileRoutesById: FileRoutesById;
}

export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
};

export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>();

/* prettier-ignore-end */

/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/"
]
},
"/": {
"filePath": "index.tsx"
}
}
}
ROUTE_MANIFEST_END */
38 changes: 38 additions & 0 deletions examples/minimal-tanstack-start/app/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
AnyRouter,
createRouter as createTanStackRouter,
} from "@tanstack/react-router";

import { routeTree } from "./routeTree.gen";

/**
* This wrapper could potentially be exported from `@uploadthing/react` similar to NextSSRPlugin
*/
function routerWithUploadThing<TRouter extends AnyRouter>(router: TRouter) {
if (router.isServer) {
// On the server, stream the router config over the wire to the client
// globalThis.__UPLOADTHING is set in `ssr.tsx` to ensure we don't
// leak the entire router code to the client
router.streamValue("__UPLOADTHING", globalThis.__UPLOADTHING);
} else {
// On the client, inject the streamed value into globalThis
// useUploadThing will pick this up and skip a client side fetch
globalThis.__UPLOADTHING = router.getStreamedValue("__UPLOADTHING");
}

return router;
}

export function createRouter() {
const router = createTanStackRouter({
routeTree,
});

return routerWithUploadThing(router);
}

declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
38 changes: 38 additions & 0 deletions examples/minimal-tanstack-start/app/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from "react";
import {
createRootRoute,
Outlet,
ScrollRestoration,
} from "@tanstack/react-router";
import { Body, Head, Html, Meta, Scripts } from "@tanstack/start";

// @ts-expect-error
import uploadthingCss from "@uploadthing/react/styles.css?url";
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved

export const Route = createRootRoute({
component: RootComponent,
links: () => [{ rel: "stylesheet", href: uploadthingCss }],
});

function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
);
}

function RootDocument({ children }: { children: React.ReactNode }) {
return (
<Html>
<Head>
<Meta />
</Head>
<Body>
{children}
<ScrollRestoration />
<Scripts />
</Body>
</Html>
);
}
11 changes: 11 additions & 0 deletions examples/minimal-tanstack-start/app/routes/api/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createAPIFileRoute } from "@tanstack/start/api";

import { createRouteHandler } from "uploadthing/server";

import { uploadRouter } from "../../server/uploadthing";

const handlers = createRouteHandler({ router: uploadRouter });
export const Route = createAPIFileRoute("/api/uploadthing")({
GET: handlers,
POST: handlers,
});
79 changes: 79 additions & 0 deletions examples/minimal-tanstack-start/app/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createFileRoute } from "@tanstack/react-router";

import {
UploadButton,
UploadDropzone,
useUploadThing,
} from "../utils/uploadthing";

export const Route = createFileRoute("/")({
component: Home,
});

function Home() {
const { startUpload } = useUploadThing("videoAndImage", {
/**
* @see https://docs.uploadthing.com/api-reference/react#useuploadthing
*/
onBeforeUploadBegin: (files) => {
console.log("Uploading", files.length, "files");
return files;
},
onUploadBegin: (name) => {
console.log("Beginning upload of", name);
},
onClientUploadComplete: (res) => {
console.log("Upload Completed.", res.length, "files uploaded");
},
onUploadProgress(p) {
console.log("onUploadProgress", p);
},
});
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved

return (
<main>
<UploadButton
/**
* @see https://docs.uploadthing.com/api-reference/react#uploadbutton
*/
endpoint="videoAndImage"
onClientUploadComplete={(res) => {
console.log(`onClientUploadComplete`, res);
alert("Upload Completed");
}}
onUploadBegin={() => {
console.log("upload begin");
}}
config={{ appendOnPaste: true, mode: "manual" }}
/>
<UploadDropzone
/**
* @see https://docs.uploadthing.com/api-reference/react#uploaddropzone
*/
endpoint="videoAndImage"
onUploadAborted={() => {
alert("Upload Aborted");
}}
onClientUploadComplete={(res) => {
console.log(`onClientUploadComplete`, res);
alert("Upload Completed");
}}
onUploadBegin={() => {
console.log("upload begin");
}}
/>
<input
type="file"
multiple
onChange={async (e) => {
const files = Array.from(e.target.files ?? []);

// Do something with files

// Then start the upload
await startUpload(files);
}}
/>
</main>
);
}
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
61 changes: 61 additions & 0 deletions examples/minimal-tanstack-start/app/server/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createUploadthing, UTFiles } from "uploadthing/next";
import type { FileRouter } from "uploadthing/next";

const f = createUploadthing({
/**
* Log out more information about the error, but don't return it to the client
* @see https://docs.uploadthing.com/errors#error-formatting
*/
errorFormatter: (err) => {
console.log("Error uploading file", err.message);
console.log(" - Above error caused by:", err.cause);

return { message: err.message };
},
});
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved

/**
* This is your Uploadthing file router. For more information:
* @see https://docs.uploadthing.com/api-reference/server#file-routes
*/
export const uploadRouter = {
videoAndImage: f({
image: {
maxFileSize: "32MB",
maxFileCount: 4,
acl: "public-read",
},
video: {
maxFileSize: "16MB",
},
blob: {
maxFileSize: "8GB",
},
})
.middleware(({ req, files }) => {
// Check some condition based on the incoming requrest
console.log("Request", req);
//^?
// if (!req.headers.get("x-some-header")) {
// throw new Error("x-some-header is required");
// }

// (Optional) Label your files with a custom identifier
const filesWithMyIds = files.map((file, idx) => ({
...file,
customId: `${idx}-HELLO`,
}));

// Return some metadata to be stored with the file
return { foo: "bar" as const, [UTFiles]: filesWithMyIds };
})
Comment on lines +35 to +51
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

Review request logging and uncomment header check if necessary

  1. The middleware is logging the entire request object, which might include sensitive information. Consider logging only necessary information or sanitizing the log output.

  2. There's a commented-out header check. If this check is important for security or functionality, it should be uncommented and implemented properly.

To address these concerns:

  1. Replace the request logging with a more specific log:
-      console.log("Request", req);
+      console.log("Request received", {
+        method: req.method,
+        url: req.url,
+        // Add other non-sensitive information as needed
+      });
  1. If the header check is necessary, uncomment and implement it:
-      // if (!req.headers.get("x-some-header")) {
-      //   throw new Error("x-some-header is required");
-      // }
+      if (!req.headers.get("x-some-header")) {
+        throw new Error("x-some-header is required");
+      }

If the header check is not needed, consider removing the commented code to improve readability.

Committable suggestion was skipped due to low confidence.

.onUploadComplete(({ file, metadata }) => {
metadata;
// ^?
file.customId;
// ^?
console.log("upload completed", file);
}),
Comment on lines +52 to +58
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

Refine upload completion logging and remove development comments

  1. The onUploadComplete callback is logging the entire file object, which might contain sensitive information. Consider logging only necessary, non-sensitive details.

  2. There are type assertion comments (^?) which appear to be leftover from development. These should be removed in the production code.

To address these points:

  1. Refine the log statement:
-      console.log("upload completed", file);
+      console.log("Upload completed", {
+        fileId: file.id,
+        fileName: file.name,
+        fileSize: file.size,
+        customId: file.customId
+      });
  1. Remove the type assertion comments:
-      metadata;
-      // ^?
-      file.customId;
-      //   ^?
+      // Use metadata and file.customId as needed
📝 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
.onUploadComplete(({ file, metadata }) => {
metadata;
// ^?
file.customId;
// ^?
console.log("upload completed", file);
}),
.onUploadComplete(({ file, metadata }) => {
// Use metadata and file.customId as needed
console.log("Upload completed", {
fileId: file.id,
fileName: file.name,
fileSize: file.size,
customId: file.customId
});
}),

} satisfies FileRouter;

export type OurFileRouter = typeof uploadRouter;
17 changes: 17 additions & 0 deletions examples/minimal-tanstack-start/app/ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getRouterManifest } from "@tanstack/start/router-manifest";
import {
createStartHandler,
defaultStreamHandler,
} from "@tanstack/start/server";

import { extractRouterConfig } from "uploadthing/server";

import { createRouter } from "./router";
import { uploadRouter } from "./server/uploadthing";

globalThis.__UPLOADTHING = extractRouterConfig(uploadRouter);
Copy link
Collaborator

Choose a reason for hiding this comment

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

kinda hate this but it works... not sure where else we can import uploadRouter without leaking it to client bundle.


export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler);
12 changes: 12 additions & 0 deletions examples/minimal-tanstack-start/app/utils/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
generateReactHelpers,
generateUploadButton,
generateUploadDropzone,
} from "@uploadthing/react";

import type { OurFileRouter } from "../server/uploadthing";

export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();

export const { useUploadThing } = generateReactHelpers<OurFileRouter>();
Loading
Loading