forked from calcom/cal.com
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
the /intuita/app-router-migration squashed commit
- Loading branch information
1 parent
6848362
commit b802bc5
Showing
135 changed files
with
1,893 additions
and
202 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { getBucket } from "abTest/utils"; | ||
import type { NextFetchEvent, NextMiddleware, NextRequest } from "next/server"; | ||
import { NextResponse } from "next/server"; | ||
import z from "zod"; | ||
|
||
const ROUTE_MAP = new Map<string, boolean>([ | ||
["/event-types", Boolean(process.env.APP_ROUTER_EVENT_TYPES_ENABLED)] as const, | ||
]); | ||
|
||
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override"; | ||
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled"; | ||
|
||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]).default("legacy"); | ||
|
||
export const abTestMiddlewareFactory = | ||
(next: NextMiddleware): NextMiddleware => | ||
async (req: NextRequest, event: NextFetchEvent) => { | ||
const { pathname } = req.nextUrl; | ||
|
||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME); | ||
|
||
const enabled = ROUTE_MAP.has(pathname) ? (ROUTE_MAP.get(pathname) ?? false) || override : false; | ||
|
||
if (pathname.includes("future") || !enabled) { | ||
return next(req, event); | ||
} | ||
|
||
const safeParsedBucket = override | ||
? { success: true as const, data: "future" as const } | ||
: bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value); | ||
|
||
if (!safeParsedBucket.success) { | ||
// cookie does not exist or it has incorrect value | ||
|
||
const res = NextResponse.next(); | ||
res.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, getBucket(), { expires: 1000 * 60 * 30 }); // 30 min in ms | ||
return res; | ||
} | ||
|
||
const bucketUrlPrefix = safeParsedBucket.data === "future" ? "future" : ""; | ||
|
||
const url = req.nextUrl.clone(); | ||
url.pathname = `${bucketUrlPrefix}${pathname}/`; | ||
return NextResponse.rewrite(url); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { AB_TEST_BUCKET_PROBABILITY } from "@calcom/lib/constants"; | ||
|
||
const cryptoRandom = () => { | ||
return crypto.getRandomValues(new Uint8Array(1))[0] / 0xff; | ||
}; | ||
|
||
export const getBucket = () => { | ||
return cryptoRandom() * 100 < AB_TEST_BUCKET_PROBABILITY ? "future" : "legacy"; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import NotFoundPage from "@pages/404"; | ||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; | ||
import { cookies, headers } from "next/headers"; | ||
|
||
import { getLocale } from "@calcom/features/auth/lib/getLocale"; | ||
|
||
import PageWrapper from "@components/PageWrapperAppDir"; | ||
|
||
const getProps = async (h: ReturnType<typeof headers>, c: ReturnType<typeof cookies>) => { | ||
// @ts-expect-error we cannot access ctx.req in app dir, however headers and cookies are only properties needed to extract the locale | ||
const locale = await getLocale({ headers: h, cookies: c }); | ||
|
||
const i18n = (await serverSideTranslations(locale)) || "en"; | ||
|
||
return { | ||
i18n, | ||
}; | ||
}; | ||
|
||
const NotFound = async () => { | ||
const h = headers(); | ||
const c = cookies(); | ||
|
||
const nonce = h.get("x-nonce") ?? undefined; | ||
|
||
const { i18n } = await getProps(h, c); | ||
|
||
return ( | ||
// @ts-expect-error withTrpc expects AppProps | ||
<PageWrapper requiresLicense={false} pageProps={{ i18n }} nonce={nonce} themeBasis={null} i18n={i18n}> | ||
<NotFoundPage /> | ||
</PageWrapper> | ||
); | ||
}; | ||
|
||
export default NotFound; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"use client"; | ||
|
||
import { createHydrateClient } from "app/_trpc/createHydrateClient"; | ||
import superjson from "superjson"; | ||
|
||
export const HydrateClient = createHydrateClient({ | ||
transformer: superjson, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { AppRouter } from "@calcom/trpc/server/routers/_app"; | ||
|
||
import { createTRPCReact } from "@trpc/react-query"; | ||
|
||
export const trpc = createTRPCReact<AppRouter>({}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
"use client"; | ||
|
||
import { type DehydratedState, Hydrate } from "@tanstack/react-query"; | ||
import { useMemo } from "react"; | ||
|
||
import type { DataTransformer } from "@trpc/server"; | ||
|
||
export function createHydrateClient(opts: { transformer?: DataTransformer }) { | ||
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) { | ||
const { state, children } = props; | ||
|
||
const transformedState: DehydratedState = useMemo(() => { | ||
if (opts.transformer) { | ||
return opts.transformer.deserialize(state); | ||
} | ||
return state; | ||
}, [state]); | ||
|
||
return <Hydrate state={transformedState}>{children}</Hydrate>; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team | ||
// file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts | ||
// repo link: https://github.com/trpc/next-13 | ||
// code is / will continue to be adapted for our usage | ||
import { dehydrate, QueryClient } from "@tanstack/query-core"; | ||
import type { DehydratedState, QueryKey } from "@tanstack/react-query"; | ||
|
||
import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc"; | ||
import { | ||
callProcedure, | ||
type AnyProcedure, | ||
type AnyQueryProcedure, | ||
type AnyRouter, | ||
type DataTransformer, | ||
type inferProcedureInput, | ||
type inferProcedureOutput, | ||
type inferRouterContext, | ||
type MaybePromise, | ||
type ProcedureRouterRecord, | ||
} from "@calcom/trpc/server"; | ||
|
||
import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared"; | ||
|
||
export function getArrayQueryKey( | ||
queryKey: string | [string] | [string, ...unknown[]] | unknown[], | ||
type: string | ||
): QueryKey { | ||
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey]; | ||
const [arrayPath, input] = queryKeyArrayed; | ||
|
||
if (!input && (!type || type === "any")) { | ||
return arrayPath.length ? [arrayPath] : ([] as unknown as QueryKey); | ||
} | ||
|
||
return [ | ||
arrayPath, | ||
{ | ||
...(typeof input !== "undefined" && { input: input }), | ||
...(type && type !== "any" && { type: type }), | ||
}, | ||
]; | ||
} | ||
|
||
// copy starts | ||
// copied from trpc/trpc repo | ||
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58 | ||
function transformQueryOrMutationCacheErrors< | ||
TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0] | ||
>(result: TState): TState { | ||
const error = result.state.error as Maybe<TRPCClientError<any>>; | ||
if (error instanceof Error && error.name === "TRPCClientError") { | ||
const newError: TRPCClientErrorLike<any> = { | ||
message: error.message, | ||
data: error.data, | ||
shape: error.shape, | ||
}; | ||
return { | ||
...result, | ||
state: { | ||
...result.state, | ||
error: newError, | ||
}, | ||
}; | ||
} | ||
return result; | ||
} | ||
// copy ends | ||
|
||
interface CreateTRPCNextLayoutOptions<TRouter extends AnyRouter> { | ||
router: TRouter; | ||
createContext: () => MaybePromise<inferRouterContext<TRouter>>; | ||
transformer?: DataTransformer; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export type DecorateProcedure<TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure | ||
? { | ||
fetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>; | ||
fetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>; | ||
prefetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>; | ||
prefetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>; | ||
} | ||
: never; | ||
|
||
type OmitNever<TType> = Pick< | ||
TType, | ||
{ | ||
[K in keyof TType]: TType[K] extends never ? never : K; | ||
}[keyof TType] | ||
>; | ||
/** | ||
* @internal | ||
*/ | ||
export type DecoratedProcedureRecord< | ||
TProcedures extends ProcedureRouterRecord, | ||
TPath extends string = "" | ||
> = OmitNever<{ | ||
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter | ||
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"], `${TPath}${TKey & string}.`> | ||
: TProcedures[TKey] extends AnyQueryProcedure | ||
? DecorateProcedure<TProcedures[TKey]> | ||
: never; | ||
}>; | ||
|
||
type CreateTRPCNextLayout<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter["_def"]["record"]> & { | ||
dehydrate(): Promise<DehydratedState>; | ||
queryClient: QueryClient; | ||
}; | ||
|
||
const getStateContainer = <TRouter extends AnyRouter>(opts: CreateTRPCNextLayoutOptions<TRouter>) => { | ||
let _trpc: { | ||
queryClient: QueryClient; | ||
context: inferRouterContext<TRouter>; | ||
} | null = null; | ||
|
||
return () => { | ||
if (_trpc === null) { | ||
_trpc = { | ||
context: opts.createContext(), | ||
queryClient: new QueryClient(), | ||
}; | ||
} | ||
|
||
return _trpc; | ||
}; | ||
}; | ||
|
||
export function createTRPCNextLayout<TRouter extends AnyRouter>( | ||
opts: CreateTRPCNextLayoutOptions<TRouter> | ||
): CreateTRPCNextLayout<TRouter> { | ||
const getState = getStateContainer(opts); | ||
|
||
const transformer = opts.transformer ?? { | ||
serialize: (v) => v, | ||
deserialize: (v) => v, | ||
}; | ||
|
||
return createFlatProxy((key) => { | ||
const state = getState(); | ||
const { queryClient } = state; | ||
if (key === "queryClient") { | ||
return queryClient; | ||
} | ||
|
||
if (key === "dehydrate") { | ||
// copy starts | ||
// copied from trpc/trpc repo | ||
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229 | ||
const dehydratedCache = dehydrate(queryClient, { | ||
shouldDehydrateQuery() { | ||
// makes sure errors are also dehydrated | ||
return true; | ||
}, | ||
}); | ||
|
||
// since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects | ||
const dehydratedCacheWithErrors = { | ||
...dehydratedCache, | ||
queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors), | ||
mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors), | ||
}; | ||
|
||
return () => transformer.serialize(dehydratedCacheWithErrors); | ||
} | ||
// copy ends | ||
|
||
return createRecursiveProxy(async (callOpts) => { | ||
const path = [key, ...callOpts.path]; | ||
const utilName = path.pop(); | ||
const ctx = await state.context; | ||
|
||
const caller = opts.router.createCaller(ctx); | ||
|
||
const pathStr = path.join("."); | ||
const input = callOpts.args[0]; | ||
|
||
if (utilName === "fetchInfinite") { | ||
return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () => | ||
caller.query(pathStr, input) | ||
); | ||
} | ||
|
||
if (utilName === "prefetch") { | ||
return queryClient.prefetchQuery({ | ||
queryKey: getArrayQueryKey([path, input], "query"), | ||
queryFn: async () => { | ||
const res = await callProcedure({ | ||
procedures: opts.router._def.procedures, | ||
path: pathStr, | ||
rawInput: input, | ||
ctx, | ||
type: "query", | ||
}); | ||
return res; | ||
}, | ||
}); | ||
} | ||
|
||
if (utilName === "prefetchInfinite") { | ||
return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () => | ||
caller.query(pathStr, input) | ||
); | ||
} | ||
|
||
return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () => | ||
caller.query(pathStr, input) | ||
); | ||
}) as CreateTRPCNextLayout<TRouter>; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { appRouter } from "@calcom/trpc/server/routers/_app"; | ||
|
||
export const serverClient = appRouter.createCaller({}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; | ||
import { headers } from "next/headers"; | ||
import superjson from "superjson"; | ||
|
||
import { CALCOM_VERSION } from "@calcom/lib/constants"; | ||
import prisma from "@calcom/prisma"; | ||
import { appRouter } from "@calcom/trpc/server/routers/_app"; | ||
|
||
import { createTRPCNextLayout } from "./createTRPCNextLayout"; | ||
|
||
export async function ssgInit() { | ||
const locale = headers().get("x-locale") ?? "en"; | ||
|
||
const i18n = (await serverSideTranslations(locale, ["common"])) || "en"; | ||
|
||
const ssg = createTRPCNextLayout({ | ||
router: appRouter, | ||
transformer: superjson, | ||
createContext() { | ||
return { prisma, session: null, locale, i18n }; | ||
}, | ||
}); | ||
|
||
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch | ||
// we can set query data directly to the queryClient | ||
const queryKey = [ | ||
["viewer", "public", "i18n"], | ||
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" }, | ||
]; | ||
|
||
ssg.queryClient.setQueryData(queryKey, { i18n }); | ||
|
||
return ssg; | ||
} |
Oops, something went wrong.