diff --git a/docs/src/content/docs/core/react-server-components.mdx b/docs/src/content/docs/core/react-server-components.mdx index 8b54d6c44..4039f2c52 100644 --- a/docs/src/content/docs/core/react-server-components.mdx +++ b/docs/src/content/docs/core/react-server-components.mdx @@ -28,8 +28,8 @@ export default function MyClientComponent() { React Server Components run on the server, they can easily fetch data and make it part of the payload that's sent to the client. ```tsx title="src/app/pages/todos/TodoPage.tsx" "async" -export async function Todos({ ctx }) { - const todos = await db.todo.findMany({ where: { userId: ctx.user.id } }); +export async function Todos({ appContext }) { + const todos = await db.todo.findMany({ where: { userId: appContext.user.id } }); return (
    {todos.map((todo) => ( @@ -39,19 +39,19 @@ export async function Todos({ ctx }) { ); } -export async function TodoPage({ ctx }) { +export async function TodoPage({ appContext }) { return (

    Todos

    Loading...
    }> - + ); } --- -The `TodoPage` component is a server component. It it rendered by a route, so it receives the `ctx` object. We pass this to the `Todos` component, which is also a server component, +The `TodoPage` component is a server component. It it rendered by a route, so it receives the `appContext` object. We pass this to the `Todos` component, which is also a server component, and renders the todos. --- ``` @@ -69,9 +69,9 @@ Allow you to execute code on the server from a client component. ```tsx title="@/pages/todos/functions.tsx" mark={1} "use server"; -export async function addTodo(formData: FormData, { ctx }: RouteOptions) { +export async function addTodo(formData: FormData, { appContext }: RouteOptions) { const title = formData.get("title"); - await db.todo.create({ data: { title, userId: ctx.user.id } }); + await db.todo.create({ data: { title, userId: appContext.user.id } }); } --- diff --git a/docs/src/content/docs/core/routing.mdx b/docs/src/content/docs/core/routing.mdx index 6c5f949c1..5b0e7782e 100644 --- a/docs/src/content/docs/core/routing.mdx +++ b/docs/src/content/docs/core/routing.mdx @@ -13,13 +13,13 @@ import { route } from "@redwoodjs/sdk/router"; export default defineApp([ // Middleware - function middleware({ request, env, ctx }) { /* Modify context */ }, - function middleware({ request, env, ctx }) { /* Modify context */ }, + function middleware({ request, env, appContext }) { /* Modify context */ }, + function middleware({ request, env, appContext }) { /* Modify context */ }, // Request Handlers - route("/", function handler({ request, env, ctx }) { + route("/", function handler({ request, env, appContext }) { return new Response("Hello, world!") }), - route("/ping", function handler({ request, env, ctx }) { + route("/ping", function handler({ request, env, appContext }) { return new Response("Pong!") }), ]); @@ -87,7 +87,7 @@ The request handler is a function, or array of functions (See [Interruptors](#in import { route } from "@redwoodjs/sdk/router"; defineApp([ - route("/a-standard-response", ({ request, params, env, ctx }) => { + route("/a-standard-response", ({ request, params, env, appContext }) => { return new Response("Hello, world!") }), route('/a-jsx-response', () => { @@ -100,7 +100,7 @@ The request handler function takes in the following options: 1. `request`: The request object. 2. `params`: The matched parameters from the request URL. 3. `env`: The Cloudflare environment. -4. `ctx`: The context object (See [Middleware & Context](#middleware-context)). +4. `appContext`: The context object (See [Middleware & `AppContext`](#middleware--app-context)). 5. `cf:` Cloudflare's [Execution Context API](https://developers.cloudflare.com/workers/runtime-apis/context/) methods, e.g. `waitUntil()` Return values: @@ -118,9 +118,9 @@ import { defineApp } from "@redwoodjs/sdk/worker"; import { route } from "@redwoodjs/sdk/router"; import { EditBlogPage } from "src/pages/blog/EditBlogPage"; -function isAuthenticated({ request, env, ctx }) { +function isAuthenticated({ request, env, appContext }) { // Ensure that this user is authenticated - if (!ctx.user) { + if (!appContext.user) { return new Response("Unauthorized", { status: 401 }) } } @@ -134,9 +134,9 @@ For the `/blog/:slug/edit` route, the `isAuthenticated` function will be execute --- ``` -## Middleware & Context +## Middleware & `AppContext` -Context is a mutable object that is passed to each request handler, interruptors, and React Server Functions. It's used to share data between the different parts of your application. You populate the context on a per-request basis via Middleware. +`appContext` is a mutable object that is passed to each request handler, interruptors, and React Server Functions. It's used to share data between the different parts of your application. You populate the context on a per-request basis via Middleware. Middleware runs before the request is matched to a route. You can specify multiple middleware functions, they'll be executed in the order they are defined. @@ -146,53 +146,52 @@ import { route } from "@redwoodjs/sdk/router"; defineApp([ sessionMiddleware, - async function getUserMiddleware({ request, env, ctx }) { - if (ctx.session.userId) { - ctx.user = await db.user.find({ where: { id: ctx.session.userId } }); + async function getUserMiddleware({ request, env, appContext }) { + if (appContext.session.userId) { + appContext.user = await db.user.find({ where: { id: appContext.session.userId } }); } }, route("/hello", [ - function ({ ctx }) { - if (!ctx.user) { + function ({ appContext }) { + if (!appContext.user) { return new Response("Unauthorized", { status: 401 }); } }, - function ({ ctx }) { - return new Response(`Hello ${ctx.user.username}!`); + function ({ appContext }) { + return new Response(`Hello ${appContext.user.username}!`); }, ]), ]); --- -The `ctx` object: +The `appContext` object: -1. `sessionMiddleware` is a function that is used to populate the `ctx.session` object -2. `getUserMiddleware` is a middleware function that is used to populate the `ctx.user` object +1. `sessionMiddleware` is a function that is used to populate the `appContext.session` object +2. `getUserMiddleware` is a middleware function that is used to populate the `appContext.user` object 3. `"/hello"` is a an array of route handlers that are executed when "/hello" is matched: - if the user is not authenticated the request will be interrupted and a 401 Unauthorized response will be returned - - if the user is authenticated the request will be passed to the next request handler and `"Hello {ctx.user.username}!"` will be returned + - if the user is authenticated the request will be passed to the next request handler and `"Hello {appContext.user.username}!"` will be returned --- ``` ## Documents -Documents are how you define the "shell" of your application's html: the ``, ``, `` tags, scripts, stylesheets, ``, and where in the `` your actual page content is rendered. In RedwoodSDK, documents are defined using the `document` function in `defineApp`. +Documents are how you define the "shell" of your application's html: the ``, ``, `` tags, scripts, stylesheets, ``, and where in the `` your actual page content is rendered. In RedwoodSDK, you tell it which document to use with the `render()` function in `defineApp`. In other words, you're asking RedwoodSDK to "render" the document. ```tsx title="src/worker.tsx" "document" import { defineApp } from "@redwoodjs/sdk/worker"; -import { route, document } from "@redwoodjs/sdk/router"; +import { route, render } from "@redwoodjs/sdk/router"; import { Document } from "@/pages/Document"; import { HomePage } from "@/pages/HomePage"; export default defineApp([ - document( - Document, [route("/", HomePage)] -)]); + render(Document, [route("/", HomePage)]) +]); --- -The `document` function takes a React component and an array of route handlers. The document will be applied to all the routes that are passed to it. +The `render` function takes a React component and an array of route handlers. The document will be applied to all the routes that are passed to it. This component will be rendered on the server side when the page loads. When defining this component, you'd add: * Your application's stylesheets and scripts diff --git a/docs/src/content/docs/core/security.mdx b/docs/src/content/docs/core/security.mdx index 9c3eb5e04..b7f76c1f2 100644 --- a/docs/src/content/docs/core/security.mdx +++ b/docs/src/content/docs/core/security.mdx @@ -73,9 +73,9 @@ export const Document = ({ rw, children }) => ( ); -export default defineApp([ +export default defineApp([ // ... - document(Document, [ + render(Document, [ // ... ]), ]); diff --git a/docs/src/content/docs/getting-started/first-project.mdx b/docs/src/content/docs/getting-started/first-project.mdx index 08451302d..307458c35 100644 --- a/docs/src/content/docs/getting-started/first-project.mdx +++ b/docs/src/content/docs/getting-started/first-project.mdx @@ -70,12 +70,12 @@ import { Home } from 'src/pages/Home'; ... -export default defineApp([ - ({ ctx }) => { - // setup ctx here - ctx; +export default defineApp([ + ({ appContext }) => { + // setup appContext here + appContext; }, - document(Document, [ + render(Document, [ index([ Home, ]), @@ -114,12 +114,12 @@ You can add additional routes by: ```tsx import { About } from "src/pages/About"; -export default defineApp([ - ({ ctx }) => { - // setup ctx here - ctx; +export default defineApp([ + ({ appContext }) => { + // setup appContext here + appContext; }, - document(Document, [index([Home]), route("/about", About)]), + render(Document, [index([Home]), route("/about", About)]), ]); ``` diff --git a/docs/src/content/docs/reference/routing.mdx b/docs/src/content/docs/reference/routing.mdx index e8b277640..fd7306552 100644 --- a/docs/src/content/docs/reference/routing.mdx +++ b/docs/src/content/docs/reference/routing.mdx @@ -41,7 +41,7 @@ Route functions handle incoming requests and return either a Response object or - `request`: The incoming Request object - `params`: URL parameters parsed from the route definition - `env`: CloudFlare environment variables and bindings -- `ctx`: A mutable object for storing request-scoped data +- `appContext`: A mutable object for storing request-scoped data Here's a basic example: @@ -50,7 +50,7 @@ Here's a basic example: code={`\ import { route } from "@redwoodjs/sdk/router"; -route("/", function ({ request, params, env, ctx }) { +route("/", function ({ request, params, env, appContext }) { return new Response("Hello, world!", { status: 200 }); });`} /> @@ -82,8 +82,8 @@ import { route } from "@redwoodjs/sdk/router"; export default defineApp([ route("/user/settings", [ // Authentication check - function checkAuth({ ctx }) { - if (!ctx.user) { + function checkAuth({ appContext }) { + if (!appContext.user) { return Response.redirect("/user/login", 302); } }, @@ -96,7 +96,7 @@ export default defineApp([ The interruptors pattern complements the global middleware system, allowing you to apply route-specific checks without cluttering your main request functions. -Note: All request functions in the chain receive the same route context (`request`, `params`, `env`, `ctx`), making it easy to share data between functions. +Note: All request functions in the chain receive the same route context (`request`, `params`, `env`, `appContext`), making it easy to share data between functions. ## Global Middleware @@ -107,7 +107,7 @@ Global middleware allows you to inspect or modify every request and response in title="src/worker.tsx" code={`\ export default defineApp([ - async function getUserMiddleware({ request, env, ctx }) { + async function getUserMiddleware({ request, env, appContext }) { const session = getSession({ request, env }) try { const user = await db.user.findFirstOrThrow({ @@ -119,13 +119,13 @@ export default defineApp([ id: session?.userId } }) - ctx.user = user + appContext.user = user } catch { - ctx.user = null + appContext.user = null } }, - route('/', function({ ctx }) { - return new Response(\`You are logged in as "\${ctx.user.email}"\`) + route('/', function({ appContext }) { + return new Response(\`You are logged in as "\${appContext.user.email}"\`) }) ])`} /> @@ -134,9 +134,9 @@ In this example, we define a global middleware function `getUserMiddleware` that 1. Retrieves the user's session information 2. Attempts to fetch the user from the database using the session's userId -3. Stores the user object in the request context (`ctx`) +3. Stores the user object in the request context (`appContext`) -The `ctx` object is then available to all subsequent route handlers and middleware functions. +The `appContext` object is then available to all subsequent route handlers and middleware functions. ## JSX @@ -180,7 +180,7 @@ By default, RedwoodSDK only renders your component's HTML without ``, ` @@ -245,13 +245,13 @@ Here's an example using both helpers: language="tsx" code={`\ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document, prefix } from "@redwoodjs/sdk/router"; +import { index, render, prefix } from "@redwoodjs/sdk/router"; import { authRoutes } from 'src/pages/auth/routes'; import { invoiceRoutes } from 'src/pages/invoice/routes'; import HomePage from 'src/pages/Home/HomePage'; export default defineApp([ - document(Document, [ + render(Document, [ // Define root route using index index([ HomePage, diff --git a/experiments/ai-stream/src/worker.tsx b/experiments/ai-stream/src/worker.tsx index 29e0e7057..cf66bba87 100644 --- a/experiments/ai-stream/src/worker.tsx +++ b/experiments/ai-stream/src/worker.tsx @@ -1,12 +1,12 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document } from "@redwoodjs/sdk/router"; +import { index, render } from "@redwoodjs/sdk/router"; import { Document } from "src/Document"; import { Chat } from "src/pages/Chat/Chat"; import { setCommonHeaders } from "src/headers"; -type Context = {}; +type AppContext = {}; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - document(Document, [index([Chat])]), + render(Document, [index([Chat])]), ]); diff --git a/experiments/billable/docs/Routing.md b/experiments/billable/docs/Routing.md index 77d6254fd..4f4aebeda 100644 --- a/experiments/billable/docs/Routing.md +++ b/experiments/billable/docs/Routing.md @@ -49,10 +49,9 @@ const router = defineRoutes([ }) ]) -router.handle({ request, ctx, env, renderPage }) +router.handle({ request, appContext, env, renderPage }) ``` - # API Reference - `defineRoutes` @@ -60,7 +59,6 @@ router.handle({ request, ctx, env, renderPage }) - `index` - `prefix` - # Links We also include an interface to generate paths in a typesafe way. This allows you to confidently @@ -88,7 +86,6 @@ link('/invoice/:id', { id: 1 }) ``` - ## TODO - Type safety. How do we ensure that the params have types? Maybe the route array has some sort of response... Like the type that it returns is a function that returns a thing... That's interesting. @@ -96,7 +93,7 @@ link('/invoice/:id', { id: 1 }) Ok. That seems like a possible way forward. What else to consider? - Type casting? Should we consider have the ability to cast things via the router? Seems like an overreach to me. -Loaders. Stick with Suspense boundary. I kinda see the benefit of been able to declare this on the component itself... Or near the component. + Loaders. Stick with Suspense boundary. I kinda see the benefit of been able to declare this on the component itself... Or near the component. - Don't hide files. I want to be able to follow the request-response cycle in my own code. What does that mean? - We should expose the express (or something else) part of the framework. The user should invoke a function to pass the request off to Redwood SDK diff --git a/experiments/billable/src/app/pages/Home/HomePage.tsx b/experiments/billable/src/app/pages/Home/HomePage.tsx index a3fcae727..6d904aed9 100644 --- a/experiments/billable/src/app/pages/Home/HomePage.tsx +++ b/experiments/billable/src/app/pages/Home/HomePage.tsx @@ -1,9 +1,9 @@ import { RouteOptions } from "@redwoodjs/sdk/worker"; import { Layout } from "../Layout"; import { InvoiceForm } from "../invoice/DetailPage/InvoiceForm"; -export default function HomePage({ ctx }: RouteOptions) { +export default function HomePage({ appContext }: RouteOptions) { return ( - + ); diff --git a/experiments/billable/src/app/pages/Layout.tsx b/experiments/billable/src/app/pages/Layout.tsx index 6f2e65aaa..a738f2a26 100644 --- a/experiments/billable/src/app/pages/Layout.tsx +++ b/experiments/billable/src/app/pages/Layout.tsx @@ -44,14 +44,14 @@ function Header({ user }: { user?: User }) { export function Layout({ children, - ctx, + appContext, }: { children: React.ReactNode; - ctx: { user?: User }; + appContext: { user?: User }; }) { return (
    -
    +
    {children}
    diff --git a/experiments/billable/src/app/pages/auth/LoginPage.tsx b/experiments/billable/src/app/pages/auth/LoginPage.tsx index b388a7b5f..3891d6142 100644 --- a/experiments/billable/src/app/pages/auth/LoginPage.tsx +++ b/experiments/billable/src/app/pages/auth/LoginPage.tsx @@ -29,7 +29,7 @@ export function LoginPage(opts: RouteOptions) { }; return ( - +

    Continue with Email Address diff --git a/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceDetailPage.tsx b/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceDetailPage.tsx index 7eae9e6f8..874c32814 100644 --- a/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceDetailPage.tsx +++ b/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceDetailPage.tsx @@ -52,19 +52,19 @@ export async function getInvoice(id: string, userId: string) { export async function InvoiceDetailPage({ params, - ctx, + appContext, }: RouteOptions<{ id: string }>) { - const invoice = await getInvoice(params.id, ctx.user.id); + const invoice = await getInvoice(params.id, appContext.user.id); return ( - + Invoices Edit Invoice - + ); } diff --git a/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceForm.tsx b/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceForm.tsx index bbc9726c3..0bca65d97 100644 --- a/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceForm.tsx +++ b/experiments/billable/src/app/pages/invoice/DetailPage/InvoiceForm.tsx @@ -35,7 +35,7 @@ function calculateTaxes(subtotal: number, taxes: InvoiceTaxes[]) { export function InvoiceForm(props: { invoice: Awaited>; - ctx: RouteOptions["ctx"]; + appContext: RouteOptions["appContext"]; }) { const [invoice, setInvoice] = useState(props.invoice); const [items, setItems] = useState(props.invoice.items); @@ -47,7 +47,7 @@ export function InvoiceForm(props: { const pdfContentRef = useRef(null); - const isLoggedIn = props.ctx?.user; + const isLoggedIn = props.appContext?.user; return (
    diff --git a/experiments/billable/src/app/pages/invoice/DetailPage/functions.ts b/experiments/billable/src/app/pages/invoice/DetailPage/functions.ts index efea7839b..387b1ffd2 100644 --- a/experiments/billable/src/app/pages/invoice/DetailPage/functions.ts +++ b/experiments/billable/src/app/pages/invoice/DetailPage/functions.ts @@ -9,12 +9,12 @@ export async function saveInvoice( invoice: Omit, items: InvoiceItem[], taxes: InvoiceTaxes[], - { ctx }, + { appContext }, ) { await db.invoice.findFirstOrThrow({ where: { id, - userId: ctx.user.id, + userId: appContext.user.id, }, }); @@ -34,11 +34,11 @@ export async function saveInvoice( }); } -export async function deleteLogo(id: string, { ctx }) { +export async function deleteLogo(id: string, { appContext }) { await db.invoice.findFirstOrThrow({ where: { id, - userId: ctx.user.id, + userId: appContext.user.id, }, }); diff --git a/experiments/billable/src/app/pages/invoice/ListPage/InvoiceListPage.tsx b/experiments/billable/src/app/pages/invoice/ListPage/InvoiceListPage.tsx index 6dcbfae9d..02e1d5f5c 100644 --- a/experiments/billable/src/app/pages/invoice/ListPage/InvoiceListPage.tsx +++ b/experiments/billable/src/app/pages/invoice/ListPage/InvoiceListPage.tsx @@ -58,10 +58,10 @@ async function getInvoiceListSummary(userId: string) { }); } -export async function InvoiceListPage({ ctx }: RouteOptions) { - const invoices = await getInvoiceListSummary(ctx.user.id); +export async function InvoiceListPage({ appContext }: RouteOptions) { + const invoices = await getInvoiceListSummary(appContext.user.id); return ( - +
    diff --git a/experiments/billable/src/app/pages/invoice/ListPage/functions.ts b/experiments/billable/src/app/pages/invoice/ListPage/functions.ts index ffefa1cfd..a2ed285b3 100644 --- a/experiments/billable/src/app/pages/invoice/ListPage/functions.ts +++ b/experiments/billable/src/app/pages/invoice/ListPage/functions.ts @@ -1,11 +1,15 @@ "use server"; import { db } from "src/db"; -import { Context } from "../../../../worker"; +import { AppContext } from "../../../../worker"; // We need to pass the context to these somehow? -export async function createInvoice({ ctx }: { ctx: Context }) { - const userId = ctx.user.id; +export async function createInvoice({ + appContext, +}: { + appContext: AppContext; +}) { + const userId = appContext.user.id; // todo(peterp, 28-01-2025): Implement templates. let lastInvoice = await db.invoice.findFirst({ diff --git a/experiments/billable/src/app/pages/invoice/routes.ts b/experiments/billable/src/app/pages/invoice/routes.ts index f01784334..79b904f47 100644 --- a/experiments/billable/src/app/pages/invoice/routes.ts +++ b/experiments/billable/src/app/pages/invoice/routes.ts @@ -4,8 +4,8 @@ import { InvoiceDetailPage } from "./DetailPage/InvoiceDetailPage"; import { InvoiceListPage } from "./ListPage/InvoiceListPage"; import { link } from "src/shared/links"; -function isAuthenticated({ ctx }) { - if (!ctx.user) { +function isAuthenticated({ appContext }) { + if (!appContext.user) { return new Response(null, { status: 302, headers: { Location: link("/") }, @@ -27,7 +27,7 @@ export const invoiceRoutes = [ route("/:id", [isAuthenticated, InvoiceDetailPage]), route("/:id/upload", [ isAuthenticated, - async ({ request, params, env, ctx }) => { + async ({ request, params, env, appContext }) => { if ( request.method !== "POST" && !request.headers.get("content-type")?.includes("multipart/form-data") @@ -40,7 +40,7 @@ export const invoiceRoutes = [ const file = formData.get("file") as File; // Stream the file directly to R2 - const r2ObjectKey = `/invoice/logos/${ctx.user.id}/${params.id}-${Date.now()}-${file.name}`; + const r2ObjectKey = `/invoice/logos/${appContext.user.id}/${params.id}-${Date.now()}-${file.name}`; await env.R2.put(r2ObjectKey, file.stream(), { httpMetadata: { contentType: file.type, diff --git a/experiments/billable/src/worker.tsx b/experiments/billable/src/worker.tsx index c3e5d5554..7c5ab3bac 100644 --- a/experiments/billable/src/worker.tsx +++ b/experiments/billable/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document, prefix } from "@redwoodjs/sdk/router"; +import { index, render, prefix } from "@redwoodjs/sdk/router"; import { ExecutionContext } from "@cloudflare/workers-types"; import { link } from "src/shared/links"; @@ -12,7 +12,7 @@ import { sessions, setupSessionStore } from "./sessionStore"; export { SessionDO } from "./session"; -export type Context = { +export type AppContext = { user: Awaited>; }; @@ -33,16 +33,16 @@ export const getUser = async (request: Request) => { } }; -const app = defineApp([ - async ({ request, ctx, env }) => { +const app = defineApp([ + async ({ request, appContext, env }) => { await setupDb(env); setupSessionStore(env); - ctx.user = await getUser(request); + appContext.user = await getUser(request); }, - document(Document, [ + render(Document, [ index([ - ({ ctx }) => { - if (ctx.user) { + ({ appContext }) => { + if (appContext.user) { return new Response(null, { status: 302, headers: { Location: link("/invoice/list") }, @@ -57,7 +57,7 @@ const app = defineApp([ ]); export default { - fetch(request: Request, env: Env, ctx: ExecutionContext) { - return app.fetch(request, env, ctx); + fetch(request: Request, env: Env, cf: ExecutionContext) { + return app.fetch(request, env, cf); }, }; diff --git a/experiments/cutable/src/app/pages/Layout.tsx b/experiments/cutable/src/app/pages/Layout.tsx index 30a54daa8..9facc951c 100644 --- a/experiments/cutable/src/app/pages/Layout.tsx +++ b/experiments/cutable/src/app/pages/Layout.tsx @@ -28,14 +28,14 @@ function Header({ user }: { user?: User }) { export function Layout({ children, - ctx, + appContext, }: { children: React.ReactNode; - ctx: { user?: User }; + appContext: { user?: User }; }) { return (
    -
    +
    {children}
    diff --git a/experiments/cutable/src/app/pages/home/HomePage.tsx b/experiments/cutable/src/app/pages/home/HomePage.tsx index 9be6d43cb..51aab8364 100644 --- a/experiments/cutable/src/app/pages/home/HomePage.tsx +++ b/experiments/cutable/src/app/pages/home/HomePage.tsx @@ -2,9 +2,9 @@ import { RouteOptions } from "@redwoodjs/sdk/router"; import { Layout } from "../Layout"; import { CalculateSheets } from "./CalculateSheets"; // import { Test } from "./Test" -export default function HomePage({ ctx }: RouteOptions) { +export default function HomePage({ appContext }: RouteOptions) { return ( - + {/* */} diff --git a/experiments/cutable/src/app/pages/project/DetailPage/CutlistDetailPage.tsx b/experiments/cutable/src/app/pages/project/DetailPage/CutlistDetailPage.tsx index 91f70370d..f800648e2 100644 --- a/experiments/cutable/src/app/pages/project/DetailPage/CutlistDetailPage.tsx +++ b/experiments/cutable/src/app/pages/project/DetailPage/CutlistDetailPage.tsx @@ -11,9 +11,9 @@ import { findOptimalPacking, calculateFreeSpaces } from "./clientFunctions"; export default async function CutlistDetailPage({ params, - ctx, + appContext, }: RouteOptions<{ id: string }>) { - const project = await getProject(params.id, ctx.user.id); + const project = await getProject(params.id, appContext.user.id); const cutlistItems = JSON.parse( project.cutlistItems as string, ) as ProjectItem[]; @@ -72,7 +72,7 @@ export default async function CutlistDetailPage({ // boards? return ( - + Projects diff --git a/experiments/cutable/src/app/pages/project/DetailPage/ProjectDetailPage.tsx b/experiments/cutable/src/app/pages/project/DetailPage/ProjectDetailPage.tsx index 2f00537ec..03ed6f0ab 100644 --- a/experiments/cutable/src/app/pages/project/DetailPage/ProjectDetailPage.tsx +++ b/experiments/cutable/src/app/pages/project/DetailPage/ProjectDetailPage.tsx @@ -51,12 +51,12 @@ export async function updateProject( export default async function ProjectDetailPage({ params, - ctx, + appContext, }: RouteOptions<{ id: string }>) { - const project = await getProject(params.id, ctx.user.id); + const project = await getProject(params.id, appContext.user.id); return ( - + Projects diff --git a/experiments/cutable/src/app/pages/project/ListPage/ProjectListPage.tsx b/experiments/cutable/src/app/pages/project/ListPage/ProjectListPage.tsx index e4565581d..b4d675f99 100644 --- a/experiments/cutable/src/app/pages/project/ListPage/ProjectListPage.tsx +++ b/experiments/cutable/src/app/pages/project/ListPage/ProjectListPage.tsx @@ -61,10 +61,10 @@ async function getProjectListSummary(userId: string) { }); } -export default async function ProjectListPage({ ctx }: RouteOptions) { - const projects = await getProjectListSummary(ctx.user.id); +export default async function ProjectListPage({ appContext }: RouteOptions) { + const projects = await getProjectListSummary(appContext.user.id); return ( - +
    diff --git a/experiments/cutable/src/app/pages/project/ListPage/functions.ts b/experiments/cutable/src/app/pages/project/ListPage/functions.ts index ea65212f3..7b95fa411 100644 --- a/experiments/cutable/src/app/pages/project/ListPage/functions.ts +++ b/experiments/cutable/src/app/pages/project/ListPage/functions.ts @@ -5,11 +5,11 @@ import { getContext } from "../../../../worker"; // We need to pass the context to these somehow? export async function createProject({ - ctx, + appContext, }: { - ctx: Awaited>; + appContext: Awaited>; }) { - const userId = ctx.user.id; + const userId = appContext.user.id; const newProject = await db.project.create({ data: { diff --git a/experiments/cutable/src/worker.tsx b/experiments/cutable/src/worker.tsx index af79c54d0..d2ec246f8 100644 --- a/experiments/cutable/src/worker.tsx +++ b/experiments/cutable/src/worker.tsx @@ -1,21 +1,21 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document } from "@redwoodjs/sdk/router"; +import { index, render } from "@redwoodjs/sdk/router"; import { Document } from "src/Document"; import HomePage from "src/pages/home/HomePage"; export { SessionDO } from "./session"; -type Context = { +type AppContext = { foo: number; }; -export default defineApp([ +export default defineApp([ // >>> Replaces `getContext()` - ({ ctx }: { ctx: Context }) => { + ({ appContext }: { appContext: AppContext }) => { // >>> You can do side effects (e.g, setup like `setupDb()`) here // >>> You can set your context here - ctx.foo = 23; + appContext.foo = 23; }, // @ts-ignore - document(Document, [index([HomePage])]), + render(Document, [index([HomePage])]), ]); diff --git a/experiments/cutl/docs/Routing.md b/experiments/cutl/docs/Routing.md index 77d6254fd..4f4aebeda 100644 --- a/experiments/cutl/docs/Routing.md +++ b/experiments/cutl/docs/Routing.md @@ -49,10 +49,9 @@ const router = defineRoutes([ }) ]) -router.handle({ request, ctx, env, renderPage }) +router.handle({ request, appContext, env, renderPage }) ``` - # API Reference - `defineRoutes` @@ -60,7 +59,6 @@ router.handle({ request, ctx, env, renderPage }) - `index` - `prefix` - # Links We also include an interface to generate paths in a typesafe way. This allows you to confidently @@ -88,7 +86,6 @@ link('/invoice/:id', { id: 1 }) ``` - ## TODO - Type safety. How do we ensure that the params have types? Maybe the route array has some sort of response... Like the type that it returns is a function that returns a thing... That's interesting. @@ -96,7 +93,7 @@ link('/invoice/:id', { id: 1 }) Ok. That seems like a possible way forward. What else to consider? - Type casting? Should we consider have the ability to cast things via the router? Seems like an overreach to me. -Loaders. Stick with Suspense boundary. I kinda see the benefit of been able to declare this on the component itself... Or near the component. + Loaders. Stick with Suspense boundary. I kinda see the benefit of been able to declare this on the component itself... Or near the component. - Don't hide files. I want to be able to follow the request-response cycle in my own code. What does that mean? - We should expose the express (or something else) part of the framework. The user should invoke a function to pass the request off to Redwood SDK diff --git a/experiments/cutl/src/app/pages/Home/HomePage.tsx b/experiments/cutl/src/app/pages/Home/HomePage.tsx index ada3288ad..ba0e8d377 100644 --- a/experiments/cutl/src/app/pages/Home/HomePage.tsx +++ b/experiments/cutl/src/app/pages/Home/HomePage.tsx @@ -2,9 +2,9 @@ import { RouteOptions } from "../../../lib/router"; import { Layout } from "../Layout"; import { CalculateSheets } from "./CalculateSheets"; // import { Test } from "./Test" -export default function HomePage({ ctx }: RouteOptions) { +export default function HomePage({ appContext }: RouteOptions) { return ( - + {/* */} diff --git a/experiments/cutl/src/app/pages/Layout.tsx b/experiments/cutl/src/app/pages/Layout.tsx index e84c9482a..ae591bcda 100644 --- a/experiments/cutl/src/app/pages/Layout.tsx +++ b/experiments/cutl/src/app/pages/Layout.tsx @@ -29,14 +29,14 @@ function Header({ user }: { user?: User }) { export function Layout({ children, - ctx, + appContext, }: { children: React.ReactNode; - ctx: { user?: User }; + appContext: { user?: User }; }) { return (
    -
    +
    {children}
    diff --git a/experiments/cutl/src/app/pages/auth/LoginPage.tsx b/experiments/cutl/src/app/pages/auth/LoginPage.tsx index c68b78684..d1823b8cc 100644 --- a/experiments/cutl/src/app/pages/auth/LoginPage.tsx +++ b/experiments/cutl/src/app/pages/auth/LoginPage.tsx @@ -16,7 +16,7 @@ import { } from "../../components/ui/input-otp"; import { link } from "../../shared/links"; -export function LoginPage({ ctx }: RouteOptions) { +export function LoginPage({ appContext }: RouteOptions) { const [email, setEmail] = useState("her.stander@gmail.com"); const [isPending, startTransition] = useTransition(); const [success, setSuccess] = useState(false); @@ -30,7 +30,7 @@ export function LoginPage({ ctx }: RouteOptions) { }; return ( - +

    Continue with Email Address diff --git a/experiments/cutl/src/app/pages/project/DetailPage/CutlistDetailPage.tsx b/experiments/cutl/src/app/pages/project/DetailPage/CutlistDetailPage.tsx index 66d29fb2c..af8f0efe8 100644 --- a/experiments/cutl/src/app/pages/project/DetailPage/CutlistDetailPage.tsx +++ b/experiments/cutl/src/app/pages/project/DetailPage/CutlistDetailPage.tsx @@ -11,9 +11,9 @@ import { findOptimalPacking, calculateFreeSpaces } from "./clientFunctions"; export default async function CutlistDetailPage({ params, - ctx, + appContext, }: RouteOptions<{ id: string }>) { - const project = await getProject(params.id, ctx.user.id); + const project = await getProject(params.id, appContext.user.id); const cutlistItems = JSON.parse( project.cutlistItems as string, ) as ProjectItem[]; @@ -72,7 +72,7 @@ export default async function CutlistDetailPage({ // boards? return ( - + Projects diff --git a/experiments/cutl/src/app/pages/project/DetailPage/ProjectDetailPage.tsx b/experiments/cutl/src/app/pages/project/DetailPage/ProjectDetailPage.tsx index 2f00537ec..03ed6f0ab 100644 --- a/experiments/cutl/src/app/pages/project/DetailPage/ProjectDetailPage.tsx +++ b/experiments/cutl/src/app/pages/project/DetailPage/ProjectDetailPage.tsx @@ -51,12 +51,12 @@ export async function updateProject( export default async function ProjectDetailPage({ params, - ctx, + appContext, }: RouteOptions<{ id: string }>) { - const project = await getProject(params.id, ctx.user.id); + const project = await getProject(params.id, appContext.user.id); return ( - + Projects diff --git a/experiments/cutl/src/app/pages/project/ListPage/ProjectListPage.tsx b/experiments/cutl/src/app/pages/project/ListPage/ProjectListPage.tsx index e4565581d..b4d675f99 100644 --- a/experiments/cutl/src/app/pages/project/ListPage/ProjectListPage.tsx +++ b/experiments/cutl/src/app/pages/project/ListPage/ProjectListPage.tsx @@ -61,10 +61,10 @@ async function getProjectListSummary(userId: string) { }); } -export default async function ProjectListPage({ ctx }: RouteOptions) { - const projects = await getProjectListSummary(ctx.user.id); +export default async function ProjectListPage({ appContext }: RouteOptions) { + const projects = await getProjectListSummary(appContext.user.id); return ( - +
    diff --git a/experiments/cutl/src/app/pages/project/ListPage/functions.ts b/experiments/cutl/src/app/pages/project/ListPage/functions.ts index 82a6101e3..7b95fa411 100644 --- a/experiments/cutl/src/app/pages/project/ListPage/functions.ts +++ b/experiments/cutl/src/app/pages/project/ListPage/functions.ts @@ -1,15 +1,15 @@ -'use server'; - +"use server"; import { db } from "../../../../db"; import { getContext } from "../../../../worker"; - - // We need to pass the context to these somehow? -export async function createProject({ ctx }: { ctx: Awaited>}) { - - const userId = ctx.user.id +export async function createProject({ + appContext, +}: { + appContext: Awaited>; +}) { + const userId = appContext.user.id; const newProject = await db.project.create({ data: { @@ -22,15 +22,14 @@ export async function createProject({ ctx }: { ctx: Awaited> = { request: Request; params: TParams; env: Env; - ctx?: any; + appContext?: any; }; type RouteMiddleware = ( @@ -70,19 +70,19 @@ export function defineRoutes(routes: RouteDefinition[]): { routes: RouteDefinition[]; handle: ({ request, - ctx, + appContext, env, renderPage, }: { request: Request; - ctx: any; + appContext: any; env: Env; renderPage: (page: any, props: Record) => Promise; }) => Response | Promise; } { return { routes, - async handle({ request, ctx, env, renderPage }) { + async handle({ request, appContext, env, renderPage }) { try { const url = new URL(request.url); let path = url.pathname; @@ -122,7 +122,7 @@ export function defineRoutes(routes: RouteDefinition[]): { ); } - const r = await h({ request, params, ctx, env }); + const r = await h({ request, params, appContext, env }); if (r instanceof Response) { return r; } @@ -131,12 +131,15 @@ export function defineRoutes(routes: RouteDefinition[]): { if (isRouteComponent(handler)) { // TODO(peterp, 2025-01-30): Serialize the request - return await renderPage(handler as RouteComponent, { params, ctx }); + return await renderPage(handler as RouteComponent, { + params, + appContext, + }); } else { return await (handler({ request, params, - ctx, + appContext, env, }) as Promise); } diff --git a/experiments/cutl/src/register/worker.ts b/experiments/cutl/src/register/worker.ts index 70f5c6bc7..ff090dbd6 100644 --- a/experiments/cutl/src/register/worker.ts +++ b/experiments/cutl/src/register/worker.ts @@ -17,18 +17,22 @@ export function registerServerReference( return baseRegisterServerReference(action, id, name); } -export function registerClientReference>(id: string, exportName: string, target: Target) { +export function registerClientReference>( + id: string, + exportName: string, + target: Target, +) { const reference = baseRegisterClientReference({}, id, exportName); - return Object.defineProperties( - target, - { - ...Object.getOwnPropertyDescriptors(reference), - $$async: { value: true }, - }, - ); + return Object.defineProperties(target, { + ...Object.getOwnPropertyDescriptors(reference), + $$async: { value: true }, + }); } -export async function rscActionHandler(req: Request, ctx: any): Promise { +export async function rscActionHandler( + req: Request, + appContext: any, +): Promise { const url = new URL(req.url); const contentType = req.headers.get("content-type"); @@ -44,5 +48,5 @@ export async function rscActionHandler(req: Request, ctx: any): Promise throw new Error(`Action ${actionId} is not a function`); } - return action(...args, { ctx }); + return action(...args, { appContext }); } diff --git a/experiments/cutl/src/session.tsx b/experiments/cutl/src/session.tsx index 01e8ee3e2..212289176 100644 --- a/experiments/cutl/src/session.tsx +++ b/experiments/cutl/src/session.tsx @@ -1,9 +1,9 @@ import { DurableObject } from "cloudflare:workers"; -import { MAX_TOKEN_DURATION } from './constants'; +import { MAX_TOKEN_DURATION } from "./constants"; interface Session { userId: string; - createdAt: number + createdAt: number; } export class SessionDO extends DurableObject { @@ -17,7 +17,7 @@ export class SessionDO extends DurableObject { const session: Session = { userId, createdAt: Date.now(), - } + }; await this.ctx.storage.put("session", session); this.session = session; @@ -36,16 +36,16 @@ export class SessionDO extends DurableObject { // has been revoked. if (!session) { return { - error: 'Invalid session' - } + error: "Invalid session", + }; } // context(justinvdm, 2025-01-15): If the session is expired, we need to revoke it. if (session.createdAt + MAX_TOKEN_DURATION < Date.now()) { await this.revokeSession(); return { - error: 'Session expired' - } + error: "Session expired", + }; } this.session = session; diff --git a/experiments/cutl/src/worker.tsx b/experiments/cutl/src/worker.tsx index 011614fdb..19db494db 100644 --- a/experiments/cutl/src/worker.tsx +++ b/experiments/cutl/src/worker.tsx @@ -35,8 +35,8 @@ export const getContext = async ( }; }; -function authRequired({ ctx }: any) { - if (!ctx.user) { +function authRequired({ appContext }: any) { + if (!appContext.user) { return new Response("Unauthorized", { status: 401 }); } } @@ -47,8 +47,8 @@ export default { const router = defineRoutes([ // index([ - // function ({ ctx }) { - // if (ctx.user.id) { + // function ({ appContext }) { + // if (appContext.user.id) { // return new Response(null, { // status: 302, // headers: { Location: link('/project/list') }, @@ -63,7 +63,6 @@ export default { // ...prefix("/project", projectRoutes), ]); - try { setupDb(env); setupEnv(env); @@ -80,13 +79,13 @@ export default { // the request will not hang. This makes this issue particularly hard to debug. await db.$queryRaw`SELECT 1`; - let ctx: Awaited> = { - user: { id: '', email: '' } + let appContext: Awaited> = { + user: { id: "", email: "" }, }; let session: Awaited> | undefined; try { session = await getSession(request, env); - ctx = await getContext(session); + appContext = await getContext(session); } catch (e) { console.error("Error getting session", e); } @@ -96,7 +95,7 @@ export default { const isRSCActionHandler = url.searchParams.has("__rsc_action_id"); let actionResult: any; if (isRSCActionHandler) { - actionResult = await rscActionHandler(request, ctx); // maybe we should include params and ctx in the action handler? + actionResult = await rscActionHandler(request, appContext); // maybe we should include params and appContext in the action handler? } if (url.pathname.startsWith("/assets/")) { @@ -131,7 +130,7 @@ export default { const response = await router.handle({ request, - ctx, + appContext, env, renderPage, }); diff --git a/experiments/realtime-poc/src/app/pages/note/functions.ts b/experiments/realtime-poc/src/app/pages/note/functions.ts index 308ae2327..a3fe218a5 100644 --- a/experiments/realtime-poc/src/app/pages/note/functions.ts +++ b/experiments/realtime-poc/src/app/pages/note/functions.ts @@ -2,18 +2,18 @@ import { RouteOptions } from "@redwoodjs/sdk/router"; -export const getContent = async (key: string, ctx?: RouteOptions) => { - const doId = ctx!.env.NOTE_DURABLE_OBJECT.idFromName(key); - const noteDO = ctx!.env.NOTE_DURABLE_OBJECT.get(doId); +export const getContent = async (key: string, opts?: RouteOptions) => { + const doId = opts!.env.NOTE_DURABLE_OBJECT.idFromName(key); + const noteDO = opts!.env.NOTE_DURABLE_OBJECT.get(doId); return noteDO.getContent(); }; export const updateContent = async ( key: string, content: string, - ctx?: RouteOptions, + opts?: RouteOptions, ) => { - const doId = ctx!.env.NOTE_DURABLE_OBJECT.idFromName(key); - const noteDO = ctx!.env.NOTE_DURABLE_OBJECT.get(doId); + const doId = opts!.env.NOTE_DURABLE_OBJECT.idFromName(key); + const noteDO = opts!.env.NOTE_DURABLE_OBJECT.get(doId); await noteDO.setContent(content); }; diff --git a/experiments/realtime-poc/src/worker.tsx b/experiments/realtime-poc/src/worker.tsx index 9da6adbfd..54a54ffd2 100644 --- a/experiments/realtime-poc/src/worker.tsx +++ b/experiments/realtime-poc/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { route, document } from "@redwoodjs/sdk/router"; +import { route, render } from "@redwoodjs/sdk/router"; import { Document } from "@/app/Document"; import { setCommonHeaders } from "@/app/headers"; import { @@ -15,12 +15,12 @@ import Note from "./app/pages/note/Note"; export { RealtimeDurableObject } from "@redwoodjs/sdk/realtime/durableObject"; export { NoteDurableObject } from "@/noteDurableObject"; -export type Context = {}; +export type AppContext = {}; -export default defineApp([ +export default defineApp([ setCommonHeaders(), realtimeRoute((env) => env.REALTIME_DURABLE_OBJECT), - document(Document, [ + render(Document, [ route("/", () => { const randomName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals], diff --git a/experiments/yt-dos/src/worker.tsx b/experiments/yt-dos/src/worker.tsx index 652607455..6b4add0c4 100644 --- a/experiments/yt-dos/src/worker.tsx +++ b/experiments/yt-dos/src/worker.tsx @@ -1,19 +1,19 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document, route } from "@redwoodjs/sdk/router"; +import { index, render, route } from "@redwoodjs/sdk/router"; import { Document } from "src/Document"; import { HomePage } from "src/pages/Home"; import { fetchYoutubeVideos } from "src/pages/serverFunctions"; -type Context = { +type AppContext = { YT_API_KEY: string; }; -export default defineApp([ - ({ ctx }) => { - // setup ctx here - ctx; +export default defineApp([ + ({ appContext }) => { + // setup appContext here + appContext; }, // @ts-ignore - document(Document, [ + render(Document, [ index([HomePage]), route("/sitemap.xml", async () => { const sitemap = ` diff --git a/experiments/zoomshare/src/app/pages/auth/functions.ts b/experiments/zoomshare/src/app/pages/auth/functions.ts index d98b74daf..9d4ee675d 100644 --- a/experiments/zoomshare/src/app/pages/auth/functions.ts +++ b/experiments/zoomshare/src/app/pages/auth/functions.ts @@ -15,9 +15,9 @@ import { db } from "@/db"; export async function startPasskeyRegistration( username: string, - ctx?: RouteOptions, + appContext?: RouteOptions, ) { - const { headers, env } = ctx!; + const { headers, env } = appContext!; const options = await generateRegistrationOptions({ rpName: env.APP_NAME, @@ -39,9 +39,9 @@ export async function startPasskeyRegistration( export async function finishPasskeyRegistration( username: string, registration: RegistrationResponseJSON, - ctx?: RouteOptions, + appContext?: RouteOptions, ) { - const { request, headers, env } = ctx!; + const { request, headers, env } = appContext!; const { origin } = new URL(request.url); const session = await sessions.load(request); @@ -82,8 +82,8 @@ export async function finishPasskeyRegistration( return true; } -export async function startPasskeyLogin(ctx?: RouteOptions) { - const { request, headers, env } = ctx!; +export async function startPasskeyLogin(appContext?: RouteOptions) { + const { request, headers, env } = appContext!; const options = await generateAuthenticationOptions({ rpID: env.RP_ID, @@ -98,9 +98,9 @@ export async function startPasskeyLogin(ctx?: RouteOptions) { export async function finishPasskeyLogin( login: AuthenticationResponseJSON, - ctx?: RouteOptions, + appContext?: RouteOptions, ) { - const { request, headers, env } = ctx!; + const { request, headers, env } = appContext!; const { origin } = new URL(request.url); const session = await sessions.load(request); diff --git a/experiments/zoomshare/src/app/pages/meetings/MeetingList.tsx b/experiments/zoomshare/src/app/pages/meetings/MeetingList.tsx index 2221a4045..5e91f2ce8 100644 --- a/experiments/zoomshare/src/app/pages/meetings/MeetingList.tsx +++ b/experiments/zoomshare/src/app/pages/meetings/MeetingList.tsx @@ -1,7 +1,7 @@ -import type { Context } from "@/worker"; +import type { AppContext } from "@/worker"; import { db } from "@/db"; -export async function MeetingList({ ctx }: { ctx: Context }) { +export async function MeetingList({ appContext }: { appContext: AppContext }) { const meetings = await db.meeting.findMany({ orderBy: { createdAt: "desc", diff --git a/experiments/zoomshare/src/worker.tsx b/experiments/zoomshare/src/worker.tsx index 71a4b4dd5..5b97df629 100644 --- a/experiments/zoomshare/src/worker.tsx +++ b/experiments/zoomshare/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { document, prefix, route } from "@redwoodjs/sdk/router"; +import { render, prefix, route } from "@redwoodjs/sdk/router"; import { Document } from "@/app/Document"; import { authRoutes } from "@/app/pages/auth/routes"; import { Session } from "./session/durableObject"; @@ -10,7 +10,7 @@ export { SessionDurableObject } from "./session/durableObject"; import crypto from "node:crypto"; import { meetingRoutes } from "./app/pages/meetings/routes"; -export type Context = { +export type AppContext = { session: Session | null; user: User; }; @@ -36,12 +36,12 @@ async function validateZoomWebhook(body: any, env: Env) { } } -const app = defineApp([ - async ({ env, ctx, request }) => { +const app = defineApp([ + async ({ env, appContext, request }) => { await setupDb(env); }, - document(Document, [ + render(Document, [ route("/", function () { return new Response("Hello World"); }), @@ -49,7 +49,7 @@ const app = defineApp([ ]), prefix("/webhook", [ route("/meeting.recording.completed", [ - async function ({ request, env, ctx }) { + async function ({ request, env, appContext }) { if ( request.method !== "POST" && request.headers.get("Content-Type") !== "application/json" diff --git a/sdk/src/runtime/lib/router.ts b/sdk/src/runtime/lib/router.ts index 33375ec21..e6fb35814 100644 --- a/sdk/src/runtime/lib/router.ts +++ b/sdk/src/runtime/lib/router.ts @@ -1,88 +1,94 @@ import { isValidElementType } from "react-is"; import { ExecutionContext } from "@cloudflare/workers-types"; -export type HandlerOptions> = { +export type HandlerOptions> = { request: Request; env: Env; cf: ExecutionContext; - ctx: TContext; + appContext: TAppContext; headers: Headers; - rw: RwContext; + rw: RwContext; }; -export type RouteOptions, TParams = any> = { +export type RouteOptions, TParams = any> = { cf: ExecutionContext; request: Request; params: TParams; env: Env; - ctx: TContext; + appContext: TAppContext; headers: Headers; - rw: RwContext; + rw: RwContext; }; -export type PageProps = Omit< - RouteOptions, +export type PageProps = Omit< + RouteOptions, "request" | "headers" | "rw" | "cf" > & { rw: { nonce: string } }; -export type DocumentProps = PageProps & { +export type DocumentProps = PageProps & { children: React.ReactNode; }; -export type RenderPageParams = { +export type RenderPageParams = { Page: React.FC>; - props: PageProps; + props: PageProps; actionResult: unknown; - Document: React.FC>; + Document: React.FC>; }; -export type RenderPage = ( - params: RenderPageParams, +export type RenderPage = ( + params: RenderPageParams, ) => Promise; -export type RwContext = { +export type RwContext = { nonce: string; - Document: React.FC>; - renderPage: RenderPage; - handleAction: (opts: RouteOptions) => Promise; + Document: React.FC>; + renderPage: RenderPage; + handleAction: (opts: RouteOptions) => Promise; }; -export type RouteMiddleware = ( - opts: RouteOptions, +export type RouteMiddleware = ( + opts: RouteOptions, ) => | Response | Promise | void | Promise | Promise; -type RouteFunction = ( - opts: RouteOptions, +type RouteFunction = ( + opts: RouteOptions, ) => Response | Promise; -type RouteComponent = ( - opts: RouteOptions, +type RouteComponent = ( + opts: RouteOptions, ) => JSX.Element | Promise; -type RouteHandler = - | RouteFunction - | RouteComponent +type RouteHandler = + | RouteFunction + | RouteComponent | [ - ...RouteMiddleware[], - RouteFunction | RouteComponent, + ...RouteMiddleware[], + ( + | RouteFunction + | RouteComponent + ), ]; -export type Route = - | RouteMiddleware - | RouteDefinition - | Array>; +export type Route = + | RouteMiddleware + | RouteDefinition + | Array>; -export type RouteDefinition, TParams = any> = { +export type RouteDefinition< + TAppContext = Record, + TParams = any, +> = { path: string; - handler: RouteHandler; + handler: RouteHandler; }; -type RouteMatch, TParams = any> = { +type RouteMatch, TParams = any> = { params: TParams; - handler: RouteHandler; + handler: RouteHandler; }; function matchPath( @@ -121,41 +127,41 @@ function matchPath( return params; } -function flattenRoutes( - routes: Route[], -): (RouteMiddleware | RouteDefinition)[] { - return routes.reduce((acc: Route[], route) => { +function flattenRoutes( + routes: Route[], +): (RouteMiddleware | RouteDefinition)[] { + return routes.reduce((acc: Route[], route) => { if (Array.isArray(route)) { return [...acc, ...flattenRoutes(route)]; } return [...acc, route]; - }, []) as (RouteMiddleware | RouteDefinition)[]; + }, []) as (RouteMiddleware | RouteDefinition)[]; } -export function defineRoutes>( - routes: Route[], +export function defineRoutes>( + routes: Route[], ): { - routes: Route[]; + routes: Route[]; handle: ({ cf, request, - ctx, + appContext, env, rw, headers, }: { cf: ExecutionContext; request: Request; - ctx: TContext; + appContext: TAppContext; env: Env; - rw: RwContext; + rw: RwContext; headers: Headers; }) => Response | Promise; } { const flattenedRoutes = flattenRoutes(routes); return { routes: flattenedRoutes, - async handle({ cf, request, ctx, env, rw, headers }) { + async handle({ cf, request, appContext, env, rw, headers }) { const url = new URL(request.url); let path = url.pathname; @@ -165,12 +171,12 @@ export function defineRoutes>( } // Find matching route - let match: RouteMatch | null = null; - const routeOptions: RouteOptions = { + let match: RouteMatch | null = null; + const routeOptions: RouteOptions = { cf, request, params: {}, - ctx, + appContext, env, rw, headers, @@ -209,7 +215,7 @@ export function defineRoutes>( const props = { params, env, - ctx, + appContext, rw: { nonce: rw.nonce }, }; return await rw.renderPage({ @@ -234,10 +240,10 @@ export function defineRoutes>( }; } -export function route( +export function route( path: string, - handler: RouteHandler, -): RouteDefinition { + handler: RouteHandler, +): RouteDefinition { if (!path.endsWith("/")) { path = path + "/"; } @@ -248,16 +254,16 @@ export function route( }; } -export function index( - handler: RouteHandler, -): RouteDefinition { +export function index( + handler: RouteHandler, +): RouteDefinition { return route("/", handler); } -export function prefix( +export function prefix( prefix: string, - routes: ReturnType>[], -): RouteDefinition[] { + routes: ReturnType>[], +): RouteDefinition[] { return routes.map((r) => { return { path: prefix + r.path, @@ -266,11 +272,11 @@ export function prefix( }); } -export function document( +export function render( Document: React.FC<{ children: React.ReactNode }>, - routes: Route[], -): Route[] { - const documentMiddleware: RouteMiddleware = ({ rw }) => { + routes: Route[], +): Route[] { + const documentMiddleware: RouteMiddleware = ({ rw }) => { rw.Document = Document; }; diff --git a/sdk/src/runtime/register/worker.ts b/sdk/src/runtime/register/worker.ts index f1ae24817..9ea97e8fc 100644 --- a/sdk/src/runtime/register/worker.ts +++ b/sdk/src/runtime/register/worker.ts @@ -31,9 +31,9 @@ export function registerClientReference>( }); } -export async function rscActionHandler( +export async function rscActionHandler( req: Request, - opts: HandlerOptions, + opts: HandlerOptions, ): Promise { const url = new URL(req.url); const contentType = req.headers.get("content-type"); diff --git a/sdk/src/runtime/worker.tsx b/sdk/src/runtime/worker.tsx index aa9d0067a..0e25da1b4 100644 --- a/sdk/src/runtime/worker.tsx +++ b/sdk/src/runtime/worker.tsx @@ -24,7 +24,7 @@ declare global { }; } -export const defineApp = (routes: Route[]) => { +export const defineApp = (routes: Route[]) => { return { fetch: async (request: Request, env: Env, cf: ExecutionContext) => { globalThis.__webpack_require__ = ssrWebpackRequire; @@ -54,12 +54,12 @@ export const defineApp = (routes: Route[]) => { const isRSCRequest = url.searchParams.has("__rsc"); const handleAction = async ( - opts: RouteOptions>, + opts: RouteOptions>, ) => { const isRSCActionHandler = url.searchParams.has("__rsc_action_id"); if (isRSCActionHandler) { - return await rscActionHandler(request, opts); // maybe we should include params and ctx in the action handler? + return await rscActionHandler(request, opts); // maybe we should include params and appContext in the action handler? } }; @@ -68,7 +68,7 @@ export const defineApp = (routes: Route[]) => { props: fullPageProps, actionResult, Document, - }: RenderPageParams) => { + }: RenderPageParams) => { let props = fullPageProps; let documentProps = fullPageProps; @@ -77,8 +77,8 @@ export const defineApp = (routes: Route[]) => { if ( Object.prototype.hasOwnProperty.call(Page, "$$isClientReference") ) { - const { ctx, params } = fullPageProps; - props = { ctx, params } as PageProps; + const { appContext, params } = fullPageProps; + props = { appContext, params } as PageProps; } if ( @@ -87,8 +87,8 @@ export const defineApp = (routes: Route[]) => { "$$isClientReference", ) ) { - const { ctx, params } = fullPageProps; - documentProps = { ctx, params } as DocumentProps; + const { appContext, params } = fullPageProps; + documentProps = { appContext, params } as DocumentProps; } const nonce = fullPageProps.rw.nonce; @@ -133,7 +133,7 @@ export const defineApp = (routes: Route[]) => { cf, request, headers: userHeaders, - ctx: {} as Context, + appContext: {} as AppContext, env, rw: { Document: DefaultDocument, diff --git a/starters/drizzle/src/app/pages/Home.tsx b/starters/drizzle/src/app/pages/Home.tsx index 1cc90f797..cb70166b2 100644 --- a/starters/drizzle/src/app/pages/Home.tsx +++ b/starters/drizzle/src/app/pages/Home.tsx @@ -1,9 +1,9 @@ import { users } from "../../db/schema"; import { RouteOptions } from "@redwoodjs/sdk/router"; -import { Context } from "@/worker"; +import { AppContext } from "@/worker"; -export async function Home({ ctx }: RouteOptions) { - const allUsers = await ctx.db.select().from(users).all(); +export async function Home({ appContext }: RouteOptions) { + const allUsers = await appContext.db.select().from(users).all(); return (

    Hello World

    diff --git a/starters/drizzle/src/worker.tsx b/starters/drizzle/src/worker.tsx index 50c846541..371e027c9 100644 --- a/starters/drizzle/src/worker.tsx +++ b/starters/drizzle/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document } from "@redwoodjs/sdk/router"; +import { index, render } from "@redwoodjs/sdk/router"; import { Document } from "src/Document"; import { Home } from "src/pages/Home"; import { setCommonHeaders } from "src/headers"; @@ -9,15 +9,15 @@ export interface Env { DB: D1Database; } -export type Context = { +export type AppContext = { db: ReturnType; }; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - ({ ctx, env }) => { - // setup db in ctx - ctx.db = drizzle(env.DB); + ({ appContext, env }) => { + // setup db in appContext + appContext.db = drizzle(env.DB); }, - document(Document, [index([Home])]), + render(Document, [index([Home])]), ]); diff --git a/starters/minimal/src/worker.tsx b/starters/minimal/src/worker.tsx index 74d3679d4..c98f17083 100644 --- a/starters/minimal/src/worker.tsx +++ b/starters/minimal/src/worker.tsx @@ -1,16 +1,16 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document } from "@redwoodjs/sdk/router"; +import { index, render } from "@redwoodjs/sdk/router"; import { Document } from "src/Document"; import { Home } from "src/pages/Home"; import { setCommonHeaders } from "src/headers"; -type Context = {}; +type AppContext = {}; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - ({ ctx }) => { - // setup ctx here - ctx; + ({ appContext }) => { + // setup appContext here + appContext; }, - document(Document, [index([Home])]), + render(Document, [index([Home])]), ]); diff --git a/starters/passkey-auth/src/app/pages/Home.tsx b/starters/passkey-auth/src/app/pages/Home.tsx index 1294c68d2..032763d12 100644 --- a/starters/passkey-auth/src/app/pages/Home.tsx +++ b/starters/passkey-auth/src/app/pages/Home.tsx @@ -1,11 +1,11 @@ -import { Context } from "@/worker"; +import { AppContext } from "@/worker"; -export function Home({ ctx }: { ctx: Context }) { +export function Home({ appContext }: { appContext: AppContext }) { return (

    - {ctx.user?.username - ? `You are logged in as user ${ctx.user.username}` + {appContext.user?.username + ? `You are logged in as user ${appContext.user.username}` : "You are not logged in"}

    diff --git a/starters/passkey-auth/src/worker.tsx b/starters/passkey-auth/src/worker.tsx index db5e5daf0..1b17cb2eb 100644 --- a/starters/passkey-auth/src/worker.tsx +++ b/starters/passkey-auth/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp, ErrorResponse } from "@redwoodjs/sdk/worker"; -import { index, document, prefix } from "@redwoodjs/sdk/router"; +import { index, render, prefix } from "@redwoodjs/sdk/router"; import { Document } from "@/app/Document"; import { Home } from "@/app/pages/Home"; import { setCommonHeaders } from "@/app/headers"; @@ -10,19 +10,19 @@ import { db, setupDb } from "./db"; import { User } from "@prisma/client"; export { SessionDurableObject } from "./session/durableObject"; -export type Context = { +export type AppContext = { session: Session | null; user: User | null; }; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - async ({ env, ctx, request, headers }) => { + async ({ env, appContext, request, headers }) => { await setupDb(env); setupSessionStore(env); try { - ctx.session = await sessions.load(request); + appContext.session = await sessions.load(request); } catch (error) { if (error instanceof ErrorResponse && error.code === 401) { await sessions.remove(request, headers); @@ -37,18 +37,18 @@ export default defineApp([ throw error; } - if (ctx.session?.userId) { - ctx.user = await db.user.findUnique({ + if (appContext.session?.userId) { + appContext.user = await db.user.findUnique({ where: { - id: ctx.session.userId, + id: appContext.session.userId, }, }); } }, - document(Document, [ + render(Document, [ index([ - ({ ctx }) => { - if (!ctx.user) { + ({ appContext }) => { + if (!appContext.user) { return new Response(null, { status: 302, headers: { Location: "/user/login" }, diff --git a/starters/prisma/src/worker.tsx b/starters/prisma/src/worker.tsx index 548cbae0d..c47ed101c 100644 --- a/starters/prisma/src/worker.tsx +++ b/starters/prisma/src/worker.tsx @@ -1,15 +1,15 @@ import { defineApp } from "@redwoodjs/sdk/worker"; -import { index, document } from "@redwoodjs/sdk/router"; +import { index, render } from "@redwoodjs/sdk/router"; import { Document } from "src/Document"; import { Home } from "src/pages/Home"; import { setupDb } from "./db"; import { setCommonHeaders } from "src/headers"; -type Context = {}; +type AppContext = {}; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - async ({ ctx, env, request }) => { + async ({ appContext, env, request }) => { await setupDb(env); }, - document(Document, [index([Home])]), + render(Document, [index([Home])]), ]); diff --git a/starters/sessions/src/app/pages/Home.tsx b/starters/sessions/src/app/pages/Home.tsx index e16059958..32716f677 100644 --- a/starters/sessions/src/app/pages/Home.tsx +++ b/starters/sessions/src/app/pages/Home.tsx @@ -1,11 +1,11 @@ -import { Context } from "@/worker"; +import { AppContext } from "@/worker"; -export function Home({ ctx }: { ctx: Context }) { +export function Home({ appContext }: { appContext: AppContext }) { return (

    - {ctx.session?.userId - ? `You are logged in as user ${ctx.session.userId}` + {appContext.session?.userId + ? `You are logged in as user ${appContext.session.userId}` : "You are not logged in"}

    diff --git a/starters/sessions/src/worker.tsx b/starters/sessions/src/worker.tsx index e285004ba..94e11b65d 100644 --- a/starters/sessions/src/worker.tsx +++ b/starters/sessions/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp, ErrorResponse } from "@redwoodjs/sdk/worker"; -import { index, document, prefix } from "@redwoodjs/sdk/router"; +import { index, render, prefix } from "@redwoodjs/sdk/router"; import { Document } from "@/app/Document"; import { Home } from "@/app/pages/Home"; import { userRoutes } from "@/app/pages/user/routes"; @@ -9,17 +9,17 @@ import { setCommonHeaders } from "./app/headers"; export { SessionDurableObject } from "./session/durableObject"; -export type Context = { +export type AppContext = { session: Session | null; }; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - async ({ env, ctx, request, headers }) => { + async ({ env, appContext, request, headers }) => { setupSessionStore(env); try { - ctx.session = await sessions.load(request); + appContext.session = await sessions.load(request); } catch (error) { if (error instanceof ErrorResponse && error.code === 401) { await sessions.remove(request, headers); @@ -32,5 +32,5 @@ export default defineApp([ } } }, - document(Document, [index([Home]), prefix("/user", userRoutes)]), + render(Document, [index([Home]), prefix("/user", userRoutes)]), ]); diff --git a/starters/standard/src/app/pages/Home.tsx b/starters/standard/src/app/pages/Home.tsx index 1294c68d2..032763d12 100644 --- a/starters/standard/src/app/pages/Home.tsx +++ b/starters/standard/src/app/pages/Home.tsx @@ -1,11 +1,11 @@ -import { Context } from "@/worker"; +import { AppContext } from "@/worker"; -export function Home({ ctx }: { ctx: Context }) { +export function Home({ appContext }: { appContext: AppContext }) { return (

    - {ctx.user?.username - ? `You are logged in as user ${ctx.user.username}` + {appContext.user?.username + ? `You are logged in as user ${appContext.user.username}` : "You are not logged in"}

    diff --git a/starters/standard/src/worker.tsx b/starters/standard/src/worker.tsx index a412a57dd..1a2dd625d 100644 --- a/starters/standard/src/worker.tsx +++ b/starters/standard/src/worker.tsx @@ -1,5 +1,5 @@ import { defineApp, ErrorResponse } from "@redwoodjs/sdk/worker"; -import { route, document, prefix } from "@redwoodjs/sdk/router"; +import { route, render, prefix } from "@redwoodjs/sdk/router"; import { Document } from "@/app/Document"; import { Home } from "@/app/pages/Home"; import { setCommonHeaders } from "@/app/headers"; @@ -10,19 +10,19 @@ import { db, setupDb } from "./db"; import type { User } from "@prisma/client"; export { SessionDurableObject } from "./session/durableObject"; -export type Context = { +export type AppContext = { session: Session | null; user: User | null; }; -export default defineApp([ +export default defineApp([ setCommonHeaders(), - async ({ env, ctx, request, headers }) => { + async ({ env, appContext, request, headers }) => { await setupDb(env); setupSessionStore(env); try { - ctx.session = await sessions.load(request); + appContext.session = await sessions.load(request); } catch (error) { if (error instanceof ErrorResponse && error.code === 401) { await sessions.remove(request, headers); @@ -37,19 +37,19 @@ export default defineApp([ throw error; } - if (ctx.session?.userId) { - ctx.user = await db.user.findUnique({ + if (appContext.session?.userId) { + appContext.user = await db.user.findUnique({ where: { - id: ctx.session.userId, + id: appContext.session.userId, }, }); } }, - document(Document, [ + render(Document, [ route("/", () => new Response("Hello, World!")), route("/protected", [ - ({ ctx }) => { - if (!ctx.user) { + ({ appContext }) => { + if (!appContext.user) { return new Response(null, { status: 302, headers: { Location: "/user/login" },