Add TanStack Start SDK integration

This commit is contained in:
mantrakp04 2026-04-29 21:36:18 -07:00
parent e831972c4c
commit 1db0f30675
12 changed files with 1120 additions and 213 deletions

View File

@ -361,3 +361,6 @@ A: Invalid `tools` entries are rejected by `requestBodySchema` in `apps/backend/
## Q: Why did the internal metrics E2E snapshots need to change in April 2026?
A: The `/api/v1/internal/metrics` response now intentionally includes `analytics_overview.daily_anonymous_visitors_fallback`, `analytics_overview.anonymous_visitors_fallback`, and `active_users_by_country`. Those additions are reflected in `packages/stack-shared/src/interface/admin-metrics.ts` and the backend route, so the E2E snapshots must include them instead of treating them as regressions.
## Q: How should a TanStack Start SDK package be added without dragging Dashboard V2 logic into the same PR?
A: Keep the integration PR scoped to generated package registration (`packages/tanstack-start/package.json`, `.gitignore`, `scripts/generate-sdks.ts`, `scripts/utils.ts`), template/package dependency metadata, and SDK runtime changes needed by TanStack Start (`cookie.ts`, token-store handling, handler SSR guard). Leave dashboard routes, hooks, app wiring, and admin API types in the dashboard PR.

2
.gitignore vendored
View File

@ -140,10 +140,12 @@ packages/js/*
packages/react/*
packages/next/*
packages/stack/*
packages/tanstack-start/*
!packages/js/package.json
!packages/react/package.json
!packages/next/package.json
!packages/stack/package.json
!packages/tanstack-start/package.json
# claude code
.claude/scheduled_tasks.lock

View File

@ -0,0 +1,115 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/tanstack-start",
"version": "2.8.86",
"repository": "https://github.com/stack-auth/stack-auth",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/index.js"
},
"./convex.config": {
"types": "./dist/integrations/convex/component/convex.config.d.ts",
"import": "./dist/esm/integrations/convex/component/convex.config.js",
"require": "./dist/integrations/convex/component/convex.config.js"
},
"./convex-auth.config": {
"types": "./dist/integrations/convex.d.ts",
"import": "./dist/esm/integrations/convex.js",
"require": "./dist/integrations/convex.js"
}
},
"homepage": "https://stack-auth.com",
"scripts": {
"typecheck": "tsc --noEmit",
"clean": "rimraf dist && rimraf node_modules",
"lint": "eslint --ext .tsx,.ts .",
"build": "rimraf dist && pnpm run css && tsdown",
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
"codegen": "pnpm run css",
"codegen:watch": "pnpm run css:watch",
"css": "pnpm run css-tw && pnpm run css-sc",
"css:watch": "concurrently -n \"tw,sc\" -k \"pnpm run css-tw:watch\" \"pnpm run css-sc:watch\"",
"css-tw:watch": "tailwindcss -i ./src/global.css -o ./src/generated/tailwind.css --watch",
"css-tw": "tailwindcss -i ./src/global.css -o ./src/generated/tailwind.css",
"css-sc": "tsx ./scripts/process-css.ts ./src/generated/tailwind.css ./src/generated/global-css.ts",
"css-sc:watch": "chokidar --silent './src/generated/tailwind.css' -c 'pnpm run css-sc' --throttle 2000"
},
"files": [
"README.md",
"dist",
"CHANGELOG.md",
"LICENSE"
],
"dependencies": {
"@ai-sdk/react": "^3.0.72",
"ai": "^6.0.0",
"@hookform/resolvers": "^5.2.2",
"@stripe/react-stripe-js": "^3.8.1",
"@stripe/stripe-js": "^7.7.0",
"@simplewebauthn/browser": "^13.2.2",
"@stackframe/stack-shared": "workspace:*",
"@stackframe/stack-ui": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"browser-image-compression": "^2.0.2",
"color": "^5.0.3",
"cookie": "^1.1.1",
"jose": "^6.1.3",
"js-cookie": "^3.0.5",
"lucide-react": "^0.378.0",
"oauth4webapi": "^3.8.3",
"@oslojs/otp": "^1.1.0",
"qrcode": "^1.5.4",
"react-easy-crop": "^5.5.6",
"react-hook-form": "^7.70.0",
"tailwindcss-animate": "^1.0.7",
"rrweb": "^1.1.3",
"tsx": "^4.21.0",
"yup": "^1.7.1"
},
"peerDependencies": {
"@types/react": ">=18.3.0",
"@tanstack/react-router": ">=1.100.0",
"@tanstack/react-start": ">=1.100.0",
"react": ">=18.3.0"
},
"peerDependenciesMeta": {
"@tanstack/react-router": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@types/react": {
"optional": true
}
},
"devDependencies": {
"@quetzallabs/i18n": "^0.1.19",
"@types/color": "^3.0.6",
"@types/cookie": "^0.6.0",
"@types/js-cookie": "^3.0.6",
"@types/qrcode": "^1.5.5",
"@types/react-avatar-editor": "^13.0.3",
"autoprefixer": "^10.4.17",
"chokidar-cli": "^3.0.0",
"esbuild": "^0.20.2",
"i18next": "^23.14.0",
"i18next-parser": "^9.0.2",
"@tanstack/react-router": "^1.167.4",
"@tanstack/react-start": "^1.166.15",
"postcss": "^8.4.38",
"postcss-nested": "^6.0.1",
"react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react-dom": "^19.0.0",
"rimraf": "^6.1.2",
"tailwindcss": "^3.4.4",
"tsdown": "^0.20.3",
"convex": "^1.27.0"
}
}

View File

@ -5,6 +5,8 @@
"name": "@stackframe/js",
"//": "ELSE_IF_PLATFORM next",
"name": "@stackframe/stack",
"//": "ELSE_IF_PLATFORM tanstack-start",
"name": "@stackframe/tanstack-start",
"//": "ELSE_IF_PLATFORM react",
"name": "@stackframe/react",
"//": "END_PLATFORM",
@ -131,6 +133,10 @@
"react-dom": ">=18.3.0",
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
"//": "END_PLATFORM",
"//": "IF_PLATFORM tanstack-start",
"@tanstack/react-router": ">=1.100.0",
"@tanstack/react-start": ">=1.100.0",
"//": "END_PLATFORM",
"react": ">=18.3.0"
},
"//": "END_PLATFORM",
@ -141,6 +147,14 @@
"optional": true
},
"//": "END_PLATFORM",
"//": "IF_PLATFORM tanstack-start",
"@tanstack/react-router": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"//": "END_PLATFORM",
"@types/react": {
"optional": true
}
@ -160,6 +174,10 @@
"i18next-parser": "^9.0.2",
"//": "NEXT_LINE_PLATFORM next",
"next": "^14.2.35",
"//": "NEXT_LINE_PLATFORM template tanstack-start",
"@tanstack/react-router": "^1.167.4",
"//": "NEXT_LINE_PLATFORM template tanstack-start",
"@tanstack/react-start": "^1.166.15",
"postcss": "^8.4.38",
"postcss-nested": "^6.0.1",
"react": "^19.0.0",

View File

@ -94,12 +94,20 @@
"@types/react-dom": ">=18.3.0",
"react-dom": ">=18.3.0",
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
"@tanstack/react-router": ">=1.100.0",
"@tanstack/react-start": ">=1.100.0",
"react": ">=18.3.0"
},
"peerDependenciesMeta": {
"@types/react-dom": {
"optional": true
},
"@tanstack/react-router": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@types/react": {
"optional": true
}
@ -117,6 +125,8 @@
"i18next": "^23.14.0",
"i18next-parser": "^9.0.2",
"next": "^14.2.35",
"@tanstack/react-router": "^1.167.4",
"@tanstack/react-start": "^1.166.15",
"postcss": "^8.4.38",
"postcss-nested": "^6.0.1",
"react": "^19.0.0",
@ -127,4 +137,4 @@
"tsdown": "^0.20.3",
"convex": "^1.27.0"
}
}
}

View File

@ -237,7 +237,7 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial<RouteProps>
const navigateRef = useRef(navigate);
navigateRef.current = navigate;
const currentLocation = props.location ?? window.location.pathname;
const searchParamsSource = new URLSearchParams(window.location.search);
const searchParamsSource = new URLSearchParams(typeof window === "undefined" ? "" : window.location.search);
const redirectTargets: (string | undefined)[] = [];
END_PLATFORM */

View File

@ -1,4 +1,5 @@
import { cookies as rscCookies, headers as rscHeaders } from '@stackframe/stack-sc/force-react-server'; // THIS_LINE_PLATFORM next
import { getCookie as tssGetCookie, getCookies as tssGetCookies, setCookie as tssSetCookie, deleteCookie as tssDeleteCookie, getRequestHeader as tssGetRequestHeader } from '@tanstack/react-start/server'; // THIS_LINE_PLATFORM tanstack-start
import { isBrowserLike } from '@stackframe/stack-shared/dist/utils/env';
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
import Cookies from "js-cookie";
@ -104,12 +105,73 @@ export async function createCookieHelper(): Promise<CookieHelper> {
await rscCookies(),
await rscHeaders(),
);
// ELSE_IF_PLATFORM tanstack-start
return createTanStackStartCookieHelper();
// ELSE_PLATFORM
return await createPlaceholderCookieHelper();
// END_PLATFORM
}
}
export function createCookieHelperSync(): CookieHelper {
if (isBrowserLike()) {
return createBrowserCookieHelper();
}
// IF_PLATFORM tanstack-start
return createTanStackStartCookieHelper();
// ELSE_PLATFORM
function throwError(): never {
throw new StackAssertionError("Synchronous server cookie helpers are not available on this platform");
}
return {
get: throwError,
getAll: throwError,
set: throwError,
setOrDelete: throwError,
delete: throwError,
};
// END_PLATFORM
}
// IF_PLATFORM tanstack-start
function determineSecureFromTanStackStartContext(): boolean {
return tssGetRequestHeader("x-forwarded-proto") === "https"
|| (tssGetCookie("stack-is-https") !== undefined);
}
function requiresSecureAttribute(name: string): boolean {
return name.startsWith("__Host-");
}
function createTanStackStartCookieHelper(): CookieHelper {
const helper: CookieHelper = {
get: (name: string) => tssGetCookie(name) ?? null,
getAll: () => tssGetCookies(),
set: (name: string, value: string, options: SetCookieOptions) => {
tssSetCookie(name, value, {
secure: options.secure ?? (requiresSecureAttribute(name) || determineSecureFromTanStackStartContext()),
maxAge: options.maxAge === "session" ? undefined : options.maxAge,
domain: options.domain,
sameSite: "lax",
path: "/",
});
},
setOrDelete: (name, value, options) => {
if (value === null) helper.delete(name, options);
else helper.set(name, value, options);
},
delete: (name: string, options: DeleteCookieOptions) => {
tssDeleteCookie(name, {
secure: requiresSecureAttribute(name),
domain: options.domain,
path: "/",
});
},
};
return helper;
}
// END_PLATFORM
export function createBrowserCookieHelper(): CookieHelper {
return {
get: getCookieClient,
@ -232,6 +294,8 @@ export async function isSecure(): Promise<boolean> {
}
// IF_PLATFORM next
return determineSecureFromServerContext(await rscCookies(), await rscHeaders());
// ELSE_IF_PLATFORM tanstack-start
return determineSecureFromTanStackStartContext();
// END_PLATFORM
return false;
}

View File

@ -42,7 +42,7 @@ import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react
import type * as yup from "yup";
import { constructRedirectUrl } from "../../../../utils/url";
import { getNewOAuthProviderOrScopeUrl, callOAuthCallback } from "../../../auth";
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createCookieHelperSync, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
import { envVars } from "../../../env";
import { ApiKey, ApiKeyCreationOptions, ApiKeyUpdateOptions, apiKeyCreationOptionsToCrud } from "../../api-keys";
import { ConvexCtx, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, RequestLike, ResolvedHandlerUrls, TokenStoreInit, stackAppInternalsSymbol } from "../../common";
@ -930,6 +930,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
switch (tokenStoreInit) {
case "cookie": {
// IF_PLATFORM tanstack-start
if (!isBrowserLike()) {
return this._getOrCreateTokenStore(cookieHelper, "nextjs-cookie");
}
// END_PLATFORM
return this._getBrowserCookieTokenStore();
}
case "nextjs-cookie": {
@ -1024,6 +1029,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
// IF_PLATFORM react-like
protected _useTokenStore(overrideTokenStoreInit?: TokenStoreInit): Store<TokenObject> {
// IF_PLATFORM tanstack-start
if (!isBrowserLike()) {
return this._getOrCreateTokenStore(createCookieHelperSync(), overrideTokenStoreInit);
}
// END_PLATFORM
suspendIfSsr();
const cookieHelper = createBrowserCookieHelper();
const tokenStore = this._getOrCreateTokenStore(cookieHelper, overrideTokenStoreInit);

File diff suppressed because it is too large Load Diff

View File

@ -191,6 +191,14 @@ withGeneratorLock(async () => {
return baseEditFn({ relativePath, content, platforms: PLATFORMS["react"] });
},
});
generateFromTemplate({
src: srcDir,
dest: path.resolve(baseDir, "tanstack-start"),
editFn: (relativePath, content) => {
return baseEditFn({ relativePath, content, platforms: PLATFORMS["tanstack-start"] });
},
});
}).catch((error) => {
console.error(error);
process.exit(1);

View File

@ -9,7 +9,8 @@ export const PLATFORMS = {
"next": ['next', 'react-like', 'js-like'],
"js": ['js', 'js-like'],
"react": ['react', 'react-like', 'js-like'],
"template": ['template', 'react-like', 'next', 'js', 'js-like', 'python-like'],
"tanstack-start": ['tanstack-start', 'react', 'react-like', 'js-like'],
"template": ['template', 'react-like', 'next', 'js', 'js-like', 'python-like', 'tanstack-start'],
"python": ['python', 'python-like'],
}

View File

@ -4,6 +4,7 @@
"STACK_*",
"CRON_SECRET",
"NEXT_PUBLIC_*",
"VITE_*",
"NEXT_PUBLIC_SENTRY_*",
"SENTRY_*",
"VERCEL_GIT_COMMIT_SHA",