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

feat: expose TRPC transformer from api package #1189

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
23 changes: 23 additions & 0 deletions apps/expo/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,27 @@ export default [
},
...baseConfig,
...reactConfig,
{
rules: {
"@typescript-eslint/no-restricted-imports": [
"error",
{
paths: [
{
name: "@acme/api",
message: "Only type imports from '@acme/api' are allowed",
allowTypeImports: true,
},
],
patterns: [
{
group: ["@acme/api/*", "!@acme/api/transformer"],
message: "Only certain modules from '@acme/api' can be imported",
allowTypeImports: true,
},
],
},
],
},
},
];
7 changes: 3 additions & 4 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@acme/api": "workspace:*",
Copy link
Member

Choose a reason for hiding this comment

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

Have Expo shipped tree shaking yet? If not I think including this as a prod dep will leak backend code

Copy link
Member

@juliusmarminge juliusmarminge Oct 3, 2024

Choose a reason for hiding this comment

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

Copy link
Author

@ochicf ochicf Oct 7, 2024

Choose a reason for hiding this comment

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

Good point, didn't though of that. I'm not sure how to check if it's happening (I'm quite new with Expo and RN). Also, not sure if updating SDK is easy or not and should be done in this PR or in a different one.

Another option would be to move the transformer logic into a separate package so we are 100% sure it doesn't bring any BE code. For example @acme/transformer or @acme/api-transformer (though IMHO it feels much better to have it as a module in @acme/api).

Copy link
Contributor

Choose a reason for hiding this comment

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

I am personally scared of adding an api package as a dependency, regardles of wether or not Expo does tree-shaking correctly or not. Might not be an issue, but it's something to be mindful of.

I personally have a @acme/shared package that exports common utils/stuff that is safe to be shared everywhere. Adding another package just for the tranformer export might be a bit verbose for this repo though

Copy link
Author

Choose a reason for hiding this comment

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

Cool, yeah it makes sense. I'll prepare that and revert the api package to just dev dependency in Expo.

"@bacons/text-decoder": "^0.0.0",
"@expo/metro-config": "^0.18.11",
"@shopify/flash-list": "1.6.4",
"@shopify/flash-list": "1.7.1",
"@tanstack/react-query": "catalog:",
"@trpc/client": "catalog:",
"@trpc/react-query": "catalog:",
Expand All @@ -39,11 +40,9 @@
"react-native-gesture-handler": "~2.16.2",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "~4.10.8",
"react-native-screens": "~3.31.1",
"superjson": "2.2.1"
"react-native-screens": "~3.31.1"
},
"devDependencies": {
"@acme/api": "workspace:*",
"@acme/eslint-config": "workspace:*",
"@acme/prettier-config": "workspace:*",
"@acme/tailwind-config": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions apps/expo/src/utils/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import superjson from "superjson";

import type { AppRouter } from "@acme/api";
import { transformer } from "@acme/api/transformer";

import { getBaseUrl } from "./base-url";
import { getToken } from "./session-store";
Expand All @@ -31,7 +31,7 @@ export function TRPCProvider(props: { children: React.ReactNode }) {
colorMode: "ansi",
}),
httpBatchLink({
transformer: superjson,
transformer,
url: `${getBaseUrl()}/api/trpc`,
headers() {
const headers = new Map<string, string>();
Expand Down
3 changes: 2 additions & 1 deletion apps/expo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
"~/*": ["./src/*"],
"@acme/api/transformer": ["../../packages/api/src/transformer.ts"]
},
"jsx": "react-native",
"types": ["nativewind/types"],
Expand Down
1 change: 0 additions & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"next": "^14.2.5",
"react": "catalog:react18",
"react-dom": "catalog:react18",
"superjson": "2.2.1",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
7 changes: 4 additions & 3 deletions apps/nextjs/src/trpc/query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
defaultShouldDehydrateQuery,
QueryClient,
} from "@tanstack/react-query";
import SuperJSON from "superjson";

import { transformer } from "@acme/api/transformer";

export const createQueryClient = () =>
new QueryClient({
Expand All @@ -13,13 +14,13 @@ export const createQueryClient = () =>
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: SuperJSON.serialize,
serializeData: transformer.output.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
hydrate: {
deserializeData: SuperJSON.deserialize,
deserializeData: transformer.output.deserialize,
},
},
});
4 changes: 2 additions & 2 deletions apps/nextjs/src/trpc/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { useState } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson";

import type { AppRouter } from "@acme/api";
import { transformer } from "@acme/api/transformer";

import { env } from "~/env";
import { createQueryClient } from "./query-client";
Expand Down Expand Up @@ -37,7 +37,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
(op.direction === "down" && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
transformer: SuperJSON,
transformer,
url: getBaseUrl() + "/api/trpc",
headers() {
const headers = new Headers();
Expand Down
4 changes: 4 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
},
"./transformer": {
"types": "./dist/transformer.d.ts",
"default": "./src/transformer.ts"
}
},
"license": "MIT",
Expand Down
22 changes: 22 additions & 0 deletions packages/api/src/transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { TRPCCombinedDataTransformer } from "@trpc/server";
import SuperJSON from "superjson";

/**
* tRPC transformer, used to serialize/deserialize data between client and server.
* This export is used internally in `@acme/api` and in both `apps/nextjs` and `apps/expo`.
*
* ! IMPORTANT: changing this is a BREAKING CHANGE !
* ? Even though this helps swapping transformers, bear in mind that distributed Expo apps
* ? will have the previous transformer bundled in their device, which will cause a mismatch
* ? in how the data is sent and received (basically, all API requests will fail).
*/
Copy link
Member

Choose a reason for hiding this comment

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

good callout. add a @see comment to trpc docs here too as a reference. not sure many people have used the "full input/output syntax" before

export const transformer: TRPCCombinedDataTransformer = {
input: {
serialize: SuperJSON.serialize,
deserialize: SuperJSON.deserialize,
},
output: {
serialize: SuperJSON.serialize,
deserialize: SuperJSON.deserialize,
},
};
5 changes: 3 additions & 2 deletions packages/api/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
* The pieces you will need to use are documented accordingly near the end
*/
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";

import type { Session } from "@acme/auth";
import { auth, validateToken } from "@acme/auth";
import { db } from "@acme/db/client";

import { transformer } from "./transformer";

/**
* Isomorphic Session getter for API requests
* - Expo requests will have a session token in the Authorization header
Expand Down Expand Up @@ -61,7 +62,7 @@ export const createTRPCContext = async (opts: {
* transformer
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
transformer,
errorFormatter: ({ shape, error }) => ({
...shape,
data: {
Expand Down
Loading