mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
[codex] Add TanStack Start SDK integration (#1399)
## Summary - Adds the generated `@stackframe/tanstack-start` workspace package registration. - Adds TanStack Start platform macros/dependencies to the SDK template and generator. - Adds TanStack Start cookie/token-store support plus the handler SSR guard needed by Start. ## Scope This intentionally excludes Dashboard V2 routes, hooks, components, app shell logic, and dashboard API type additions. Those stay in the existing dashboard PR/branch. ## Validation - `pnpm install --lockfile-only --ignore-scripts` - `pnpm install --ignore-scripts` - `pnpm -C packages/template lint src/components-page/stack-handler-client.tsx src/lib/cookie.ts src/lib/stack-app/apps/implementations/client-app-impl.ts` Package typecheck was attempted with `pnpm -C packages/template typecheck`, but the clean worktree lacks generated package declaration outputs for workspace dependencies such as `@stackframe/stack-shared` and `@stackframe/stack-ui`. Per repo instructions, package builds/codegen are not run by agents. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * TanStack Start integration: published SDK package, example demo app, dashboard onboarding flow, framework-aware CTAs/docs, and a TanStack-specific provider for client-only auth routes. * Improved client/server auth: safer runtime guards and consistent cookie/token-store behavior across SSR and client. * **Documentation** * New Integrations guide and expanded getting-started/setup docs with TanStack Start examples and env/key guidance. * **Chores** * Template, build, tooling, and demo config updates to support the new platform. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
acc646cb0b
commit
68ae6d1f1c
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
|
||||
BIN
apps/dashboard/public/tanstack-start-logo.png
Normal file
BIN
apps/dashboard/public/tanstack-start-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { CodeBlock } from '@/components/code-block';
|
||||
import { APIEnvKeys, NextJsEnvKeys } from '@/components/env-keys';
|
||||
import { APIEnvKeys, NextJsEnvKeys, ViteEnvKeys } from '@/components/env-keys';
|
||||
import { InlineCode } from '@/components/inline-code';
|
||||
import { StyledLink } from '@/components/link';
|
||||
import { CopyPromptButton, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
|
||||
@ -27,22 +27,58 @@ const nameClasses = "text-green-600 dark:text-green-500";
|
||||
|
||||
const INSTALL_COMMAND_BY_FRAMEWORK = {
|
||||
nextjs: 'npx @stackframe/stack-cli@latest init',
|
||||
tanstackStart: 'npm install @stackframe/tanstack-start',
|
||||
react: 'npm install @stackframe/react',
|
||||
javascript: 'npm install @stackframe/js',
|
||||
python: 'pip install requests',
|
||||
} as const;
|
||||
|
||||
const buildInstallPrompt = (command: string) => deindent`
|
||||
type SetupFramework = keyof typeof INSTALL_COMMAND_BY_FRAMEWORK;
|
||||
|
||||
const TANSTACK_START_SETUP_PROMPT = deindent`
|
||||
Please set up Stack Auth in my TanStack Start app.
|
||||
|
||||
1. Install the alpha TanStack Start package:
|
||||
|
||||
npm install @stackframe/tanstack-start
|
||||
|
||||
2. Configure the app with these environment variables:
|
||||
|
||||
VITE_STACK_PROJECT_ID=<project-id>
|
||||
STACK_SECRET_SERVER_KEY=<secret-server-key>
|
||||
|
||||
3. Create a StackClientApp using @stackframe/tanstack-start with:
|
||||
- projectId: import.meta.env.VITE_STACK_PROJECT_ID
|
||||
- tokenStore: "cookie"
|
||||
- redirectMethod: "window"
|
||||
|
||||
4. Wrap the TanStack Start root route with StackProvider and StackTheme.
|
||||
|
||||
5. Add a /handler/$ route using StackHandler. The handler route must set ssr: false and pass location={pathname} from useLocation().
|
||||
|
||||
Use only the environment variables listed above.
|
||||
|
||||
After it finishes, verify that the Stack Auth MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.stack-auth.com/\`. If it is not registered, please add it manually so you have live access to Stack Auth docs and APIs.
|
||||
`;
|
||||
|
||||
const buildInstallPrompt = (framework: SetupFramework) => {
|
||||
if (framework === "tanstackStart") {
|
||||
return TANSTACK_START_SETUP_PROMPT;
|
||||
}
|
||||
|
||||
const command = INSTALL_COMMAND_BY_FRAMEWORK[framework];
|
||||
return deindent`
|
||||
Please run the following command in my project's terminal:
|
||||
|
||||
${command}
|
||||
|
||||
After it finishes, verify that the Stack Auth MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.stack-auth.com/mcp\`. The command above should handle this automatically; if for any reason it didn't, please add the MCP server manually so you have live access to Stack Auth docs and APIs.
|
||||
`;
|
||||
};
|
||||
|
||||
export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
const adminApp = useAdminApp();
|
||||
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'react' | 'javascript' | 'python'>('nextjs');
|
||||
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python'>('nextjs');
|
||||
const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null);
|
||||
const projectConfig = adminApp.useProject().useConfig();
|
||||
const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey;
|
||||
@ -220,6 +256,142 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
}
|
||||
];
|
||||
|
||||
const tanstackStartSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Stack Auth",
|
||||
content: <>
|
||||
<Typography>
|
||||
In a new or existing TanStack Start project, install the alpha Stack Auth package:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @stackframe/tanstack-start`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@stackframe/tanstack-start</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Put these keys in your TanStack Start environment file.
|
||||
</Typography>
|
||||
<StackAuthKeys keys={keys} onGenerateKeys={onGenerateKeys} type="vite" />
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create stack/client.ts file",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file called <InlineCode>src/stack/client.ts</InlineCode> and initialize Stack Auth with cookie storage.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { StackClientApp } from "@stackframe/tanstack-start";
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: "window",
|
||||
});
|
||||
`}
|
||||
title="src/stack/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Update the root route",
|
||||
content: <>
|
||||
<Typography>
|
||||
Wrap your TanStack Start root route with <InlineCode>StackProvider</InlineCode> and <InlineCode>StackTheme</InlineCode>.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
maxHeight={300}
|
||||
content={deindent`
|
||||
import { StackProvider, StackTheme } from "@stackframe/tanstack-start";
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import { stackClientApp } from "../stack/client";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<StackProvider app={stackClientApp}>
|
||||
<StackTheme>
|
||||
<Outlet />
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
`}
|
||||
title="src/routes/__root.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
title: "Add the handler route",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a splat route for Stack Auth's built-in auth pages.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { StackHandler } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/handler/$")({
|
||||
ssr: false,
|
||||
component: HandlerPage,
|
||||
});
|
||||
|
||||
function HandlerPage() {
|
||||
const { pathname } = useLocation();
|
||||
return <StackHandler fullPage location={pathname} />;
|
||||
}
|
||||
`}
|
||||
title="src/routes/handler/$.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
<Typography>
|
||||
If you start your TanStack Start app and navigate to <StyledLink href="http://localhost:3000/handler/sign-up">http://localhost:3000/handler/sign-up</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
},
|
||||
];
|
||||
|
||||
const javascriptSteps = [
|
||||
{
|
||||
step: 2,
|
||||
@ -480,7 +652,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
<CopyPromptButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
content={buildInstallPrompt(INSTALL_COMMAND_BY_FRAMEWORK[selectedFramework])}
|
||||
content={buildInstallPrompt(selectedFramework)}
|
||||
>
|
||||
<SparkleIcon className="w-4 h-4 mr-2 text-purple-500 dark:text-purple-400" weight="fill" />
|
||||
Copy prompt
|
||||
@ -500,6 +672,11 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
name: 'Next.js',
|
||||
reverseIfDark: true,
|
||||
imgSrc: '/next-logo.svg',
|
||||
}, {
|
||||
id: 'tanstackStart',
|
||||
name: 'TanStack Start',
|
||||
reverseIfDark: false,
|
||||
imgSrc: '/tanstack-start-logo.png',
|
||||
}, {
|
||||
id: 'react',
|
||||
name: 'React',
|
||||
@ -538,6 +715,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
</div>,
|
||||
},
|
||||
...(selectedFramework === 'nextjs' ? nextJsSteps : []),
|
||||
...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []),
|
||||
...(selectedFramework === 'react' ? reactSteps : []),
|
||||
...(selectedFramework === 'javascript' ? javascriptSteps : []),
|
||||
...(selectedFramework === 'python' ? pythonSteps : []),
|
||||
@ -638,7 +816,7 @@ function GlobeIllustrationInner() {
|
||||
function StackAuthKeys(props: {
|
||||
keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null,
|
||||
onGenerateKeys: () => Promise<void>,
|
||||
type: 'next' | 'raw',
|
||||
type: 'next' | 'vite' | 'raw',
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full border rounded-xl p-8 gap-4 flex flex-col">
|
||||
@ -650,6 +828,11 @@ function StackAuthKeys(props: {
|
||||
publishableClientKey={props.keys.publishableClientKey}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
) : props.type === 'vite' ? (
|
||||
<ViteEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
) : (
|
||||
<APIEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
|
||||
@ -4,7 +4,7 @@ import { useAdminApp } from "@/app/(main)/(protected)/projects/[projectId]/use-a
|
||||
import { AppStoreEntry } from "@/components/app-store-entry";
|
||||
import { useRouter } from "@/components/router";
|
||||
import { useUpdateConfig } from "@/lib/config-update";
|
||||
import { ALL_APPS_FRONTEND, getAppPath, isSubApp, type AppId } from "@/lib/apps-frontend";
|
||||
import { ALL_APPS_FRONTEND, getAppPath, getDocumentationHref, isSubApp, type AppId } from "@/lib/apps-frontend";
|
||||
import { isAppEnabled } from "@/lib/apps-utils";
|
||||
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
@ -28,6 +28,8 @@ export default function AppDetailsPageClient({ appId }: { appId: AppId }) {
|
||||
const parentAppFrontend = parentAppId == null ? null : ALL_APPS_FRONTEND[parentAppId];
|
||||
const parentAppEnabled = parentAppId == null ? false : isAppEnabled(config.apps.installed, parentAppId);
|
||||
const appPath = getAppPath(project.id, appFrontend);
|
||||
const documentationHref = getDocumentationHref(appFrontend);
|
||||
const appDestination = documentationHref ?? appPath;
|
||||
const subAppDestinationPath = parentAppFrontend == null
|
||||
? null
|
||||
: parentAppEnabled
|
||||
@ -40,11 +42,19 @@ export default function AppDetailsPageClient({ appId }: { appId: AppId }) {
|
||||
configUpdate: { [`apps.installed.${appId}.enabled`]: true },
|
||||
pushable: true,
|
||||
});
|
||||
router.push(appPath);
|
||||
if (documentationHref != null) {
|
||||
window.location.href = documentationHref;
|
||||
} else {
|
||||
router.push(appPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
router.push(subAppDestinationPath ?? appPath);
|
||||
if (documentationHref != null) {
|
||||
window.location.href = documentationHref;
|
||||
} else {
|
||||
router.push(subAppDestinationPath ?? appDestination);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable = async () => {
|
||||
|
||||
@ -58,9 +58,11 @@ type AppSection = {
|
||||
items: {
|
||||
name: string,
|
||||
href: string,
|
||||
external?: boolean,
|
||||
match: (fullUrl: URL) => boolean,
|
||||
}[],
|
||||
firstItemHref?: string,
|
||||
firstItemExternal?: boolean,
|
||||
};
|
||||
|
||||
type BottomItem = {
|
||||
@ -209,6 +211,7 @@ function NavItem({
|
||||
if (isCollapsed) {
|
||||
// For sections, navigate to the first item when collapsed
|
||||
const collapsedHref = isSection && item.firstItemHref ? item.firstItemHref : href;
|
||||
const collapsedTarget = isSection && item.firstItemExternal ? "_blank" : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
@ -226,7 +229,7 @@ function NavItem({
|
||||
: "hover:bg-white/40 dark:hover:bg-background/60 text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Link href={collapsedHref ?? "#"} onClick={onClick}>
|
||||
<Link href={collapsedHref ?? "#"} target={collapsedTarget} onClick={onClick}>
|
||||
<IconComponent className={iconClasses} />
|
||||
</Link>
|
||||
</Button>
|
||||
@ -351,6 +354,7 @@ function NavSubItem({
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
target={item.external ? "_blank" : undefined}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-150 hover:transition-none",
|
||||
@ -404,6 +408,7 @@ function AppNavItem({
|
||||
const items = navigableFrontend.navigationItems.map((navItem) => ({
|
||||
name: navItem.displayName,
|
||||
href: getItemPath(projectId, navigableFrontend, navItem),
|
||||
external: navItem.external,
|
||||
match: (fullUrl: URL) => testItemPath(projectId, navigableFrontend, navItem, fullUrl),
|
||||
}));
|
||||
return {
|
||||
@ -413,6 +418,7 @@ function AppNavItem({
|
||||
href: getAppPath(projectId, appFrontend),
|
||||
icon: appFrontend.icon,
|
||||
firstItemHref: items[0]?.href,
|
||||
firstItemExternal: items[0]?.external,
|
||||
};
|
||||
}, [app.displayName, appId, appFrontend, projectId]);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useAdminApp, useProjectId } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app";
|
||||
import { useRouter } from "@/components/router";
|
||||
import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui";
|
||||
import { ALL_APPS_FRONTEND, AppFrontend, getAppPath, isSubApp } from "@/lib/apps-frontend";
|
||||
import { ALL_APPS_FRONTEND, AppFrontend, getAppPath, getDocumentationHref, isSubApp } from "@/lib/apps-frontend";
|
||||
import { isAppEnabled } from "@/lib/apps-utils";
|
||||
import { useUpdateConfig } from "@/lib/config-update";
|
||||
import { CheckIcon, DotsThreeVerticalIcon } from "@phosphor-icons/react";
|
||||
@ -220,6 +220,7 @@ export function AppListItem({
|
||||
|
||||
const isEnabled = isAppEnabled(config.apps.installed, appId);
|
||||
const appPath = getAppPath(project.id, appFrontend);
|
||||
const appDestinationPath = getDocumentationHref(appFrontend) ?? appPath;
|
||||
const appDetailsPath = `/projects/${project.id}/apps/${appId}`;
|
||||
const router = useRouter();
|
||||
const parentAppId = isSubApp(appFrontend) ? appFrontend.parentAppId : null;
|
||||
@ -249,7 +250,7 @@ export function AppListItem({
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={parentDestinationPath ?? (isEnabled ? appPath : appDetailsPath)}
|
||||
href={parentDestinationPath ?? (isEnabled ? appDestinationPath : appDetailsPath)}
|
||||
className={cn(
|
||||
"flex items-center gap-3 p-3 rounded-lg transition-all",
|
||||
"hover:bg-gray-50 dark:hover:bg-gray-800/50",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { AppIcon } from "@/components/app-square";
|
||||
import { Badge, Button, Dialog, DialogContent, DialogTitle, ScrollArea, cn } from "@/components/ui";
|
||||
import { ALL_APPS_FRONTEND, isSubApp, type AppId } from "@/lib/apps-frontend";
|
||||
import { ALL_APPS_FRONTEND, getDocumentationHref, isSubApp, type AppId } from "@/lib/apps-frontend";
|
||||
import { ArrowRightIcon, CaretLeftIcon, CaretRightIcon, CheckIcon, LightningIcon, ShieldCheckIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { ALL_APPS, ALL_APP_TAGS } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
import Image from "next/image";
|
||||
@ -25,6 +25,7 @@ export function AppStoreEntry({
|
||||
}) {
|
||||
const app = ALL_APPS[appId];
|
||||
const appFrontend = ALL_APPS_FRONTEND[appId];
|
||||
const isDocumentationBackedApp = getDocumentationHref(appFrontend) != null;
|
||||
const parentAppId = isSubApp(appFrontend) ? appFrontend.parentAppId : null;
|
||||
const parentApp = parentAppId == null ? null : ALL_APPS[parentAppId];
|
||||
const screenshotContainerRef = useRef<HTMLDivElement>(null);
|
||||
@ -154,7 +155,7 @@ export function AppStoreEntry({
|
||||
className="px-8 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium shadow-lg shadow-blue-500/20"
|
||||
>
|
||||
<ArrowRightIcon className="w-4 h-4 mr-2" />
|
||||
Open App
|
||||
{isDocumentationBackedApp ? "Open Docs" : "Open App"}
|
||||
</Button>
|
||||
{onDisable && (
|
||||
<Button
|
||||
|
||||
@ -127,3 +127,27 @@ export function NextJsEnvKeys(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ViteEnvKeys(props: {
|
||||
projectId: string,
|
||||
secretServerKey?: string,
|
||||
}) {
|
||||
const envFileContent = Object.entries({
|
||||
VITE_STACK_API_URL: getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') === "https://api.stack-auth.com" ? undefined : getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL'),
|
||||
VITE_STACK_PROJECT_ID: props.projectId,
|
||||
STACK_SECRET_SERVER_KEY: props.secretServerKey,
|
||||
})
|
||||
.filter(([, value]) => value != null)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("\n");
|
||||
|
||||
return (
|
||||
<CopyField
|
||||
type="textarea"
|
||||
monospace
|
||||
height={envFileContent.split("\n").length * 26}
|
||||
value={envFileContent}
|
||||
fixedSize
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Link } from "@/components/link";
|
||||
import { ChartLineIcon, ClipboardTextIcon, CreditCardIcon, EnvelopeSimpleIcon, FingerprintSimpleIcon, KeyIcon, MailboxIcon, RocketIcon, ShieldCheckIcon, SparkleIcon, TelevisionSimpleIcon, TriangleIcon, UserGearIcon, UsersIcon, VaultIcon, WebhooksLogoIcon } from "@phosphor-icons/react";
|
||||
import { ChartLineIcon, ClipboardTextIcon, CodeIcon, CreditCardIcon, EnvelopeSimpleIcon, FingerprintSimpleIcon, KeyIcon, MailboxIcon, RocketIcon, ShieldCheckIcon, SparkleIcon, TelevisionSimpleIcon, TriangleIcon, UserGearIcon, UsersIcon, VaultIcon, WebhooksLogoIcon } from "@phosphor-icons/react";
|
||||
import { StackAdminApp } from "@stackframe/stack";
|
||||
import { ALL_APPS } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
import { getRelativePart, isChildUrl } from "@stackframe/stack-shared/dist/utils/urls";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
import ConvexLogo from "../../public/convex-logo.png";
|
||||
import NeonLogo from "../../public/neon-logo.png";
|
||||
import TanStackStartLogo from "../../public/tanstack-start-logo.png";
|
||||
import VercelLogo from "../../public/vercel-logo.svg";
|
||||
|
||||
export type AppId = keyof typeof ALL_APPS;
|
||||
@ -25,6 +26,7 @@ type BreadcrumbDefinition = {
|
||||
type AppNavigationItem = {
|
||||
displayName: string,
|
||||
href: string,
|
||||
external?: boolean,
|
||||
matchPath?: (relativePart: string) => boolean,
|
||||
getBreadcrumbItems?: (stackAdminApp: StackAdminApp<false>, relativePart: string) => Promise<BreadcrumbDefinition | null | undefined>,
|
||||
};
|
||||
@ -33,6 +35,7 @@ export type AppFrontend = {
|
||||
icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>,
|
||||
logo?: React.FunctionComponent<{}>,
|
||||
href: string,
|
||||
documentationHref?: string,
|
||||
screenshots: (string | StaticImageData)[],
|
||||
storeDescription: JSX.Element,
|
||||
} & (
|
||||
@ -57,12 +60,24 @@ export function isSubApp(appFrontend: AppFrontend): appFrontend is SubAppFronten
|
||||
return "parentAppId" in appFrontend;
|
||||
}
|
||||
|
||||
export function getDocumentationHref(appFrontend: AppFrontend): string | null {
|
||||
return "documentationHref" in appFrontend ? appFrontend.documentationHref ?? null : null;
|
||||
}
|
||||
|
||||
export function getAppPath(projectId: string, appFrontend: AppFrontend) {
|
||||
const url = new URL(appFrontend.href, `${DUMMY_ORIGIN}/projects/${projectId}/`);
|
||||
return getRelativePart(url);
|
||||
}
|
||||
|
||||
function isExternalHref(href: string) {
|
||||
return href.startsWith("http://") || href.startsWith("https://");
|
||||
}
|
||||
|
||||
export function getItemPath(projectId: string, appFrontend: NavigableAppFrontend, item: AppNavigationItem) {
|
||||
if (item.external || isExternalHref(item.href)) {
|
||||
return item.href;
|
||||
}
|
||||
|
||||
const url = new URL(item.href, new URL(appFrontend.href, `${DUMMY_ORIGIN}/projects/${projectId}/`) + "/");
|
||||
return getRelativePart(url);
|
||||
}
|
||||
@ -82,6 +97,10 @@ export function testAppPath(projectId: string, appFrontend: AppFrontend, fullUrl
|
||||
}
|
||||
|
||||
export function testItemPath(projectId: string, appFrontend: NavigableAppFrontend, item: AppNavigationItem, fullUrl: URL) {
|
||||
if (item.external || isExternalHref(item.href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.matchPath) return item.matchPath(getRelativePart(fullUrl));
|
||||
|
||||
const url = new URL(getItemPath(projectId, appFrontend, item), fullUrl);
|
||||
@ -333,6 +352,27 @@ export const ALL_APPS_FRONTEND = {
|
||||
screenshots: getScreenshots('vercel', 2),
|
||||
storeDescription: <>Deploy your Stack Auth project to <Link href="https://vercel.com" target="_blank">Vercel</Link> with the Vercel x Stack Auth integration.</>,
|
||||
},
|
||||
"tanstack-start": {
|
||||
icon: CodeIcon,
|
||||
logo: () => <Image src={TanStackStartLogo} alt="TanStack Start logo" />,
|
||||
href: "tanstack-start",
|
||||
documentationHref: "https://docs.stack-auth.com/guides/integrations/tanstack-start/overview",
|
||||
navigationItems: [
|
||||
{
|
||||
displayName: "Docs",
|
||||
href: "https://docs.stack-auth.com/guides/integrations/tanstack-start/overview",
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
screenshots: [],
|
||||
storeDescription: (
|
||||
<>
|
||||
<p>TanStack Start integration adds Stack Auth to full-stack React apps built with TanStack Router and Vite.</p>
|
||||
<p>Install the alpha `@stackframe/tanstack-start` package, wire the Stack provider into your root route, and mount the built-in auth handler pages under your app origin.</p>
|
||||
<p>The dashboard sidebar entry opens the integration docs so your team can jump back to setup instructions from the project.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
analytics: {
|
||||
icon: ChartLineIcon,
|
||||
href: "analytics",
|
||||
@ -445,4 +485,3 @@ async function getEmailDraftBreadcrumbItems(stackAdminApp: StackAdminApp<false>,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -182,6 +182,16 @@
|
||||
],
|
||||
importance: 2,
|
||||
},
|
||||
{
|
||||
name: "TanStack Start demo",
|
||||
portSuffix: "43",
|
||||
description: [
|
||||
"Src: ./examples/tanstack-start-demo",
|
||||
"Alpha SDK integration demo",
|
||||
],
|
||||
img: "https://tanstack.com/favicon.ico",
|
||||
importance: 2,
|
||||
},
|
||||
{
|
||||
name: "Docs",
|
||||
portSuffix: "26",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
"guides/integrations/tanstack-start/overview",
|
||||
"guides/integrations/supabase/overview",
|
||||
"guides/integrations/convex/overview",
|
||||
"guides/integrations/vercel/overview"
|
||||
|
||||
166
docs-mintlify/guides/integrations/tanstack-start/overview.mdx
Normal file
166
docs-mintlify/guides/integrations/tanstack-start/overview.mdx
Normal file
@ -0,0 +1,166 @@
|
||||
---
|
||||
title: TanStack Start
|
||||
description: Add Stack Auth to a TanStack Start app.
|
||||
---
|
||||
|
||||
<Info>
|
||||
The `@stackframe/tanstack-start` package is currently alpha. Pin exact package versions before shipping production apps.
|
||||
</Info>
|
||||
|
||||
TanStack Start is a full-stack React framework built on TanStack Router and Vite. Stack Auth's TanStack Start package provides the same auth components and hooks as the React SDK, with cookie handling wired for TanStack Start.
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Create or open a TanStack Start app">
|
||||
If you do not have a TanStack Start app yet, create one with the TanStack CLI:
|
||||
|
||||
```bash title="Terminal"
|
||||
npx @tanstack/cli@latest create
|
||||
```
|
||||
|
||||
TanStack also publishes official examples if you prefer to start from a working project.
|
||||
</Step>
|
||||
|
||||
<Step title="Install Stack Auth">
|
||||
Install the alpha TanStack Start package:
|
||||
|
||||
```bash title="Terminal"
|
||||
npm install @stackframe/tanstack-start
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Create Stack Auth keys">
|
||||
In the [Stack Auth dashboard](https://app.stack-auth.com/projects), create a project and add these variables to your TanStack Start environment:
|
||||
|
||||
```bash title=".env"
|
||||
VITE_STACK_PROJECT_ID=<your-project-id>
|
||||
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
|
||||
```
|
||||
|
||||
Keep `STACK_SECRET_SERVER_KEY` server-only. Do not expose it to client code.
|
||||
</Step>
|
||||
|
||||
<Step title="Create a Stack client app">
|
||||
Create a Stack client app with cookie storage:
|
||||
|
||||
```ts title="src/stack/client.ts"
|
||||
import { StackClientApp } from "@stackframe/tanstack-start";
|
||||
|
||||
export const stackClientApp = new StackClientApp({
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: "window",
|
||||
});
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Wrap the root route">
|
||||
Add `StackProvider` and `StackTheme` around your route outlet:
|
||||
|
||||
```tsx title="src/routes/__root.tsx"
|
||||
import { StackProvider, StackTheme } from "@stackframe/tanstack-start";
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import { stackClientApp } from "../stack/client";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<StackProvider app={stackClientApp}>
|
||||
<StackTheme>
|
||||
<Outlet />
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Add the auth handler route">
|
||||
Create a splat route at `/handler/*` for Stack Auth's built-in pages:
|
||||
|
||||
```tsx title="src/routes/handler/$.tsx"
|
||||
import { StackHandler } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/handler/$")({
|
||||
ssr: false,
|
||||
component: HandlerPage,
|
||||
});
|
||||
|
||||
function HandlerPage() {
|
||||
const { pathname } = useLocation();
|
||||
return <StackHandler fullPage location={pathname} />;
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Use auth in routes">
|
||||
Use Stack hooks from inside components rendered under the provider:
|
||||
|
||||
```tsx title="src/routes/index.tsx"
|
||||
import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
const user = useUser();
|
||||
|
||||
if (!user) {
|
||||
return <Link to="/handler/sign-in">Sign in</Link>;
|
||||
}
|
||||
|
||||
return <div>Signed in as {user.primaryEmail}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Routes that use browser redirects should render on the client:
|
||||
|
||||
```tsx title="src/routes/protected.tsx"
|
||||
import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/protected")({
|
||||
ssr: false,
|
||||
component: ProtectedPage,
|
||||
});
|
||||
|
||||
function ProtectedPage() {
|
||||
const user = useUser({ or: "redirect" });
|
||||
return <div>Welcome, {user.primaryEmail}</div>;
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Notes
|
||||
|
||||
- The handler route must stay under the same origin as your app when using `tokenStore: "cookie"`.
|
||||
- Render the handler route on the client (`ssr: false`) because built-in auth pages read browser location state.
|
||||
- Render routes that rely on `useUser({ or: "redirect" })` on the client (`ssr: false`) when using `redirectMethod: "window"`.
|
||||
- Use `redirectMethod: "window"` unless you explicitly wire a TanStack Router navigation adapter.
|
||||
- If you change auth routes, configure the matching `urls` on `StackClientApp`.
|
||||
- For server-only logic, use TanStack Start server functions and keep `STACK_SECRET_SERVER_KEY` out of client modules.
|
||||
|
||||
For TanStack Start framework details, see the [TanStack Start quick start](https://tanstack.com/start/latest/docs/framework/react/quick-start) and [server functions guide](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions).
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { ALL_APPS, AppId } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
import { AppIcon, appSquarePaddingExpression, appSquareWidthExpression } from "@stackframe/stack-shared/dist/apps/apps-ui";
|
||||
import { BarChart3, ClipboardList, CreditCard, KeyRound, Mail, Mails, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
|
||||
import { BarChart3, ClipboardList, Code, CreditCard, KeyRound, Mail, Mails, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
@ -10,6 +10,7 @@ import { cn } from "../../lib/cn";
|
||||
const APP_URL_OVERRIDES: Partial<Record<AppId, string>> = {
|
||||
teams: '/docs/apps/orgs-and-teams',
|
||||
rbac: '/docs/apps/permissions',
|
||||
"tanstack-start": '/docs/guides/integrations/tanstack-start/overview',
|
||||
};
|
||||
|
||||
// Icon mapping for docs (no Next.js Image dependencies)
|
||||
@ -38,6 +39,7 @@ const APP_ICONS: Record<AppId, React.FunctionComponent<React.SVGProps<SVGSVGElem
|
||||
</>
|
||||
)),
|
||||
vercel: Triangle,
|
||||
"tanstack-start": Code,
|
||||
onboarding: ClipboardList,
|
||||
analytics: BarChart3,
|
||||
};
|
||||
@ -121,4 +123,3 @@ export function AppGrid({ appIds, className }: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
3
examples/tanstack-start-demo/.env.development
Normal file
3
examples/tanstack-start-demo/.env.development
Normal file
@ -0,0 +1,3 @@
|
||||
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
|
||||
VITE_STACK_PROJECT_ID=internal
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
|
||||
4
examples/tanstack-start-demo/.eslintrc.cjs
Normal file
4
examples/tanstack-start-demo/.eslintrc.cjs
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ["../../configs/eslint/defaults.js"],
|
||||
ignorePatterns: ["/*", "!/src"],
|
||||
};
|
||||
4
examples/tanstack-start-demo/.gitignore
vendored
Normal file
4
examples/tanstack-start-demo/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.output
|
||||
.tanstack
|
||||
dist
|
||||
node_modules
|
||||
40
examples/tanstack-start-demo/package.json
Normal file
40
examples/tanstack-start-demo/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@stackframe/example-tanstack-start-demo",
|
||||
"version": "2.8.86",
|
||||
"repository": "https://github.com/stack-auth/stack-auth",
|
||||
"description": "TanStack Start demo app for Stack Auth",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rimraf .output && rimraf node_modules",
|
||||
"dev": "vite dev --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}43",
|
||||
"build": "vite build",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"lint": "eslint --ext .ts,.tsx ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@stackframe/stack-shared": "workspace:*",
|
||||
"@stackframe/stack-ui": "workspace:*",
|
||||
"@stackframe/tanstack-start": "workspace:*",
|
||||
"@tanstack/react-router": "^1.168.23",
|
||||
"@tanstack/react-start": "^1.167.42",
|
||||
"nitro": "^3.0.0",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.0",
|
||||
"@types/react": "19.2.1",
|
||||
"@types/react-dom": "19.2.1",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"rimraf": "^5.0.10",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.23.0"
|
||||
}
|
||||
6
examples/tanstack-start-demo/postcss.config.js
Normal file
6
examples/tanstack-start-demo/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
12
examples/tanstack-start-demo/src/client.tsx
Normal file
12
examples/tanstack-start-demo/src/client.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { StartClient } from "@tanstack/react-start/client";
|
||||
import { StrictMode, startTransition } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<StartClient />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
33
examples/tanstack-start-demo/src/components/header.tsx
Normal file
33
examples/tanstack-start-demo/src/components/header.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { UserButton } from "@stackframe/tanstack-start";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<>
|
||||
<header className="fixed left-0 right-0 top-0 z-50 border-b border-zinc-200 bg-white/95 px-4 backdrop-blur dark:border-zinc-800 dark:bg-zinc-950/95">
|
||||
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between gap-4">
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link to="/" className="font-semibold tracking-tight">
|
||||
Stack TanStack Demo
|
||||
</Link>
|
||||
<Link to="/protected" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
|
||||
Protected
|
||||
</Link>
|
||||
</nav>
|
||||
<ClientMountedUserButton />
|
||||
</div>
|
||||
</header>
|
||||
<div className="h-14" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientMountedUserButton() {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
return isMounted ? <UserButton /> : <div className="h-9 w-9" />;
|
||||
}
|
||||
104
examples/tanstack-start-demo/src/routeTree.gen.ts
Normal file
104
examples/tanstack-start-demo/src/routeTree.gen.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ProtectedRouteImport } from './routes/protected'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as HandlerSplatRouteImport } from './routes/handler/$'
|
||||
|
||||
const ProtectedRoute = ProtectedRouteImport.update({
|
||||
id: '/protected',
|
||||
path: '/protected',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const HandlerSplatRoute = HandlerSplatRouteImport.update({
|
||||
id: '/handler/$',
|
||||
path: '/handler/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/protected': typeof ProtectedRoute
|
||||
'/handler/$': typeof HandlerSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/protected': typeof ProtectedRoute
|
||||
'/handler/$': typeof HandlerSplatRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/protected': typeof ProtectedRoute
|
||||
'/handler/$': typeof HandlerSplatRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/protected' | '/handler/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/protected' | '/handler/$'
|
||||
id: '__root__' | '/' | '/protected' | '/handler/$'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
ProtectedRoute: typeof ProtectedRoute
|
||||
HandlerSplatRoute: typeof HandlerSplatRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/protected': {
|
||||
id: '/protected'
|
||||
path: '/protected'
|
||||
fullPath: '/protected'
|
||||
preLoaderRoute: typeof ProtectedRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/handler/$': {
|
||||
id: '/handler/$'
|
||||
path: '/handler/$'
|
||||
fullPath: '/handler/$'
|
||||
preLoaderRoute: typeof HandlerSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ProtectedRoute: ProtectedRoute,
|
||||
HandlerSplatRoute: HandlerSplatRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
18
examples/tanstack-start-demo/src/router.tsx
Normal file
18
examples/tanstack-start-demo/src/router.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { createRouter } from "@tanstack/react-router";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
export function getRouter() {
|
||||
return createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultNotFoundComponent: () => (
|
||||
<main className="grid min-h-screen place-items-center bg-zinc-100 px-4 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
|
||||
<div className="w-full max-w-md rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">404</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
|
||||
<p className="mt-4 text-zinc-600 dark:text-zinc-300">This route is not part of the TanStack Start demo.</p>
|
||||
</div>
|
||||
</main>
|
||||
),
|
||||
});
|
||||
}
|
||||
58
examples/tanstack-start-demo/src/routes/__root.tsx
Normal file
58
examples/tanstack-start-demo/src/routes/__root.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
/// <reference types="vite/client" />
|
||||
import "../styles.css";
|
||||
|
||||
import { StackProvider, StackTheme } from "@stackframe/tanstack-start";
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import type { ReactNode } from "react";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { Header } from "~/components/header";
|
||||
import { createStackApp } from "~/stack";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: "utf-8" },
|
||||
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||
{ title: "Stack Auth TanStack Start Demo" },
|
||||
{
|
||||
name: "description",
|
||||
content: "TanStack Start demo application using Stack Auth.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
shellComponent: RootDocument,
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function RootComponent() {
|
||||
const stackApp = useMemo(() => createStackApp(), []);
|
||||
|
||||
return (
|
||||
<StackProvider app={stackApp}>
|
||||
<StackTheme>
|
||||
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
|
||||
<Header />
|
||||
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
|
||||
<Suspense fallback={null}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
12
examples/tanstack-start-demo/src/routes/handler/$.tsx
Normal file
12
examples/tanstack-start-demo/src/routes/handler/$.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { StackHandler } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/handler/$")({
|
||||
ssr: false,
|
||||
component: HandlerPage,
|
||||
});
|
||||
|
||||
function HandlerPage() {
|
||||
const { pathname } = useLocation();
|
||||
return <StackHandler fullPage location={pathname} />;
|
||||
}
|
||||
76
examples/tanstack-start-demo/src/routes/index.tsx
Normal file
76
examples/tanstack-start-demo/src/routes/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { UserAvatar, useStackApp, useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
const user = useUser({ includeRestricted: true });
|
||||
const app = useStackApp();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<section className="grid w-full place-items-center">
|
||||
<div className="w-full max-w-xl rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">TanStack Start alpha</p>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Welcome to the Stack demo app.</h1>
|
||||
<p className="mt-4 text-zinc-600 dark:text-zinc-300">
|
||||
This example uses <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-sm dark:bg-zinc-800">@stackframe/tanstack-start</code> with file-based routes and Stack Auth handler pages.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<button className="rounded-md bg-zinc-950 px-4 py-2 text-sm font-medium text-white transition-colors hover:transition-none dark:bg-white dark:text-zinc-950" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignIn())}>
|
||||
Sign in
|
||||
</button>
|
||||
<button className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignUp())}>
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="grid w-full place-items-center">
|
||||
<div className="w-full max-w-2xl rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-center">
|
||||
<UserAvatar user={user} size={96} />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">Signed in as</p>
|
||||
<h1 className="truncate text-3xl font-semibold tracking-tight">{user.displayName ?? user.primaryEmail ?? user.id}</h1>
|
||||
{user.isRestricted && (
|
||||
<span className="mt-2 inline-flex rounded bg-amber-100 px-2 py-1 text-sm font-medium text-amber-800 dark:bg-amber-900/40 dark:text-amber-200">
|
||||
Restricted
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl className="mt-8 grid gap-3 text-sm">
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
|
||||
<dt className="font-medium text-zinc-500 dark:text-zinc-400">User ID</dt>
|
||||
<dd className="min-w-0 break-all font-mono">{user.id}</dd>
|
||||
</div>
|
||||
{user.primaryEmail && (
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
|
||||
<dt className="font-medium text-zinc-500 dark:text-zinc-400">Email</dt>
|
||||
<dd>{user.primaryEmail}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
|
||||
<dt className="font-medium text-zinc-500 dark:text-zinc-400">Restricted</dt>
|
||||
<dd>{user.isRestricted ? `Yes${user.restrictedReason ? ` (${user.restrictedReason.type})` : ""}` : "No"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<button className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 hover:transition-none" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignOut())}>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
23
examples/tanstack-start-demo/src/routes/protected.tsx
Normal file
23
examples/tanstack-start-demo/src/routes/protected.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/protected")({
|
||||
ssr: false,
|
||||
component: ProtectedPage,
|
||||
});
|
||||
|
||||
function ProtectedPage() {
|
||||
const user = useUser({ or: "redirect" });
|
||||
|
||||
return (
|
||||
<section className="grid w-full place-items-center">
|
||||
<div className="w-full max-w-xl rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p className="mb-2 text-sm font-medium text-green-600 dark:text-green-400">Protected route</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">You can see this because you are signed in.</h1>
|
||||
<p className="mt-4 text-zinc-600 dark:text-zinc-300">
|
||||
TanStack Start rendered this route with Stack Auth session state for <span className="font-medium text-zinc-950 dark:text-zinc-50">{user.displayName ?? user.primaryEmail ?? user.id}</span>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
29
examples/tanstack-start-demo/src/stack.ts
Normal file
29
examples/tanstack-start-demo/src/stack.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { StackClientApp } from "@stackframe/tanstack-start";
|
||||
|
||||
function getPortPrefix(): string {
|
||||
return import.meta.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81";
|
||||
}
|
||||
|
||||
function replaceStackPortPrefix(value: string): string {
|
||||
return value.replace(/\$\{NEXT_PUBLIC_STACK_PORT_PREFIX:-81\}/g, getPortPrefix());
|
||||
}
|
||||
|
||||
function getStackApiUrl(): string {
|
||||
const configured = import.meta.env.VITE_STACK_API_URL as string | undefined;
|
||||
return configured ? replaceStackPortPrefix(configured) : `http://localhost:${getPortPrefix()}02`;
|
||||
}
|
||||
|
||||
export function createStackApp() {
|
||||
return new StackClientApp({
|
||||
projectId: import.meta.env.VITE_STACK_PROJECT_ID ?? "internal",
|
||||
publishableClientKey: import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY ?? "this-publishable-client-key-is-for-local-development-only",
|
||||
baseUrl: getStackApiUrl(),
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: "window",
|
||||
urls: {
|
||||
afterSignIn: "/protected",
|
||||
afterSignUp: "/protected",
|
||||
afterSignOut: "/",
|
||||
},
|
||||
});
|
||||
}
|
||||
24
examples/tanstack-start-demo/src/styles.css
Normal file
24
examples/tanstack-start-demo/src/styles.css
Normal file
@ -0,0 +1,24 @@
|
||||
/* stylelint-disable scss/at-rule-no-unknown */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* stylelint-enable scss/at-rule-no-unknown */
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
background: rgb(244 244 245);
|
||||
}
|
||||
|
||||
html:has(head > [data-stack-theme="dark"]) {
|
||||
color-scheme: dark;
|
||||
background: rgb(9 9 11);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
12
examples/tanstack-start-demo/tailwind.config.js
Normal file
12
examples/tanstack-start-demo/tailwind.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ["selector", 'html:has(head > [data-stack-theme="dark"])'],
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/stack-ui/src/**/*.{ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
20
examples/tanstack-start-demo/tsconfig.json
Normal file
20
examples/tanstack-start-demo/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
94
examples/tanstack-start-demo/vite.config.ts
Normal file
94
examples/tanstack-start-demo/vite.config.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { nitro } from "nitro/vite";
|
||||
import tsConfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const stackAuthRootPath = fileURLToPath(new URL("../..", import.meta.url));
|
||||
|
||||
function watchNodeModules(modules: string[]): Plugin {
|
||||
return {
|
||||
name: "watch-node-modules",
|
||||
config() {
|
||||
return {
|
||||
server: {
|
||||
watch: {
|
||||
ignored: modules.map((moduleName) => `!**/node_modules/${moduleName}/**`),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function waitForWorkspacePackages(packages: string[]): Plugin {
|
||||
const packageDistEntries = packages.map((pkg) => ({
|
||||
name: pkg,
|
||||
entry: path.resolve(__dirname, "node_modules", pkg, "dist", "esm", "index.js"),
|
||||
}));
|
||||
|
||||
async function waitForFile(filePath: string, timeoutMs = 60_000): Promise<void> {
|
||||
if (fs.existsSync(filePath)) return;
|
||||
const start = performance.now();
|
||||
return await new Promise((resolve, reject) => {
|
||||
const interval = setInterval(() => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else if (performance.now() - start > timeoutMs) {
|
||||
clearInterval(interval);
|
||||
reject(new Error(`Timed out waiting for ${filePath} to exist`));
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "wait-for-workspace-packages",
|
||||
enforce: "pre",
|
||||
async buildStart() {
|
||||
const missing = packageDistEntries.filter((pkg) => !fs.existsSync(pkg.entry));
|
||||
if (missing.length === 0) return;
|
||||
console.log(`Waiting for workspace packages to build: ${missing.map((pkg) => pkg.name).join(", ")}`);
|
||||
await Promise.all(missing.map((pkg) => waitForFile(pkg.entry)));
|
||||
console.log("All workspace packages are ready.");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isVitest = mode === "test" || process.env.VITEST === "true";
|
||||
|
||||
return {
|
||||
server: {
|
||||
port: Number(`${process.env.NEXT_PUBLIC_STACK_PORT_PREFIX || "81"}43`),
|
||||
fs: {
|
||||
allow: [stackAuthRootPath],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
ssr: {
|
||||
noExternal: [/^@stackframe\//, /^@radix-ui\//],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["@stackframe/stack-shared", "@stackframe/stack-shared/config"],
|
||||
},
|
||||
plugins: [
|
||||
...(isVitest ? [] : [
|
||||
waitForWorkspacePackages(["@stackframe/tanstack-start", "@stackframe/stack-shared", "@stackframe/stack-ui"]),
|
||||
watchNodeModules(["@stackframe/tanstack-start", "@stackframe/stack-shared", "@stackframe/stack-ui"]),
|
||||
]),
|
||||
tsConfigPaths(),
|
||||
...(isVitest ? [] : [
|
||||
tanstackStart(),
|
||||
nitro(),
|
||||
]),
|
||||
viteReact(),
|
||||
],
|
||||
};
|
||||
});
|
||||
@ -51,8 +51,8 @@
|
||||
"db:migrate": "pnpm pre && pnpm run --filter=@stackframe/backend db:migrate",
|
||||
"fern": "pnpm pre && pnpm run --filter=@stackframe/docs fern",
|
||||
"dev:full": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-setup-prompt-docs:watch\" \"turbo run dev --concurrency 99999\"",
|
||||
"dev": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-openapi-docs:watch\" \"pnpm run generate-setup-prompt-docs:watch\" \"turbo run dev --concurrency 99999 --filter=./apps/* --filter=@stackframe/docs-mintlify --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo \"",
|
||||
"dev:tui": "pnpm pre && (trap 'kill 0' EXIT; pnpm run generate-sdks:watch & pnpm run generate-openapi-docs:watch & pnpm run generate-setup-prompt-docs:watch & turbo run dev --ui tui --concurrency 99999 --filter=./apps/* --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo)",
|
||||
"dev": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-openapi-docs:watch\" \"pnpm run generate-setup-prompt-docs:watch\" \"turbo run dev --concurrency 99999 --filter=./apps/* --filter=@stackframe/docs-mintlify --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo --filter=./examples/tanstack-start-demo \"",
|
||||
"dev:tui": "pnpm pre && (trap 'kill 0' EXIT; pnpm run generate-sdks:watch & pnpm run generate-openapi-docs:watch & pnpm run generate-setup-prompt-docs:watch & turbo run dev --ui tui --concurrency 99999 --filter=./apps/* --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo --filter=./examples/tanstack-start-demo)",
|
||||
"dev:inspect": "pnpm pre && STACK_BACKEND_DEV_EXTRA_ARGS=\"--inspect\" pnpm run dev",
|
||||
"dev:profile": "pnpm pre && STACK_BACKEND_DEV_EXTRA_ARGS=\"--experimental-cpu-prof\" pnpm run dev",
|
||||
"dev:basic": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/backend --filter=@stackframe/mcp --filter=@stackframe/dashboard --filter=@stackframe/mock-oauth-server\"",
|
||||
|
||||
@ -85,4 +85,4 @@
|
||||
"tsdown": "^0.20.3",
|
||||
"convex": "^1.27.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,4 +114,4 @@
|
||||
"tsdown": "^0.20.3",
|
||||
"convex": "^1.27.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,6 +150,12 @@ export const ALL_APPS = {
|
||||
tags: ["integration", "developers"],
|
||||
stage: "stable",
|
||||
},
|
||||
"tanstack-start": {
|
||||
displayName: "TanStack Start",
|
||||
subtitle: "Use Stack Auth in TanStack Start apps",
|
||||
tags: ["integration", "developers"],
|
||||
stage: "alpha",
|
||||
},
|
||||
"analytics": {
|
||||
displayName: "Analytics",
|
||||
subtitle: "View and explore analytics data",
|
||||
|
||||
@ -122,4 +122,4 @@
|
||||
"tsdown": "^0.20.3",
|
||||
"convex": "^1.27.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
132
packages/tanstack-start/package.json
Normal file
132
packages/tanstack-start/package.json
Normal file
@ -0,0 +1,132 @@
|
||||
{
|
||||
"//": "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.88",
|
||||
"repository": "https://github.com/hexclave/stack-auth",
|
||||
"sideEffects": false,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": {
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./tanstack-start-server-context": {
|
||||
"types": "./dist/tanstack-start-server-context.combined.d.ts",
|
||||
"import": {
|
||||
"browser": "./dist/esm/tanstack-start-server-context.default.js",
|
||||
"default": "./dist/esm/tanstack-start-server-context.server.js"
|
||||
},
|
||||
"require": {
|
||||
"browser": "./dist/tanstack-start-server-context.default.js",
|
||||
"default": "./dist/tanstack-start-server-context.server.js"
|
||||
}
|
||||
},
|
||||
"./convex.config": {
|
||||
"types": "./dist/integrations/convex/component/convex.config.d.ts",
|
||||
"import": {
|
||||
"default": "./dist/esm/integrations/convex/component/convex.config.js"
|
||||
},
|
||||
"require": {
|
||||
"default": "./dist/integrations/convex/component/convex.config.js"
|
||||
}
|
||||
},
|
||||
"./convex-auth.config": {
|
||||
"types": "./dist/integrations/convex.d.ts",
|
||||
"import": {
|
||||
"default": "./dist/esm/integrations/convex.js"
|
||||
},
|
||||
"require": {
|
||||
"default": "./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": "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": {
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
@ -26,6 +28,19 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"//": "IF_PLATFORM tanstack-start",
|
||||
"./tanstack-start-server-context": {
|
||||
"types": "./dist/tanstack-start-server-context.combined.d.ts",
|
||||
"import": {
|
||||
"browser": "./dist/esm/tanstack-start-server-context.default.js",
|
||||
"default": "./dist/esm/tanstack-start-server-context.server.js"
|
||||
},
|
||||
"require": {
|
||||
"browser": "./dist/tanstack-start-server-context.default.js",
|
||||
"default": "./dist/tanstack-start-server-context.server.js"
|
||||
}
|
||||
},
|
||||
"//": "END_PLATFORM",
|
||||
"./convex.config": {
|
||||
"types": "./dist/integrations/convex/component/convex.config.d.ts",
|
||||
"import": {
|
||||
@ -131,6 +146,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",
|
||||
@ -160,6 +179,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",
|
||||
|
||||
@ -17,6 +17,17 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./tanstack-start-server-context": {
|
||||
"types": "./dist/tanstack-start-server-context.combined.d.ts",
|
||||
"import": {
|
||||
"browser": "./dist/esm/tanstack-start-server-context.default.js",
|
||||
"default": "./dist/esm/tanstack-start-server-context.server.js"
|
||||
},
|
||||
"require": {
|
||||
"browser": "./dist/tanstack-start-server-context.default.js",
|
||||
"default": "./dist/tanstack-start-server-context.server.js"
|
||||
}
|
||||
},
|
||||
"./convex.config": {
|
||||
"types": "./dist/integrations/convex/component/convex.config.d.ts",
|
||||
"import": {
|
||||
@ -94,6 +105,8 @@
|
||||
"@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": {
|
||||
@ -117,6 +130,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 +142,4 @@
|
||||
"tsdown": "^0.20.3",
|
||||
"convex": "^1.27.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,8 +236,8 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial<RouteProps>
|
||||
const navigate = stackApp.useNavigate();
|
||||
const navigateRef = useRef(navigate);
|
||||
navigateRef.current = navigate;
|
||||
const currentLocation = props.location ?? window.location.pathname;
|
||||
const searchParamsSource = new URLSearchParams(window.location.search);
|
||||
const currentLocation = props.location ?? (typeof window === "undefined" ? new URL(stackApp.urls.handler, placeholderOrigin).pathname : window.location.pathname);
|
||||
const searchParamsSource = new URLSearchParams(typeof window === "undefined" ? "" : window.location.search);
|
||||
const redirectTargets: (string | undefined)[] = [];
|
||||
END_PLATFORM */
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { cookies as rscCookies, headers as rscHeaders } from '@stackframe/stack-sc/force-react-server'; // THIS_LINE_PLATFORM next
|
||||
import { isBrowserLike } from '@stackframe/stack-shared/dist/utils/env';
|
||||
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
|
||||
import * as tanstackStartServerContext from "@stackframe/tanstack-start/tanstack-start-server-context"; // THIS_LINE_PLATFORM tanstack-start
|
||||
import Cookies from "js-cookie";
|
||||
import { calculatePKCECodeChallenge, generateRandomCodeVerifier, generateRandomState } from "oauth4webapi";
|
||||
|
||||
@ -67,6 +68,49 @@ import { calculatePKCECodeChallenge, generateRandomCodeVerifier, generateRandomS
|
||||
type SetCookieOptions = { maxAge: number | "session", noOpIfServerComponent?: boolean, domain?: string, secure?: boolean };
|
||||
type DeleteCookieOptions = { noOpIfServerComponent?: boolean, domain?: string };
|
||||
|
||||
// IF_PLATFORM tanstack-start
|
||||
let tanStackStartCookieHelperPromise: Promise<CookieHelper> | null = null;
|
||||
|
||||
function getTanStackStartServerContext() {
|
||||
const {
|
||||
deleteCookie,
|
||||
getCookie,
|
||||
getCookies,
|
||||
getRequestHeader,
|
||||
setCookie,
|
||||
} = tanstackStartServerContext;
|
||||
if (
|
||||
deleteCookie == null
|
||||
|| getCookie == null
|
||||
|| getCookies == null
|
||||
|| getRequestHeader == null
|
||||
|| setCookie == null
|
||||
) {
|
||||
throw new StackAssertionError("TanStack Start server context is only available during server rendering");
|
||||
}
|
||||
return {
|
||||
deleteCookie,
|
||||
getCookie,
|
||||
getCookies,
|
||||
getRequestHeader,
|
||||
setCookie,
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface ImportMetaEnv {
|
||||
SSR: boolean,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv,
|
||||
}
|
||||
}
|
||||
|
||||
// END_PLATFORM
|
||||
|
||||
function ensureClient() {
|
||||
if (!isBrowserLike()) {
|
||||
throw new Error("cookieClient functions can only be called in a browser environment, yet window is undefined");
|
||||
@ -95,6 +139,16 @@ export async function createPlaceholderCookieHelper(): Promise<CookieHelper> {
|
||||
};
|
||||
}
|
||||
|
||||
function requiresSecureAttribute(name: string): boolean {
|
||||
return name.startsWith("__Host-");
|
||||
}
|
||||
|
||||
function validateCookieOptions(name: string, options: DeleteCookieOptions | SetCookieOptions) {
|
||||
if (requiresSecureAttribute(name) && options.domain !== undefined) {
|
||||
throw new StackAssertionError("__Host- cookies must not specify a Domain attribute");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCookieHelper(): Promise<CookieHelper> {
|
||||
if (isBrowserLike()) {
|
||||
return createBrowserCookieHelper();
|
||||
@ -104,12 +158,90 @@ export async function createCookieHelper(): Promise<CookieHelper> {
|
||||
await rscCookies(),
|
||||
await rscHeaders(),
|
||||
);
|
||||
// ELSE_IF_PLATFORM tanstack-start
|
||||
if (import.meta.env.SSR) {
|
||||
const cookieHelperPromise = tanStackStartCookieHelperPromise
|
||||
?? Promise.resolve(createTanStackStartCookieHelper(getTanStackStartServerContext()));
|
||||
tanStackStartCookieHelperPromise = cookieHelperPromise;
|
||||
return await cookieHelperPromise;
|
||||
}
|
||||
return await createPlaceholderCookieHelper();
|
||||
// ELSE_PLATFORM
|
||||
return await createPlaceholderCookieHelper();
|
||||
// END_PLATFORM
|
||||
}
|
||||
}
|
||||
|
||||
export function createCookieHelperSync(): CookieHelper {
|
||||
if (isBrowserLike()) {
|
||||
return createBrowserCookieHelper();
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// IF_PLATFORM tanstack-start
|
||||
function determineSecureFromTanStackStartContext(api: ReturnType<typeof getTanStackStartServerContext>): boolean {
|
||||
return api.getRequestHeader("x-forwarded-proto") === "https"
|
||||
|| (api.getCookie("stack-is-https") !== undefined);
|
||||
}
|
||||
|
||||
function refreshTanStackStartIsHttpsCookie(api: ReturnType<typeof getTanStackStartServerContext>) {
|
||||
api.setCookie("stack-is-https", "true", {
|
||||
secure: true,
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
function createTanStackStartCookieHelper(api: ReturnType<typeof getTanStackStartServerContext>): CookieHelper {
|
||||
const helper: CookieHelper = {
|
||||
get: (name: string) => {
|
||||
const all = helper.getAll();
|
||||
return all[name] ?? null;
|
||||
},
|
||||
getAll: () => {
|
||||
// set a helper cookie, see comment in `NextCookieHelper.set` below
|
||||
refreshTanStackStartIsHttpsCookie(api);
|
||||
return api.getCookies();
|
||||
},
|
||||
set: (name: string, value: string, options: SetCookieOptions) => {
|
||||
validateCookieOptions(name, options);
|
||||
api.setCookie(name, value, {
|
||||
secure: requiresSecureAttribute(name) || (options.secure ?? determineSecureFromTanStackStartContext(api)),
|
||||
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) => {
|
||||
validateCookieOptions(name, options);
|
||||
const secure = requiresSecureAttribute(name) || determineSecureFromTanStackStartContext(api);
|
||||
api.deleteCookie(name, {
|
||||
secure,
|
||||
domain: options.domain,
|
||||
path: "/",
|
||||
});
|
||||
},
|
||||
};
|
||||
return helper;
|
||||
}
|
||||
// END_PLATFORM
|
||||
|
||||
export function createBrowserCookieHelper(): CookieHelper {
|
||||
return {
|
||||
get: getCookieClient,
|
||||
@ -166,6 +298,7 @@ function createNextCookieHelper(
|
||||
}, {} as Record<string, string>);
|
||||
},
|
||||
set: (name: string, value: string, options: SetCookieOptions) => {
|
||||
validateCookieOptions(name, options);
|
||||
// Whenever the client is on HTTPS, we want to set the Secure flag on the cookie.
|
||||
//
|
||||
// This is not easy to find out on a Next.js server, so see the algorithm at the top of this file.
|
||||
@ -177,10 +310,11 @@ function createNextCookieHelper(
|
||||
|
||||
try {
|
||||
rscCookiesAwaited.set(name, value, {
|
||||
secure: isSecureCookie,
|
||||
secure: requiresSecureAttribute(name) || isSecureCookie,
|
||||
maxAge: options.maxAge === "session" ? undefined : options.maxAge,
|
||||
domain: options.domain,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
} catch (e) {
|
||||
handleCookieError(e, options);
|
||||
@ -195,10 +329,11 @@ function createNextCookieHelper(
|
||||
},
|
||||
delete(name: string, options: DeleteCookieOptions) {
|
||||
try {
|
||||
validateCookieOptions(name, options);
|
||||
if (options.domain !== undefined) {
|
||||
rscCookiesAwaited.delete({ name, domain: options.domain });
|
||||
rscCookiesAwaited.delete({ name, domain: options.domain, path: "/" });
|
||||
} else {
|
||||
rscCookiesAwaited.delete(name);
|
||||
rscCookiesAwaited.delete({ name, path: "/" });
|
||||
}
|
||||
} catch (e) {
|
||||
handleCookieError(e, options);
|
||||
@ -232,6 +367,10 @@ export async function isSecure(): Promise<boolean> {
|
||||
}
|
||||
// IF_PLATFORM next
|
||||
return determineSecureFromServerContext(await rscCookies(), await rscHeaders());
|
||||
// ELSE_IF_PLATFORM tanstack-start
|
||||
if (import.meta.env.SSR) {
|
||||
return determineSecureFromTanStackStartContext(getTanStackStartServerContext());
|
||||
}
|
||||
// END_PLATFORM
|
||||
return false;
|
||||
}
|
||||
@ -299,12 +438,14 @@ function _internalShouldSetPartitionedClient() {
|
||||
}
|
||||
|
||||
function setCookieClientInternal(name: string, value: string, options: SetCookieOptions) {
|
||||
const secure = options.secure ?? determineSecureFromClientContext();
|
||||
validateCookieOptions(name, options);
|
||||
const secure = requiresSecureAttribute(name) || (options.secure ?? determineSecureFromClientContext());
|
||||
const partitioned = shouldSetPartitionedClient();
|
||||
Cookies.set(name, value, {
|
||||
expires: options.maxAge === "session" ? undefined : new Date(Date.now() + (options.maxAge) * 1000),
|
||||
domain: options.domain,
|
||||
secure,
|
||||
path: "/",
|
||||
sameSite: "Lax",
|
||||
...(partitioned ? {
|
||||
partitioned,
|
||||
@ -314,11 +455,12 @@ function setCookieClientInternal(name: string, value: string, options: SetCookie
|
||||
}
|
||||
|
||||
function deleteCookieClientInternal(name: string, options: DeleteCookieOptions) {
|
||||
validateCookieOptions(name, options);
|
||||
for (const partitioned of [true, false]) {
|
||||
if (options.domain !== undefined) {
|
||||
Cookies.remove(name, { domain: options.domain, secure: determineSecureFromClientContext(), partitioned });
|
||||
Cookies.remove(name, { domain: options.domain, secure: determineSecureFromClientContext(), partitioned, path: "/" });
|
||||
}
|
||||
Cookies.remove(name, { secure: determineSecureFromClientContext(), partitioned });
|
||||
Cookies.remove(name, { secure: requiresSecureAttribute(name) || determineSecureFromClientContext(), partitioned, path: "/" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,8 @@ import { BotChallengeExecutionFailedError, BotChallengeUserCancelledError, withB
|
||||
import type { TurnstileAction } from "@stackframe/stack-shared/dist/utils/turnstile";
|
||||
import { isRelative } from "@stackframe/stack-shared/dist/utils/urls";
|
||||
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
|
||||
import * as tanstackStartServerContext from "@stackframe/tanstack-start/tanstack-start-server-context"; // THIS_LINE_PLATFORM tanstack-start
|
||||
import * as TanStackRouter from "@tanstack/react-router"; // THIS_LINE_PLATFORM tanstack-start
|
||||
import * as cookie from "cookie";
|
||||
import * as NextNavigationUnscrambled from "next/navigation"; // import the entire module to get around some static compiler warnings emitted by Next.js in some cases | THIS_LINE_PLATFORM next
|
||||
import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
|
||||
@ -152,6 +154,26 @@ function getHeaderValueFromRequestLikeHeaders(headers: RequestLike["headers"], n
|
||||
return null;
|
||||
}
|
||||
|
||||
// IF_PLATFORM tanstack-start
|
||||
function getTanStackStartRequestHeader(name: string): string | null {
|
||||
const { getRequestHeader } = tanstackStartServerContext;
|
||||
if (getRequestHeader == null) {
|
||||
throw new StackAssertionError("TanStack Start request headers are only available during server rendering");
|
||||
}
|
||||
return getRequestHeader(name) ?? null;
|
||||
}
|
||||
// END_PLATFORM
|
||||
|
||||
async function getServerRequestHost(): Promise<string | null> {
|
||||
// IF_PLATFORM next
|
||||
return (await sc.headers?.())?.get("host") ?? null;
|
||||
// ELSE_IF_PLATFORM tanstack-start
|
||||
return getTanStackStartRequestHeader("host");
|
||||
// ELSE_PLATFORM
|
||||
return null;
|
||||
// END_PLATFORM
|
||||
}
|
||||
|
||||
type StackClientAppImplConstructorOptionsResolved<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & { inheritsFrom?: undefined };
|
||||
|
||||
export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, ProjectId extends string = string> implements StackClientApp<HasTokenStore, ProjectId> {
|
||||
@ -608,6 +630,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
this._tokenStoreInit = resolvedOptions.tokenStore;
|
||||
this._redirectMethod = resolvedOptions.redirectMethod || (isBrowserLike() ? "window" : "none");
|
||||
this._redirectMethod = resolvedOptions.redirectMethod || "nextjs"; // THIS_LINE_PLATFORM next
|
||||
this._redirectMethod = resolvedOptions.redirectMethod || "tanstack-start"; // THIS_LINE_PLATFORM tanstack-start
|
||||
this._urlOptions = resolvedOptions.urls ?? {};
|
||||
this._oauthScopesOnSignIn = resolvedOptions.oauthScopesOnSignIn ?? {};
|
||||
if (isBrowserLike() && (resolvedOptions.tokenStore === "cookie" || resolvedOptions.tokenStore === "nextjs-cookie")) {
|
||||
@ -888,12 +911,9 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
let hostname;
|
||||
if (isBrowserLike()) {
|
||||
hostname = window.location.hostname;
|
||||
} else {
|
||||
hostname = await getServerRequestHost();
|
||||
}
|
||||
// IF_PLATFORM next
|
||||
else {
|
||||
hostname = (await sc.headers?.())?.get("host");
|
||||
}
|
||||
// END_PLATFORM
|
||||
if (!hostname) {
|
||||
console.warn("No hostname found when queueing custom refresh cookie update");
|
||||
return;
|
||||
@ -997,6 +1017,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": {
|
||||
@ -1105,6 +1130,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(use(createCookieHelper()), overrideTokenStoreInit);
|
||||
}
|
||||
// END_PLATFORM
|
||||
suspendIfSsr();
|
||||
const cookieHelper = createBrowserCookieHelper();
|
||||
const tokenStore = this._getOrCreateTokenStore(cookieHelper, overrideTokenStoreInit);
|
||||
@ -2520,6 +2550,10 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
} else if (isReactServer && this._redirectMethod === "nextjs") {
|
||||
NextNavigation.redirect(options.url.toString(), options.replace ? NextNavigation.RedirectType.replace : NextNavigation.RedirectType.push);
|
||||
// END_PLATFORM
|
||||
// IF_PLATFORM tanstack-start
|
||||
} else if (this._redirectMethod === "tanstack-start" && !isBrowserLike()) {
|
||||
throw TanStackRouter.redirect({ href: options.url.toString(), replace: options.replace });
|
||||
// END_PLATFORM
|
||||
} else if (typeof this._redirectMethod === "object" && this._redirectMethod.navigate) {
|
||||
this._redirectMethod.navigate(options.url.toString());
|
||||
} else {
|
||||
@ -2544,6 +2578,10 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
const router = NextNavigation.useRouter();
|
||||
return (to: string) => router.push(to);
|
||||
// END_PLATFORM
|
||||
// IF_PLATFORM tanstack-start
|
||||
} else if (this._redirectMethod === "tanstack-start") {
|
||||
return (to: string) => window.location.assign(to);
|
||||
// END_PLATFORM
|
||||
} else {
|
||||
return (to: string) => { };
|
||||
}
|
||||
@ -2589,6 +2627,20 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
await this._redirectIfTrusted(plan.url, options);
|
||||
}
|
||||
|
||||
protected _redirectToHandlerDuringRender(handlerName: keyof HandlerUrls, options?: RedirectToOptions): boolean {
|
||||
// IF_PLATFORM tanstack-start
|
||||
if (this._redirectMethod === "tanstack-start" && !isBrowserLike()) {
|
||||
const rawUrls = getUrls(this._urlOptions, { projectId: this.projectId });
|
||||
const rawHandlerUrl = rawUrls[handlerName];
|
||||
if (!rawHandlerUrl) {
|
||||
throw new Error(`No URL for handler name ${handlerName}`);
|
||||
}
|
||||
throw TanStackRouter.redirect({ href: rawHandlerUrl, replace: options?.replace });
|
||||
}
|
||||
// END_PLATFORM
|
||||
return false;
|
||||
}
|
||||
|
||||
async redirectToSignIn(options?: RedirectToOptions) { return await this._redirectToHandler("signIn", options); }
|
||||
async redirectToSignUp(options?: RedirectToOptions) { return await this._redirectToHandler("signUp", options); }
|
||||
async redirectToSignOut(options?: RedirectToOptions) { return await this._redirectToHandler("signOut", options); }
|
||||
@ -2742,9 +2794,13 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
switch (options?.or) {
|
||||
case 'redirect': {
|
||||
if (!crud?.is_anonymous && crud?.is_restricted) {
|
||||
runAsynchronously(this.redirectToOnboarding({ replace: true }));
|
||||
if (!this._redirectToHandlerDuringRender("onboarding", { replace: true })) {
|
||||
runAsynchronously(this.redirectToOnboarding({ replace: true }));
|
||||
}
|
||||
} else {
|
||||
runAsynchronously(this.redirectToSignIn({ replace: true }));
|
||||
if (!this._redirectToHandlerDuringRender("signIn", { replace: true })) {
|
||||
runAsynchronously(this.redirectToSignIn({ replace: true }));
|
||||
}
|
||||
}
|
||||
suspend();
|
||||
throw new StackAssertionError("suspend should never return");
|
||||
|
||||
@ -184,7 +184,17 @@ export function createEmptyTokenStore() {
|
||||
const cachePromiseByHookId = new Map<string, ReactPromise<Result<unknown>>>();
|
||||
export function useAsyncCache<D extends any[], T>(cache: AsyncCache<D, Result<T>>, dependencies: D, caller: string): T {
|
||||
// we explicitly don't want to run this hook in SSR
|
||||
// IF_PLATFORM tanstack-start
|
||||
if (!isBrowserLike()) {
|
||||
const result = use(cache.getOrWait(dependencies, "read-write"));
|
||||
if (result.status === "error") {
|
||||
throw result.error;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
// ELSE_PLATFORM
|
||||
suspendIfSsr(caller);
|
||||
// END_PLATFORM
|
||||
|
||||
// on the dashboard, we do some perf monitoring for pre-fetching which should hook right in here
|
||||
const asyncCacheHooks: any[] = getGlobal("use-async-cache-execution-hooks") ?? [];
|
||||
@ -220,7 +230,7 @@ export function useAsyncCache<D extends any[], T>(cache: AsyncCache<D, Result<T>
|
||||
const promise = React.useSyncExternalStore(
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
() => throwErr(new Error("getServerSnapshot should never be called in useAsyncCache because we restrict to CSR earlier"))
|
||||
getSnapshot,
|
||||
);
|
||||
|
||||
const result = use(promise);
|
||||
|
||||
@ -30,6 +30,7 @@ export type EmailConfig = {
|
||||
|
||||
export type RedirectMethod = "window"
|
||||
| "nextjs" // THIS_LINE_PLATFORM next
|
||||
| "tanstack-start" // THIS_LINE_PLATFORM tanstack-start
|
||||
| "none"
|
||||
| {
|
||||
useNavigate: () => (to: string) => void,
|
||||
|
||||
@ -31,6 +31,35 @@ function NextStackProvider({
|
||||
</StackProviderClient>
|
||||
);
|
||||
}
|
||||
// ELSE_IF_PLATFORM tanstack-start
|
||||
function TanStackStartStackProvider({
|
||||
children,
|
||||
app,
|
||||
lang,
|
||||
translationOverrides,
|
||||
}: {
|
||||
lang?: React.ComponentProps<typeof TranslationProvider>['lang'],
|
||||
/**
|
||||
* A mapping of English translations to translated equivalents.
|
||||
*
|
||||
* These will take priority over the translations from the language specified in the `lang` property. Note that the
|
||||
* keys are case-sensitive.
|
||||
*/
|
||||
translationOverrides?: Record<string, string>,
|
||||
children: React.ReactNode,
|
||||
// list all three types of apps even though server and admin are subclasses of client so it's clear that you can pass any
|
||||
app: StackClientApp<true>,
|
||||
}) {
|
||||
return (
|
||||
<StackProviderClient app={app} serialized={false}>
|
||||
<TranslationProvider lang={lang} translationOverrides={translationOverrides}>
|
||||
<Suspense fallback={null}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</TranslationProvider>
|
||||
</StackProviderClient>
|
||||
);
|
||||
}
|
||||
// ELSE_PLATFORM
|
||||
function ReactStackProvider({
|
||||
children,
|
||||
@ -63,6 +92,8 @@ function ReactStackProvider({
|
||||
|
||||
// IF_PLATFORM next
|
||||
export default NextStackProvider;
|
||||
/* ELSE_PLATFORM
|
||||
/* ELSE_IF_PLATFORM tanstack-start
|
||||
export default TanStackStartStackProvider;
|
||||
ELSE_PLATFORM
|
||||
export default ReactStackProvider;
|
||||
END_PLATFORM */
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import * as browserContext from "./tanstack-start-server-context.default";
|
||||
import * as serverContext from "./tanstack-start-server-context.server";
|
||||
|
||||
export declare const getCookie: typeof serverContext.getCookie | typeof browserContext.getCookie;
|
||||
export declare const getCookies: typeof serverContext.getCookies | typeof browserContext.getCookies;
|
||||
export declare const setCookie: typeof serverContext.setCookie | typeof browserContext.setCookie;
|
||||
export declare const deleteCookie: typeof serverContext.deleteCookie | typeof browserContext.deleteCookie;
|
||||
export declare const getRequestHeader: typeof serverContext.getRequestHeader | typeof browserContext.getRequestHeader;
|
||||
9
packages/template/src/tanstack-start-server-context.d.ts
vendored
Normal file
9
packages/template/src/tanstack-start-server-context.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare module "@stackframe/tanstack-start/tanstack-start-server-context" {
|
||||
type TanStackStartServerContext = typeof import("@tanstack/react-start/server");
|
||||
|
||||
export const deleteCookie: TanStackStartServerContext["deleteCookie"] | undefined;
|
||||
export const getCookie: TanStackStartServerContext["getCookie"] | undefined;
|
||||
export const getCookies: TanStackStartServerContext["getCookies"] | undefined;
|
||||
export const getRequestHeader: TanStackStartServerContext["getRequestHeader"] | undefined;
|
||||
export const setCookie: TanStackStartServerContext["setCookie"] | undefined;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export const getCookie = undefined;
|
||||
export const getCookies = undefined;
|
||||
export const setCookie = undefined;
|
||||
export const deleteCookie = undefined;
|
||||
export const getRequestHeader = undefined;
|
||||
@ -0,0 +1,7 @@
|
||||
export {
|
||||
deleteCookie,
|
||||
getCookie,
|
||||
getCookies,
|
||||
getRequestHeader,
|
||||
setCookie,
|
||||
} from "@tanstack/react-start/server";
|
||||
@ -3,6 +3,8 @@ import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import sharedConfig from '../../vitest.shared'
|
||||
|
||||
const tanstackStartServerContextStub = fileURLToPath(new URL('./src/tanstack-start-server-context.default.ts', import.meta.url)) // THIS_LINE_PLATFORM template
|
||||
|
||||
const SOURCE_FILE_PATTERN = /\.(jsx?|tsx?)$/;
|
||||
const CLIENT_VERSION_SENTINEL = "STACK_COMPILE_TIME_CLIENT_PACKAGE_VERSION_SENTINEL";
|
||||
const ENFORCE_PRE: "pre" = "pre";
|
||||
@ -45,6 +47,11 @@ const replaceCompileTimeClientVersion = () => {
|
||||
export default mergeConfig(
|
||||
sharedConfig,
|
||||
defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@stackframe/tanstack-start/tanstack-start-server-context": tanstackStartServerContextStub, // THIS_LINE_PLATFORM template
|
||||
},
|
||||
},
|
||||
plugins: [replaceCompileTimeClientVersion()],
|
||||
}),
|
||||
)
|
||||
|
||||
@ -54,7 +54,7 @@ function generateFromTemplate(options: {
|
||||
// If the resulting file is package.json, add a comment field to the JSON.
|
||||
if (path.basename(relativePath) === "package.json") {
|
||||
const jsonObj = JSON.parse(newContent);
|
||||
newContent = JSON.stringify({ "//": COMMENT_LINE, ...jsonObj }, null, 2);
|
||||
newContent = JSON.stringify({ "//": COMMENT_LINE, ...jsonObj }, null, 2) + "\n";
|
||||
}
|
||||
|
||||
return newContent;
|
||||
@ -109,7 +109,7 @@ function processPackageJson(path: string, content: string) {
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse package.json at ${path}`, { cause: error });
|
||||
}
|
||||
return JSON.stringify({ "//": `${COMMENT_LINE} (FOR package.json FILES, PLEASE EDIT package-template.json)`, ...jsonObj }, null, 2);
|
||||
return JSON.stringify({ "//": `${COMMENT_LINE} (FOR package.json FILES, PLEASE EDIT package-template.json)`, ...jsonObj }, null, 2) + "\n";
|
||||
}
|
||||
|
||||
function baseEditFn(options: {
|
||||
@ -131,6 +131,14 @@ function baseEditFn(options: {
|
||||
withGeneratorLock(async () => {
|
||||
const baseDir = path.resolve(__dirname, "..", "packages");
|
||||
const srcDir = path.resolve(baseDir, "template");
|
||||
const tanstackStartOnlyTemplateFiles = new Set([
|
||||
"src/tanstack-start-server-context.combined.ts",
|
||||
"src/tanstack-start-server-context.default.ts",
|
||||
"src/tanstack-start-server-context.server.ts",
|
||||
]);
|
||||
const templateOnlyFiles = new Set([
|
||||
"src/tanstack-start-server-context.d.ts",
|
||||
]);
|
||||
|
||||
// Copy package-template.json to package.json in the template,
|
||||
// applying macros and adding a comment field.
|
||||
@ -168,7 +176,9 @@ withGeneratorLock(async () => {
|
||||
"src/global.d.ts",
|
||||
];
|
||||
|
||||
if (ignores.some((ignorePath) => relativePath.startsWith(ignorePath)) || relativePath.endsWith(".tsx")) {
|
||||
if (tanstackStartOnlyTemplateFiles.has(relativePath) || templateOnlyFiles.has(relativePath)) {
|
||||
return false;
|
||||
} else if (ignores.some((ignorePath) => relativePath.startsWith(ignorePath)) || relativePath.endsWith(".tsx")) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@ -182,6 +192,7 @@ withGeneratorLock(async () => {
|
||||
editFn: (relativePath, content) => {
|
||||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["next"] });
|
||||
},
|
||||
filterFn: (relativePath) => !tanstackStartOnlyTemplateFiles.has(relativePath),
|
||||
});
|
||||
|
||||
generateFromTemplate({
|
||||
@ -190,6 +201,16 @@ withGeneratorLock(async () => {
|
||||
editFn: (relativePath, content) => {
|
||||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["react"] });
|
||||
},
|
||||
filterFn: (relativePath) => !tanstackStartOnlyTemplateFiles.has(relativePath),
|
||||
});
|
||||
|
||||
generateFromTemplate({
|
||||
src: srcDir,
|
||||
dest: path.resolve(baseDir, "tanstack-start"),
|
||||
editFn: (relativePath, content) => {
|
||||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["tanstack-start"] });
|
||||
},
|
||||
filterFn: (relativePath) => !templateOnlyFiles.has(relativePath),
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
@ -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'],
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"STACK_*",
|
||||
"CRON_SECRET",
|
||||
"NEXT_PUBLIC_*",
|
||||
"VITE_*",
|
||||
"NEXT_PUBLIC_SENTRY_*",
|
||||
"SENTRY_*",
|
||||
"VERCEL_GIT_COMMIT_SHA",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user