Skip to content

Commit 426b8de

Browse files
feat(cli): add fullstack tanstack start (#613)
1 parent 4f3a44a commit 426b8de

File tree

27 files changed

+539
-115
lines changed

27 files changed

+539
-115
lines changed

apps/cli/src/helpers/core/api-setup.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ function getApiDependencies(
6868

6969
if (frontendType.hasReactWeb) {
7070
if (api === "orpc") {
71-
deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
71+
deps.web = {
72+
dependencies: ["@orpc/tanstack-query", "@orpc/client", "@orpc/server"],
73+
};
7274
} else if (api === "trpc") {
7375
deps.web = {
7476
dependencies: [
@@ -84,6 +86,7 @@ function getApiDependencies(
8486
"@tanstack/vue-query",
8587
"@orpc/tanstack-query",
8688
"@orpc/client",
89+
"@orpc/server",
8790
],
8891
devDependencies: ["@tanstack/vue-query-devtools"],
8992
};
@@ -92,6 +95,7 @@ function getApiDependencies(
9295
dependencies: [
9396
"@orpc/tanstack-query",
9497
"@orpc/client",
98+
"@orpc/server",
9599
"@tanstack/svelte-query",
96100
],
97101
devDependencies: ["@tanstack/svelte-query-devtools"],
@@ -101,6 +105,7 @@ function getApiDependencies(
101105
dependencies: [
102106
"@orpc/tanstack-query",
103107
"@orpc/client",
108+
"@orpc/server",
104109
"@tanstack/solid-query",
105110
],
106111
devDependencies: [

apps/cli/src/helpers/core/create-project.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { log } from "@clack/prompts";
22
import fs from "fs-extra";
33
import type { ProjectConfig } from "../../types";
4+
import { setupBetterAuthPlugins } from "../../utils/better-auth-plugin-setup";
45
import { writeBtsConfig } from "../../utils/bts-config";
56
import { exitWithError } from "../../utils/errors";
67
import { setupCatalogs } from "../../utils/setup-catalogs";
@@ -86,6 +87,8 @@ export async function createProject(
8687
await setupAuth(options);
8788
}
8889

90+
await setupBetterAuthPlugins(projectDir, options);
91+
8992
if (options.payments && options.payments !== "none") {
9093
await setupPayments(options);
9194
}

apps/cli/src/helpers/core/env-setup.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function getClientServerVar(
1010
const hasNextJs = frontend.includes("next");
1111
const hasNuxt = frontend.includes("nuxt");
1212
const hasSvelte = frontend.includes("svelte");
13+
const hasTanstackStart = frontend.includes("tanstack-start");
1314

1415
// For fullstack self, no base URL is needed (same-origin)
1516
if (backend === "self") {
@@ -20,6 +21,7 @@ function getClientServerVar(
2021
if (hasNextJs) key = "NEXT_PUBLIC_SERVER_URL";
2122
else if (hasNuxt) key = "NUXT_PUBLIC_SERVER_URL";
2223
else if (hasSvelte) key = "PUBLIC_SERVER_URL";
24+
else if (hasTanstackStart) key = "VITE_SERVER_URL";
2325

2426
return { key, value: "http://localhost:3000", write: true } as const;
2527
}
@@ -28,9 +30,11 @@ function getConvexVar(frontend: string[]) {
2830
const hasNextJs = frontend.includes("next");
2931
const hasNuxt = frontend.includes("nuxt");
3032
const hasSvelte = frontend.includes("svelte");
33+
const hasTanstackStart = frontend.includes("tanstack-start");
3134
if (hasNextJs) return "NEXT_PUBLIC_CONVEX_URL";
3235
if (hasNuxt) return "NUXT_PUBLIC_CONVEX_URL";
3336
if (hasSvelte) return "PUBLIC_CONVEX_URL";
37+
if (hasTanstackStart) return "VITE_CONVEX_URL";
3438
return "VITE_CONVEX_URL";
3539
}
3640

@@ -227,8 +231,12 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
227231
const nativeDir = path.join(projectDir, "apps/native");
228232
if (await fs.pathExists(nativeDir)) {
229233
let envVarName = "EXPO_PUBLIC_SERVER_URL";
230-
let serverUrl =
231-
backend === "self" ? "http://localhost:3001" : "http://localhost:3000";
234+
let serverUrl = "http://localhost:3000";
235+
236+
if (backend === "self") {
237+
// Both TanStack Start and Next.js use port 3001 for fullstack
238+
serverUrl = "http://localhost:3001";
239+
}
232240

233241
if (backend === "convex") {
234242
envVarName = "EXPO_PUBLIC_CONVEX_URL";

apps/cli/src/helpers/core/post-installation.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function displayPostInstallInstructions(
6363
const nativeInstructions =
6464
frontend?.includes("native-nativewind") ||
6565
frontend?.includes("native-unistyles")
66-
? getNativeInstructions(isConvex, isBackendSelf)
66+
? getNativeInstructions(isConvex, isBackendSelf, frontend || [])
6767
: "";
6868
const pwaInstructions =
6969
addons?.includes("pwa") && frontend?.includes("react-router")
@@ -171,12 +171,12 @@ export async function displayPostInstallInstructions(
171171
output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
172172

173173
if (api === "orpc") {
174-
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api\n`;
174+
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
175175
}
176176
}
177177

178178
if (isBackendSelf && api === "orpc") {
179-
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}/rpc/api\n`;
179+
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}/api/rpc/api-reference\n`;
180180
}
181181

182182
if (addons?.includes("starlight")) {
@@ -214,7 +214,11 @@ export async function displayPostInstallInstructions(
214214
consola.box(output);
215215
}
216216

217-
function getNativeInstructions(isConvex: boolean, isBackendSelf: boolean) {
217+
function getNativeInstructions(
218+
isConvex: boolean,
219+
isBackendSelf: boolean,
220+
_frontend: string[],
221+
) {
218222
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
219223
const exampleUrl = isConvex
220224
? "https://<YOUR_CONVEX_URL>"

apps/cli/src/helpers/core/template-manager.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@ export async function setupFrontendTemplates(
125125

126126
if (
127127
context.backend === "self" &&
128-
reactFramework === "next" &&
128+
(reactFramework === "next" || reactFramework === "tanstack-start") &&
129129
context.api !== "none"
130130
) {
131131
const apiFullstackDir = path.join(
132132
PKG_ROOT,
133-
`templates/api/${context.api}/fullstack/next`,
133+
`templates/api/${context.api}/fullstack/${reactFramework}`,
134134
);
135135
if (await fs.pathExists(apiFullstackDir)) {
136136
await processAndCopyFiles(
@@ -597,10 +597,13 @@ export async function setupAuthTemplate(
597597
);
598598
}
599599

600-
if (context.backend === "self" && reactFramework === "next") {
600+
if (
601+
context.backend === "self" &&
602+
(reactFramework === "next" || reactFramework === "tanstack-start")
603+
) {
601604
const authFullstackSrc = path.join(
602605
PKG_ROOT,
603-
`templates/auth/${authProvider}/fullstack/next`,
606+
`templates/auth/${authProvider}/fullstack/${reactFramework}`,
604607
);
605608
if (await fs.pathExists(authFullstackSrc)) {
606609
await processAndCopyFiles(
@@ -938,10 +941,13 @@ export async function setupExamplesTemplate(
938941
} else {
939942
}
940943

941-
if (context.backend === "self" && reactFramework === "next") {
944+
if (
945+
context.backend === "self" &&
946+
(reactFramework === "next" || reactFramework === "tanstack-start")
947+
) {
942948
const exampleFullstackSrc = path.join(
943949
exampleBaseDir,
944-
"fullstack/next",
950+
`fullstack/${reactFramework}`,
945951
);
946952
if (await fs.pathExists(exampleFullstackSrc)) {
947953
await processAndCopyFiles(

apps/cli/src/helpers/core/workspace-setup.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,39 +26,47 @@ export async function setupWorkspaceDependencies(
2626

2727
const authPackageDir = path.join(projectDir, "packages/auth");
2828
if (await fs.pathExists(authPackageDir)) {
29+
const authDeps: Record<string, string> = {};
30+
if (options.database !== "none" && (await fs.pathExists(dbPackageDir))) {
31+
authDeps[`@${projectName}/db`] = workspaceVersion;
32+
}
33+
2934
await addPackageDependency({
3035
dependencies: commonDeps,
3136
devDependencies: commonDevDeps,
32-
customDependencies: {
33-
[`@${projectName}/db`]: workspaceVersion,
34-
},
37+
customDependencies: authDeps,
3538
projectDir: authPackageDir,
3639
});
3740
}
3841

3942
const apiPackageDir = path.join(projectDir, "packages/api");
4043
if (await fs.pathExists(apiPackageDir)) {
44+
const apiDeps: Record<string, string> = {};
45+
if (options.auth !== "none" && (await fs.pathExists(authPackageDir))) {
46+
apiDeps[`@${projectName}/auth`] = workspaceVersion;
47+
}
48+
if (options.database !== "none" && (await fs.pathExists(dbPackageDir))) {
49+
apiDeps[`@${projectName}/db`] = workspaceVersion;
50+
}
51+
4152
await addPackageDependency({
4253
dependencies: commonDeps,
4354
devDependencies: commonDevDeps,
44-
customDependencies: {
45-
[`@${projectName}/auth`]: workspaceVersion,
46-
[`@${projectName}/db`]: workspaceVersion,
47-
},
55+
customDependencies: apiDeps,
4856
projectDir: apiPackageDir,
4957
});
5058
}
5159

5260
const serverPackageDir = path.join(projectDir, "apps/server");
5361
if (await fs.pathExists(serverPackageDir)) {
5462
const serverDeps: Record<string, string> = {};
55-
if (await fs.pathExists(apiPackageDir)) {
63+
if (options.api !== "none" && (await fs.pathExists(apiPackageDir))) {
5664
serverDeps[`@${projectName}/api`] = workspaceVersion;
5765
}
58-
if (await fs.pathExists(authPackageDir)) {
66+
if (options.auth !== "none" && (await fs.pathExists(authPackageDir))) {
5967
serverDeps[`@${projectName}/auth`] = workspaceVersion;
6068
}
61-
if (await fs.pathExists(dbPackageDir)) {
69+
if (options.database !== "none" && (await fs.pathExists(dbPackageDir))) {
6270
serverDeps[`@${projectName}/db`] = workspaceVersion;
6371
}
6472

@@ -74,10 +82,10 @@ export async function setupWorkspaceDependencies(
7482

7583
if (await fs.pathExists(webPackageDir)) {
7684
const webDeps: Record<string, string> = {};
77-
if (await fs.pathExists(apiPackageDir)) {
85+
if (options.api !== "none" && (await fs.pathExists(apiPackageDir))) {
7886
webDeps[`@${projectName}/api`] = workspaceVersion;
7987
}
80-
if (await fs.pathExists(authPackageDir)) {
88+
if (options.auth !== "none" && (await fs.pathExists(authPackageDir))) {
8189
webDeps[`@${projectName}/auth`] = workspaceVersion;
8290
}
8391

@@ -93,7 +101,7 @@ export async function setupWorkspaceDependencies(
93101

94102
if (await fs.pathExists(nativePackageDir)) {
95103
const nativeDeps: Record<string, string> = {};
96-
if (await fs.pathExists(apiPackageDir)) {
104+
if (options.api !== "none" && (await fs.pathExists(apiPackageDir))) {
97105
nativeDeps[`@${projectName}/api`] = workspaceVersion;
98106
}
99107

apps/cli/src/prompts/backend.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { DEFAULT_CONFIG } from "../constants";
33
import type { Backend, Frontend } from "../types";
44
import { exitCancelled } from "../utils/errors";
55

6-
// Temporarily restrict to Next.js only for backend="self"
6+
// Temporarily restrict to Next.js and TanStack Start only for backend="self"
77
const FULLSTACK_FRONTENDS: readonly Frontend[] = [
88
"next",
9+
"tanstack-start",
910
// "nuxt", // TODO: Add support in future update
1011
// "svelte", // TODO: Add support in future update
11-
// "tanstack-start", // TODO: Add support in future update
1212
] as const;
1313

1414
export async function getBackendFrameworkChoice(
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { SyntaxKind } from "ts-morph";
2+
import type { ProjectConfig } from "../types";
3+
import { ensureArrayProperty, tsProject } from "./ts-morph";
4+
5+
export async function setupBetterAuthPlugins(
6+
projectDir: string,
7+
config: ProjectConfig,
8+
) {
9+
const authIndexPath = `${projectDir}/packages/auth/src/index.ts`;
10+
const authIndexFile = tsProject.addSourceFileAtPath(authIndexPath);
11+
12+
if (!authIndexFile) {
13+
console.warn("Better Auth index file not found, skipping plugin setup");
14+
return;
15+
}
16+
17+
const pluginsToAdd: string[] = [];
18+
const importsToAdd: string[] = [];
19+
20+
if (
21+
config.backend === "self" &&
22+
config.frontend?.includes("tanstack-start")
23+
) {
24+
pluginsToAdd.push("reactStartCookies()");
25+
importsToAdd.push(
26+
'import { reactStartCookies } from "better-auth/react-start";',
27+
);
28+
}
29+
30+
if (
31+
config.frontend?.includes("native-nativewind") ||
32+
config.frontend?.includes("native-unistyles")
33+
) {
34+
pluginsToAdd.push("expo()");
35+
importsToAdd.push('import { expo } from "@better-auth/expo";');
36+
}
37+
38+
if (pluginsToAdd.length === 0) {
39+
return;
40+
}
41+
42+
importsToAdd.forEach((importStatement) => {
43+
const existingImport = authIndexFile.getImportDeclaration((declaration) =>
44+
declaration
45+
.getModuleSpecifierValue()
46+
.includes(importStatement.split('"')[1]),
47+
);
48+
49+
if (!existingImport) {
50+
authIndexFile.insertImportDeclaration(0, {
51+
moduleSpecifier: importStatement.split('"')[1],
52+
namedImports: [importStatement.split("{")[1].split("}")[0].trim()],
53+
});
54+
}
55+
});
56+
57+
const betterAuthCall = authIndexFile
58+
.getDescendantsOfKind(SyntaxKind.CallExpression)
59+
.find((call) => call.getExpression().getText() === "betterAuth");
60+
61+
if (betterAuthCall) {
62+
const configObject = betterAuthCall.getArguments()[0];
63+
64+
if (
65+
configObject &&
66+
configObject.getKind() === SyntaxKind.ObjectLiteralExpression
67+
) {
68+
const objLiteral = configObject.asKindOrThrow(
69+
SyntaxKind.ObjectLiteralExpression,
70+
);
71+
72+
const pluginsArray = ensureArrayProperty(objLiteral, "plugins");
73+
74+
pluginsToAdd.forEach((plugin) => {
75+
pluginsArray.addElement(plugin);
76+
});
77+
}
78+
}
79+
80+
authIndexFile.save();
81+
}

apps/cli/src/utils/compatibility-rules.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ export function ensureSingleWebAndNative(frontends: Frontend[]) {
4343
}
4444
}
4545

46-
// Temporarily restrict to Next.js only for backend="self"
46+
// Temporarily restrict to Next.js and TanStack Start only for backend="self"
4747
const FULLSTACK_FRONTENDS: readonly Frontend[] = [
4848
"next",
49+
"tanstack-start",
4950
// "nuxt", // TODO: Add support in future update
5051
// "svelte", // TODO: Add support in future update
51-
// "tanstack-start", // TODO: Add support in future update
5252
] as const;
5353

5454
export function validateSelfBackendCompatibility(
@@ -66,7 +66,7 @@ export function validateSelfBackendCompatibility(
6666

6767
if (!hasSupportedWeb) {
6868
exitWithError(
69-
"Backend 'self' (fullstack) currently only supports Next.js frontend. Please use --frontend next. Support for Nuxt, SvelteKit, and TanStack Start will be added in a future update.",
69+
"Backend 'self' (fullstack) currently only supports Next.js and TanStack Start frontends. Please use --frontend next or --frontend tanstack-start. Support for Nuxt and SvelteKit will be added in a future update.",
7070
);
7171
}
7272

@@ -86,7 +86,7 @@ export function validateSelfBackendCompatibility(
8686
backend === "self"
8787
) {
8888
exitWithError(
89-
"Backend 'self' (fullstack) currently only supports Next.js frontend. Please use --frontend next or choose a different backend. Support for Nuxt, SvelteKit, and TanStack Start will be added in a future update.",
89+
"Backend 'self' (fullstack) currently only supports Next.js and TanStack Start frontends. Please use --frontend next or --frontend tanstack-start or choose a different backend. Support for Nuxt and SvelteKit will be added in a future update.",
9090
);
9191
}
9292
}

apps/cli/templates/api/orpc/fullstack/next/src/app/api/rpc/[[...rest]]/route.ts.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async function handleRequest(req: NextRequest) {
3535
if (rpcResult.response) return rpcResult.response;
3636

3737
const apiResult = await apiHandler.handle(req, {
38-
prefix: "/api/rpc/api",
38+
prefix: "/api/rpc/api-reference",
3939
context: await createContext(req),
4040
});
4141
if (apiResult.response) return apiResult.response;

0 commit comments

Comments
 (0)