create-t3-supabase-zustand
Version:
Create web application with the t3 stack, Supabase, and Zustand
756 lines • 52.3 kB
JavaScript
#!/usr/bin/env node
import ct from"path";import{execa as Wt}from"execa";import pt from"fs-extra";import*as f from"@clack/prompts";import ce from"chalk";import{Command as Ct}from"commander";import be from"path";import{fileURLToPath as ft}from"url";var gt=ft(import.meta.url),ht=be.dirname(gt),p=be.join(ht,"../"),ye=` ___ ___ ___ __ _____ ___ _____ ____ __ ___ ___
/ __| _ \\ __| / \\_ _| __| |_ _|__ / / \\ | _ \\ _ \\
| (__| / _| / /\\ \\| | | _| | | |_ \\ / /\\ \\| _/ _/
\\___|_|_\\___|_/\u203E\u203E\\_\\_| |___| |_| |___/ /_/\u203E\u203E\\_\\_| |_|
`,V="my-t3-app",ne="create-t3-app";import bt from"node:crypto";import W from"path";import ie from"fs-extra";var xe=({projectDir:e,packages:s,databaseProvider:r,scopedAppName:t})=>{let n=s?.nextAuth.inUse,i=s?.prisma.inUse,o=s?.drizzle.inUse,a=i===!0||o===!0,c=r==="planetscale",m=yt(!!n,!!i,!!o,r,t),u="";if(a?c?n?u="with-auth-db-planetscale.js":u="with-db-planetscale.js":n?u="with-auth-db.js":u="with-db.js":n&&(u="with-auth.js"),u!==""){let F=W.join(p,"template/extras/src/env",u),ee=W.join(e,"src/env.js");ie.copyFileSync(F,ee)}let x=W.join(e,".env"),h=W.join(e,".env.example"),v=xt+m,k=Buffer.from(bt.getRandomValues(new Uint8Array(32))).toString("base64"),L=m.replace('AUTH_SECRET=""',`AUTH_SECRET="${k}" # Generated by create-t3-app.`);ie.writeFileSync(x,L,"utf-8"),ie.writeFileSync(h,v,"utf-8")},yt=(e,s,r,t,n)=>{let i=`
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
`.trim().concat(`
`);return e&&(i+=`
# Next Auth
# You can generate a new secret on the command line with:
# npx auth secret
# https://next-auth.js.org/configuration/options#secret
AUTH_SECRET=""
# Next Auth Discord Provider
AUTH_DISCORD_ID=""
AUTH_DISCORD_SECRET=""
`),s&&(i+=`
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
`),r&&(i+=`
# Drizzle
`),(s||r)&&(t==="planetscale"?r?i+=`# Get the Database URL from the "prisma" dropdown selector in PlanetScale.
# Change the query params at the end of the URL to "?ssl={"rejectUnauthorized":true}"
DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}'`:i=`# Get the Database URL from the "prisma" dropdown selector in PlanetScale.
DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?sslaccept=strict'`:t==="mysql"?i+=`DATABASE_URL="mysql://root:password@localhost:3306/${n}"`:t==="postgres"?i+=`DATABASE_URL="postgresql://postgres:password@localhost:5432/${n}"`:t==="sqlite"&&(i+='DATABASE_URL="file:./db.sqlite"'),i+=`
`),!e&&!s&&(i+=`
# Example:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"
`),i},xt=`
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
`.trim().concat(`
`);import C from"path";import re from"fs-extra";import ve from"path";import Se from"fs-extra";import wt from"sort-package-json";var we={"@supabase/supabase-js":"^2.39.0","@supabase/ssr":"^0.1.0",zustand:"^4.4.7",immer:"^10.0.3","next-auth":"5.0.0-beta.25","@auth/prisma-adapter":"^2.7.2","@auth/drizzle-adapter":"^1.7.2",prisma:"^6.5.0","@prisma/client":"^6.5.0","@prisma/adapter-planetscale":"^6.5.0","drizzle-kit":"^0.30.5","drizzle-orm":"^0.41.0",mysql2:"^3.11.0","@planetscale/database":"^1.19.0",postgres:"^3.4.4","@libsql/client":"^0.14.0",tailwindcss:"^4.0.15",postcss:"^8.5.3","@tailwindcss/postcss":"^4.0.15","@trpc/client":"^11.0.0","@trpc/server":"^11.0.0","@trpc/react-query":"^11.0.0","@trpc/next":"^11.0.0","@tanstack/react-query":"^5.69.0",superjson:"^2.2.1","server-only":"^0.0.1","@biomejs/biome":"1.9.4",prettier:"^3.5.3","@eslint/eslintrc":"^3.3.1","prettier-plugin-tailwindcss":"^0.6.11",eslint:"^9.23.0","eslint-config-next":"^15.2.3","eslint-plugin-drizzle":"^0.2.3","typescript-eslint":"^8.27.0"};var d=e=>{let{dependencies:s,devMode:r,projectDir:t}=e,n=Se.readJSONSync(ve.join(t,"package.json"));s.forEach(o=>{let a=we[o];r&&n.devDependencies?n.devDependencies[o]=a:n.dependencies&&(n.dependencies[o]=a)});let i=wt(n);Se.writeJSONSync(ve.join(t,"package.json"),i,{spaces:2})};var Pe=({projectDir:e,packages:s})=>{let r=s?.prisma.inUse,t=s?.drizzle.inUse,n=["next-auth"];r&&n.push("@auth/prisma-adapter"),t&&n.push("@auth/drizzle-adapter"),d({projectDir:e,dependencies:n,devMode:!1});let i=C.join(p,"template/extras"),o="src/app/api/auth/[...nextauth]/route.ts",a=C.join(i,o),c=C.join(e,o),m=C.join(i,"src/server/auth/config",r?"with-prisma.ts":t?"with-drizzle.ts":"base.ts"),u=C.join(e,"src/server/auth/config.ts"),x=C.join(i,"src/server/auth/index.ts"),h=C.join(e,"src/server/auth/index.ts");re.copySync(a,c),re.copySync(m,u),re.copySync(x,h)};import j from"path";import J from"fs-extra";import vt from"path";import _e from"fs-extra";import St from"sort-package-json";var A=e=>{let{scripts:s,projectDir:r}=e,t=vt.join(r,"package.json"),n=_e.readJSONSync(t);n.scripts={...n.scripts,...s};let i=St(n);_e.writeJSONSync(t,i,{spaces:2})};var ke=({projectDir:e,packages:s,databaseProvider:r})=>{d({projectDir:e,dependencies:["prisma"],devMode:!0}),d({projectDir:e,dependencies:["@prisma/client"],devMode:!1}),r==="planetscale"&&d({projectDir:e,dependencies:["@prisma/adapter-planetscale","@planetscale/database"],devMode:!1});let t=j.join(p,"template/extras"),n=j.join(t,"prisma/schema",`${s?.nextAuth.inUse?"with-auth":"base"}${r==="planetscale"?"-planetscale":""}.prisma`),i=J.readFileSync(n,"utf-8");r!=="sqlite"&&(i=i.replace('provider = "sqlite"',`provider = "${{mysql:"mysql",postgres:"postgresql",planetscale:"mysql"}[r]}"`),["mysql","planetscale"].includes(r)&&(i=i.replace("// @db.Text","@db.Text")));let o=j.join(e,"prisma/schema.prisma");J.mkdirSync(j.dirname(o),{recursive:!0}),J.writeFileSync(o,i);let a=j.join(t,r==="planetscale"?"src/server/db/db-prisma-planetscale.ts":"src/server/db/db-prisma.ts"),c=j.join(e,"src/server/db.ts");A({projectDir:e,scripts:{postinstall:"prisma generate","db:push":"prisma db push","db:studio":"prisma studio","db:generate":"prisma migrate dev","db:migrate":"prisma migrate deploy"}}),J.copySync(a,c)};import y from"path";import b from"fs-extra";var Ae=({projectDir:e})=>{d({projectDir:e,dependencies:["@supabase/supabase-js","@supabase/ssr"],devMode:!1});let s=y.join(e,"src/lib"),r=y.join(s,"supabase");b.mkdirSync(r,{recursive:!0});let t=`
import { createBrowserClient } from '@supabase/ssr';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export const createClient = () => {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
};
export const createServerSupabaseClient = () => {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: Record<string, string>) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options: Record<string, string>) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
};
`;b.writeFileSync(y.join(r,"client.ts"),t);let n=`
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[];
export type Database = {
public: {
Tables: {
profiles: {
Row: {
id: string;
created_at: string;
updated_at: string;
username: string | null;
avatar_url: string | null;
};
Insert: {
id: string;
created_at?: string;
updated_at?: string;
username?: string | null;
avatar_url?: string | null;
};
Update: {
id?: string;
created_at?: string;
updated_at?: string;
username?: string | null;
avatar_url?: string | null;
};
};
};
Views: {
[_ in never]: never;
};
Functions: {
[_ in never]: never;
};
Enums: {
[_ in never]: never;
};
};
};
export type Tables<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Row'];
export type Insertable<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Insert'];
export type Updatable<T extends keyof Database['public']['Tables']> = Database['public']['Tables'][T]['Update'];
`;b.writeFileSync(y.join(r,"types.ts"),n);let i=`
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createServerClient } from '@supabase/ssr';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: Record<string, string>) {
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: Record<string, string>) {
request.cookies.set({
name,
value: '',
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: '',
...options,
});
},
},
}
);
const { data } = await supabase.auth.getSession();
// Protect routes that require authentication
const protectedRoutes = ['/dashboard', '/profile'];
const isProtectedRoute = protectedRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
);
if (isProtectedRoute && !data.session) {
return NextResponse.redirect(new URL('/auth/signin', request.url));
}
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public (public files)
*/
'/((?!_next/static|_next/image|favicon.ico|public).*)',
],
};
`;b.writeFileSync(y.join(e,"src/middleware.ts"),i);let o=y.join(e,"src/components/auth");b.mkdirSync(o,{recursive:!0});let a=`
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
export function AuthForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSignUp, setIsSignUp] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const supabase = createClient();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
if (isSignUp) {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: \`\${window.location.origin}/auth/callback\`,
},
});
if (error) throw error;
alert('Check your email for the confirmation link!');
} else {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
router.push('/dashboard');
router.refresh();
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
const handleOAuthSignIn = async (provider: 'github' | 'google') => {
setLoading(true);
setError(null);
try {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: \`\${window.location.origin}/auth/callback\`,
},
});
if (error) throw error;
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setLoading(false);
}
};
return (
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-2xl font-bold">{isSignUp ? 'Create an account' : 'Sign in to your account'}</h1>
</div>
{error && (
<div className="p-3 text-sm text-red-500 bg-red-100 rounded-md">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<button
type="submit"
disabled={loading}
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{loading ? 'Loading...' : isSignUp ? 'Sign up' : 'Sign in'}
</button>
</div>
</form>
<div className="flex items-center justify-center mt-6">
<div className="text-sm">
<button
onClick={() => setIsSignUp(!isSignUp)}
className="font-medium text-indigo-600 hover:text-indigo-500"
>
{isSignUp ? 'Already have an account? Sign in' : 'Need an account? Sign up'}
</button>
</div>
</div>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 text-gray-500 bg-white">Or continue with</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3 mt-6">
<button
onClick={() => handleOAuthSignIn('github')}
disabled={loading}
className="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50"
>
GitHub
</button>
<button
onClick={() => handleOAuthSignIn('google')}
disabled={loading}
className="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50"
>
Google
</button>
</div>
</div>
</div>
);
}
`;b.writeFileSync(y.join(o,"auth-form.tsx"),a);let c=y.join(e,"src/app/auth/callback");b.mkdirSync(c,{recursive:!0});let m=`
import { redirect } from 'next/navigation';
import { createServerSupabaseClient } from '@/lib/supabase/client';
export default async function AuthCallbackPage() {
const supabase = createServerSupabaseClient();
const { error } = await supabase.auth.exchangeCodeForSession(
new URL(request.url).searchParams.get('code') ?? ''
);
if (error) {
console.error('Error exchanging code for session:', error);
return redirect('/auth/signin?error=Could not authenticate user');
}
return redirect('/dashboard');
}
`;b.writeFileSync(y.join(c,"page.tsx"),m);let u=y.join(e,"src/app/auth/signin");b.mkdirSync(u,{recursive:!0});let x=`
import { AuthForm } from '@/components/auth/auth-form';
export default function SignInPage() {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<AuthForm />
</div>
);
}
`;b.writeFileSync(y.join(u,"page.tsx"),x);let h=`
# Supabase
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key
`,v=y.join(e,".env.example");b.existsSync(v)?b.appendFileSync(v,h):b.writeFileSync(v,h);let k=y.join(e,"supabase/migrations");b.mkdirSync(k,{recursive:!0});let L=`
-- Create profiles table
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
username TEXT UNIQUE,
avatar_url TEXT
);
-- Enable Row Level Security
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Create policies
CREATE POLICY "Public profiles are viewable by everyone"
ON public.profiles FOR SELECT USING (true);
CREATE POLICY "Users can update their own profile"
ON public.profiles FOR UPDATE USING (auth.uid() = id);
-- Create trigger to create profile when user signs up
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username)
VALUES (new.id, new.email);
RETURN new;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
`;b.writeFileSync(y.join(k,"00000000000000_initial_schema.sql"),L);let F=`
# Supabase Setup Instructions
1. Create a new Supabase project at https://supabase.com
2. Get your project URL and API keys from the Supabase dashboard
3. Update the .env file with your Supabase credentials:
- NEXT_PUBLIC_SUPABASE_URL
- NEXT_PUBLIC_SUPABASE_ANON_KEY
- SUPABASE_SERVICE_ROLE_KEY
4. Run the SQL migrations in the supabase/migrations directory
5. Configure authentication providers in the Supabase dashboard
For more information, see the [Supabase documentation](https://supabase.com/docs).
`;b.writeFileSync(y.join(e,"SUPABASE_SETUP.md"),F)};import D from"path";import Ee from"fs-extra";var Ie=({projectDir:e})=>{d({projectDir:e,dependencies:["tailwindcss","postcss","@tailwindcss/postcss"],devMode:!0});let s=D.join(p,"template/extras"),r=D.join(s,"config/postcss.config.js"),t=D.join(e,"postcss.config.js"),n=D.join(s,"src/styles/globals.css"),i=D.join(e,"src/styles/globals.css");Ee.copySync(r,t),Ee.copySync(n,i)};import g from"path";import Pt from"fs-extra";var Ce=({projectDir:e,packages:s,appRouter:r})=>{d({projectDir:e,dependencies:["@tanstack/react-query","superjson","@trpc/server","@trpc/client","@trpc/react-query"],devMode:!1});let t=s?.nextAuth.inUse,n=s?.prisma.inUse,i=s?.drizzle.inUse,o=n===!0||i===!0,a=g.join(p,"template/extras"),u=r?"src/app/api/trpc/[trpc]/route.ts":"src/pages/api/trpc/[trpc].ts",x=g.join(a,u),h=g.join(e,u),v=t&&o?"with-auth-db.ts":t?"with-auth.ts":o?"with-db.ts":"base.ts",k=g.join(a,"src/server/api",r?"trpc-app":"trpc-pages",v),L=g.join(e,"src/server/api/trpc.ts"),F=g.join(a,"src/server/api/root.ts"),ee=g.join(e,"src/server/api/root.ts"),ut=t&&n?"with-auth-prisma.ts":t&&i?"with-auth-drizzle.ts":t?"with-auth.ts":n?"with-prisma.ts":i?"with-drizzle.ts":"base.ts",dt=g.join(a,"src/server/api/routers/post",ut),mt=g.join(e,"src/server/api/routers/post.ts"),te=[[x,h],[k,L],[F,ee],[dt,mt]];if(r){d({dependencies:["server-only"],devMode:!1,projectDir:e});let N=g.join(a,"src/trpc");te.push([g.join(N,"server.ts"),g.join(e,"src/trpc/server.ts")],[g.join(N,"react.tsx"),g.join(e,"src/trpc/react.tsx")],[g.join(a,"src/app/_components",s?.tailwind.inUse?"post-tw.tsx":"post.tsx"),g.join(e,"src/app/_components/post.tsx")],[g.join(a,"src/trpc/query-client.ts"),g.join(e,"src/trpc/query-client.ts")])}else{d({dependencies:["@trpc/next"],devMode:!1,projectDir:e});let N=g.join(a,"src/utils/api.ts"),se=g.join(e,"src/utils/api.ts");te.push([N,se])}te.forEach(([N,se])=>{Pt.copySync(N,se)})};import P from"path";import _ from"fs-extra";var Te=({projectDir:e})=>{d({projectDir:e,dependencies:["zustand","immer"],devMode:!1});let s=P.join(e,"src/store");_.mkdirSync(s,{recursive:!0});let r=`
import { create } from 'zustand';
import { createClient } from '@/lib/supabase/client';
export type User = {
id: string;
email?: string;
username?: string;
avatarUrl?: string;
};
type AuthState = {
user: User | null;
isLoading: boolean;
error: string | null;
initialized: boolean;
// Actions
setUser: (user: User | null) => void;
signOut: () => Promise<void>;
checkSession: () => Promise<void>;
};
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
isLoading: true,
error: null,
initialized: false,
setUser: (user) => set({ user }),
signOut: async () => {
try {
set({ isLoading: true, error: null });
const supabase = createClient();
await supabase.auth.signOut();
set({ user: null, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'An error occurred during sign out',
isLoading: false
});
}
},
checkSession: async () => {
try {
set({ isLoading: true, error: null });
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (user) {
set({
user: {
id: user.id,
email: user.email,
},
isLoading: false,
initialized: true
});
} else {
set({ user: null, isLoading: false, initialized: true });
}
} catch (error) {
set({
error: error instanceof Error ? error.message : 'An error occurred checking session',
isLoading: false,
initialized: true
});
}
},
}));
`;_.writeFileSync(P.join(s,"auth-store.ts"),r);let t=`
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
type AppState = {
theme: 'light' | 'dark';
sidebarOpen: boolean;
// Actions
setTheme: (theme: 'light' | 'dark') => void;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
};
export const useAppStore = create<AppState>()(
immer((set) => ({
theme: 'light',
sidebarOpen: false,
setTheme: (theme) => set((state) => {
state.theme = theme;
}),
toggleSidebar: () => set((state) => {
state.sidebarOpen = !state.sidebarOpen;
}),
setSidebarOpen: (open) => set((state) => {
state.sidebarOpen = open;
}),
}))
);
`;_.writeFileSync(P.join(s,"app-store.ts"),t);let n=P.join(e,"src/providers");_.mkdirSync(n,{recursive:!0});let i=`
'use client';
import { useEffect } from 'react';
import { useAuthStore } from '@/store/auth-store';
import { createClient } from '@/lib/supabase/client';
export function StoreProvider({ children }: { children: React.ReactNode }) {
const { setUser, checkSession } = useAuthStore();
// Initialize auth state
useEffect(() => {
checkSession();
// Set up auth state change listener
const supabase = createClient();
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
if (session?.user) {
setUser({
id: session.user.id,
email: session.user.email,
username: session.user.user_metadata.username as string,
avatarUrl: session.user.user_metadata.avatar_url as string,
});
} else {
setUser(null);
}
}
);
return () => {
subscription.unsubscribe();
};
}, [checkSession, setUser]);
return <>{children}</>;
}
`;_.writeFileSync(P.join(n,"store-provider.tsx"),i);let o=P.join(e,"src/hooks");_.mkdirSync(o,{recursive:!0});let a=`
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/auth-store';
export function useProtectedRoute() {
const router = useRouter();
const { user, isLoading, initialized } = useAuthStore();
useEffect(() => {
if (initialized && !isLoading && !user) {
router.push('/auth/signin');
}
}, [user, isLoading, initialized, router]);
return { user, isLoading };
}
`;_.writeFileSync(P.join(o,"use-protected-route.ts"),a);let c=P.join(e,"src/app/dashboard");_.mkdirSync(c,{recursive:!0});let m=`
'use client';
import { useProtectedRoute } from '@/hooks/use-protected-route';
import { useAuthStore } from '@/store/auth-store';
import { useAppStore } from '@/store/app-store';
export default function DashboardPage() {
useProtectedRoute();
const { user, signOut } = useAuthStore();
const { theme, setTheme } = useAppStore();
if (!user) {
return <div className="flex items-center justify-center min-h-screen">Loading...</div>;
}
return (
<div className="p-8">
<h1 className="mb-6 text-3xl font-bold">Dashboard</h1>
<div className="p-6 mb-6 bg-white rounded-lg shadow-md">
<h2 className="mb-4 text-xl font-semibold">User Profile</h2>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>ID:</strong> {user.id}</p>
</div>
<div className="p-6 mb-6 bg-white rounded-lg shadow-md">
<h2 className="mb-4 text-xl font-semibold">Theme Settings</h2>
<div className="flex items-center space-x-4">
<span>Current theme: {theme}</span>
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600"
>
Toggle Theme
</button>
</div>
</div>
<button
onClick={() => signOut()}
className="px-4 py-2 text-white bg-red-500 rounded hover:bg-red-600"
>
Sign Out
</button>
</div>
);
}
`;_.writeFileSync(P.join(c,"page.tsx"),m);let u=`
import { StoreProvider } from '@/providers/store-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<StoreProvider>
{children}
</StoreProvider>
);
}
`;_.writeFileSync(P.join(e,"src/app/layout-with-store.tsx"),u);let x=`
# Zustand State Management
This project uses Zustand for state management. Zustand is a small, fast and scalable state-management solution.
## Store Structure
- \`auth-store.ts\`: Manages authentication state and integrates with Supabase Auth
- \`app-store.ts\`: Manages application-wide state like theme, sidebar, etc.
## Usage Examples
### Accessing Auth State
\`\`\`tsx
import { useAuthStore } from '@/store/auth-store';
function MyComponent() {
const { user, signOut } = useAuthStore();
if (!user) return <div>Not logged in</div>;
return (
<div>
<p>Welcome, {user.email}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}
\`\`\`
### Using App State with Immer
\`\`\`tsx
import { useAppStore } from '@/store/app-store';
function ThemeToggle() {
const { theme, setTheme } = useAppStore();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme (Current: {theme})
</button>
);
}
\`\`\`
## Protected Routes
Use the \`useProtectedRoute\` hook to protect routes that require authentication:
\`\`\`tsx
import { useProtectedRoute } from '@/hooks/use-protected-route';
export default function ProtectedPage() {
const { user, isLoading } = useProtectedRoute();
if (isLoading) return <div>Loading...</div>;
return <div>Protected content for {user?.email}</div>;
}
\`\`\`
For more information, see the [Zustand documentation](https://github.com/pmndrs/zustand).
`;_.writeFileSync(P.join(e,"ZUSTAND_USAGE.md"),x)};import oe from"path";import _t from"fs-extra";var Re=({projectDir:e})=>{d({projectDir:e,dependencies:["@biomejs/biome"],devMode:!0});let s=oe.join(p,"template/extras"),r=oe.join(s,"config/biome.jsonc"),t=oe.join(e,"biome.jsonc");_t.copySync(r,t),A({projectDir:e,scripts:{"check:unsafe":"biome check --write --unsafe .","check:write":"biome check --write .",check:"biome check ."}})};import ae from"fs";import ze from"path";import Ue from"path";var H=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e);var Y=e=>{let r=H(e).split("/"),t=r[r.length-1];if(t==="."){let o=Ue.resolve(process.cwd());t=Ue.basename(o)}let n=r.findIndex(o=>o.startsWith("@"));r.findIndex(o=>o.startsWith("@"))!==-1&&(t=r.slice(n).join("/"));let i=r.filter(o=>!o.startsWith("@")).join("/");return[t,i]};var kt=e=>e.replace(/[^a-zA-Z0-9_.-]/g,"_").toLowerCase(),Ne=({projectDir:e,databaseProvider:s,projectName:r})=>{let t=ze.join(p,`template/extras/start-database/${s}.sh`),n=ae.readFileSync(t,"utf-8"),i=ze.join(e,"start-database.sh"),[o]=r==="."?Y(e):[r],a=kt(o);ae.writeFileSync(i,n.replaceAll("project1",a)),ae.chmodSync(i,"755")};import E from"path";import T from"fs-extra";var je=({projectDir:e,packages:s,scopedAppName:r,databaseProvider:t})=>{d({projectDir:e,dependencies:["drizzle-kit"],devMode:!0}),d({projectDir:e,dependencies:["drizzle-orm",{planetscale:"@planetscale/database",mysql:"mysql2",postgres:"postgres",sqlite:"@libsql/client"}[t]],devMode:!1});let n=E.join(p,"template/extras"),i=E.join(n,`config/drizzle-config-${t==="planetscale"?"mysql":t}.ts`),o=E.join(e,"drizzle.config.ts"),a=E.join(n,"src/server/db/schema-drizzle",s?.nextAuth.inUse?`with-auth-${t}.ts`:`base-${t}.ts`),c=E.join(e,"src/server/db/schema.ts"),m=T.readFileSync(a,"utf-8");m=m.replace("project1_${name}",`${r}_\${name}`);let u=T.readFileSync(i,"utf-8");u=u.replace("project1_*",`${r}_*`);let x=E.join(n,`src/server/db/index-drizzle/with-${t}.ts`),h=E.join(e,"src/server/db/index.ts");A({projectDir:e,scripts:{"db:push":"drizzle-kit push","db:studio":"drizzle-kit studio","db:generate":"drizzle-kit generate","db:migrate":"drizzle-kit migrate"}}),T.copySync(i,o),T.mkdirSync(E.dirname(c),{recursive:!0}),T.writeFileSync(c,m),T.writeFileSync(o,u),T.copySync(x,h)};import I from"path";import le from"fs-extra";var w=()=>{let e=process.env.npm_config_user_agent;return e?e.startsWith("yarn")?"yarn":e.startsWith("pnpm")?"pnpm":e.startsWith("bun")?"bun":"npm":"npm"};var Oe=({projectDir:e,packages:s})=>{let r=["prettier","eslint","eslint-config-next","typescript-eslint","@eslint/eslintrc"];s?.tailwind.inUse&&r.push("prettier-plugin-tailwindcss"),s?.drizzle.inUse&&r.push("eslint-plugin-drizzle"),d({projectDir:e,dependencies:r,devMode:!0});let t=I.join(p,"template/extras"),n;s?.tailwind.inUse?n=I.join(t,"config/_tailwind.prettier.config.js"):n=I.join(t,"config/_prettier.config.js");let i=I.join(e,"prettier.config.js");if(le.copySync(n,i),w()==="pnpm"){let u=I.join(t,"pnpm/_npmrc");le.copySync(u,I.join(e,".npmrc"))}A({projectDir:e,scripts:{lint:"next lint","lint:fix":"next lint --fix",check:"next lint && tsc --noEmit","format:write":'prettier --write "**/*.{ts,tsx,js,jsx,mdx}" --cache',"format:check":'prettier --check "**/*.{ts,tsx,js,jsx,mdx}" --cache'}});let a=!!s?.drizzle?.inUse,c=I.join(t,a?"config/_eslint.drizzle.js":"config/_eslint.base.js"),m=I.join(e,"eslint.config.js");le.copySync(c,m)};var K=["mysql","postgres","sqlite","planetscale"],Le=(e,s)=>({nextAuth:{inUse:e.includes("nextAuth"),installer:Pe},prisma:{inUse:e.includes("prisma"),installer:ke},drizzle:{inUse:e.includes("drizzle"),installer:je},tailwind:{inUse:e.includes("tailwind"),installer:Ie},trpc:{inUse:e.includes("trpc"),installer:Ce},supabase:{inUse:e.includes("supabase"),installer:Ae},zustand:{inUse:e.includes("zustand"),installer:Te},dbContainer:{inUse:["mysql","postgres"].includes(s),installer:Ne},envVariables:{inUse:!0,installer:xe},eslint:{inUse:e.includes("eslint"),installer:Oe},biome:{inUse:e.includes("biome"),installer:Re}});import At from"path";import Et from"fs-extra";var O=()=>{let e=At.join(p,"package.json");return Et.readJSONSync(e).version??"1.0.0"};var M=class extends Error{constructor(s){super(s)}};import X from"chalk";var l={error(...e){console.log(X.red(...e))},warn(...e){console.log(X.yellow(...e))},info(...e){console.log(X.cyan(...e))},success(...e){console.log(X.green(...e))}};var It=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,Fe=e=>{let s=H(e),r=s.split("/"),t=r.findIndex(i=>i.startsWith("@")),n=r[r.length-1];if(r.findIndex(i=>i.startsWith("@"))!==-1&&(n=r.slice(t).join("/")),!(s==="."||It.test(n??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};var De=e=>{if(e.startsWith(".")||e.startsWith("/"))return"Import alias can't start with '.' or '/'"};var R={appName:V,packages:["supabase","zustand","tailwind","trpc","eslint"],flags:{noGit:!1,noInstall:!1,default:!1,CI:!1,tailwind:!1,trpc:!1,prisma:!1,drizzle:!1,nextAuth:!1,supabase:!1,zustand:!1,importAlias:"~/",appRouter:!1,dbProvider:"postgres",eslint:!1,biome:!1},databaseProvider:"postgres"},Me=async()=>{let e=R,s=new Ct().name(ne).description("A CLI for creating web applications with the t3 stack").argument("[dir]","The name of the application, as well as the name of the directory to create").option("--noGit","Explicitly tell the CLI to not initialize a new git repo in the project",!1).option("--noInstall","Explicitly tell the CLI to not run the package manager's install command",!1).option("-y, --default","Bypass the CLI and use all default options to bootstrap a new t3-app",!1).option("--CI","Boolean value if we're running in CI",!1).option("--tailwind [boolean]","Experimental: Boolean value if we should install Tailwind CSS. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--nextAuth [boolean]","Experimental: Boolean value if we should install NextAuth.js. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--prisma [boolean]","Experimental: Boolean value if we should install Prisma. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--drizzle [boolean]","Experimental: Boolean value if we should install Drizzle. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--trpc [boolean]","Experimental: Boolean value if we should install tRPC. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--supabase [boolean]","Experimental: Boolean value if we should install Supabase. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--zustand [boolean]","Experimental: Boolean value if we should install Zustand. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("-i, --import-alias","Explicitly tell the CLI to use a custom import alias",R.flags.importAlias).option("--dbProvider [provider]",`Choose a database provider to use. Possible values: ${K.join(", ")}`,R.flags.dbProvider).option("--appRouter [boolean]","Explicitly tell the CLI to use the new Next.js app router",t=>!!t&&t!=="false").option("--eslint [boolean]","Experimental: Boolean value if we should install eslint and prettier. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").option("--biome [boolean]","Experimental: Boolean value if we should install biome. Must be used in conjunction with `--CI`.",t=>!!t&&t!=="false").version(O(),"-v, --version","Display the version number").addHelpText("afterAll",`
The t3 stack was inspired by ${ce.hex("#E8DCFF").bold("@t3dotgg")} and has been used to build awesome fullstack applications like ${ce.hex("#E24A8D").underline("https://ping.gg")}
`).parse(process.argv);process.env.npm_config_user_agent?.startsWith("yarn/3")&&l.warn(` WARNING: It looks like you are using Yarn 3. This is currently not supported,
and likely to result in a crash. Please run create-t3-app with another
package manager such as pnpm, npm, or Yarn Classic.
See: https://github.com/t3-oss/create-t3-app/issues/57`);let r=s.args[0];if(r&&(e.appName=r),e.flags=s.opts(),e.flags.CI)return e.packages=[],e.flags.trpc&&e.packages.push("trpc"),e.flags.tailwind&&e.packages.push("tailwind"),e.flags.prisma&&e.packages.push("prisma"),e.flags.drizzle&&e.packages.push("drizzle"),e.flags.nextAuth&&e.packages.push("nextAuth"),e.flags.supabase&&e.packages.push("supabase"),e.flags.zustand&&e.packages.push("zustand"),e.flags.eslint&&e.packages.push("eslint"),e.flags.biome&&e.packages.push("biome"),e.flags.prisma&&e.flags.drizzle&&(l.warn("Incompatible combination Prisma + Drizzle. Exiting."),process.exit(0)),e.flags.biome&&e.flags.eslint&&(l.warn("Incompatible combination Biome + ESLint. Exiting."),process.exit(0)),K.includes(e.flags.dbProvider)===!1&&(l.warn(`Incompatible database provided. Use: ${K.join(", ")}. Exiting.`),process.exit(0)),e.databaseProvider=e.packages.includes("drizzle")||e.packages.includes("prisma")?e.flags.dbProvider:"sqlite",e;if(e.flags.default)return e;try{if(process.env.TERM_PROGRAM?.toLowerCase().includes("mintty"))throw l.warn(` WARNING: It looks like you are using MinTTY, which is non-interactive. This is most likely because you are
using Git Bash. If that's that case, please use Git Bash from another terminal, such as Windows Terminal. Alternatively, you
can provide the arguments from the CLI directly: https://create.t3.gg/en/installation#experimental-usage to skip the prompts.`),new M("Non-interactive environment");let t=w(),n=await f.group({...!r&&{name:()=>f.text({message:"What will your project be called?",defaultValue:r,validate:Fe})},language:()=>f.select({message:"Will you be using TypeScript or JavaScript?",options:[{value:"typescript",label:"TypeScript"},{value:"javascript",label:"JavaScript"}],initialValue:"typescript"}),_:({results:o})=>o.language==="javascript"?f.note(ce.redBright("Wrong answer, using TypeScript instead")):void 0,styling:()=>f.confirm({message:"Will you be using Tailwind CSS for styling?"}),trpc:()=>f.confirm({message:"Would you like to use tRPC?"}),authentication:()=>f.select({message:"What authentication provider would you like to use?",options:[{value:"none",label:"None"},{value:"next-auth",label:"NextAuth.js"},{value:"supabase",label:"Supabase Auth"}],initialValue:"supabase"}),stateManagement:()=>f.select({message:"What state management would you like to use?",options:[{value:"none",label:"None"},{value:"zustand",label:"Zustand"}],initialValue:"zustand"}),database:()=>f.select({message:"What database ORM would you like to use?",options:[{value:"none",label:"None"},{value:"prisma",label:"Prisma"},{value:"drizzle",label:"Drizzle"},{value:"supabase",label:"Supabase Database"}],initialValue:"supabase"}),appRouter:()=>f.confirm({message:"Would you like to use Next.js App Router?",initialValue:!0}),databaseProvider:({results:o})=>{if(o.database!=="none")return f.select({message:"What database provider would you like to use?",options:[{value:"sqlite",label:"SQLite (LibSQL)"},{value:"mysql",label:"MySQL"},{value:"postgres",label:"PostgreSQL"},{value:"planetscale",label:"PlanetScale"}],initialValue:"sqlite"})},linter:()=>f.select({message:"Would you like to use ESLint and Prettier or Biome for linting and formatting?",options:[{value:"eslint",label:"ESLint/Prettier"},{value:"biome",label:"Biome"}],initialValue:"eslint"}),...!e.flags.noGit&&{git:()=>f.confirm({message:"Should we initialize a Git repository and stage the changes?",initialValue:!R.flags.noGit})},...!e.flags.noInstall&&{install:()=>f.confirm({message:`Should we run '${t}`+(t==="yarn"?"'?":" install' for you?"),initialValue:!R.flags.noInstall})},importAlias:()=>f.text({message:"What import alias would you like to use?",defaultValue:R.flags.importAlias,placeholder:R.flags.importAlias,validate:De})},{onCancel(){process.exit(1)}}),i=[];return n.styling&&i.push("tailwind"),n.trpc&&i.push("trpc"),n.authentication==="next-auth"&&i.push("nextAuth"),n.authentication==="supabase"&&i.push("supabase"),n.stateManagement==="zustand"&&i.push("zustand"),n.database==="prisma"&&i.push("prisma"),n.database==="drizzle"&&i.push("drizzle"),n.database==="supabase"&&i.push("supabase"),n.linter==="eslint"&&i.push("eslint"),n.linter==="biome"&&i.push("biome"),{appName:n.name??e.appName,packages:i,databaseProvider:n.databaseProvider||"sqlite",flags:{...e.flags,appRouter:n.appRouter??e.flags.appRouter,noGit:!n.git||e.flags.noGit,noInstall:!n.install||e.flags.noInstall,importAlias:n.importAlias??e.flags.importAlias}}}catch(t){if(t instanceof M)l.warn(`
${ne} needs an interactive terminal to provide options`),await f.confirm({message:"Continue scaffolding a default T3 app?",initialValue:!0})||(l.info("Exiting..."),process.exit(0)),l.info(`Bootstrapping a default T3 app in ./${e.appName}`);else throw t}return e};import He from"fs";import G from"path";import Be from"chalk";import Tt from"ora";var Ge=e=>{let{packages:s}=e;l.info("Adding boilerplate...");for(let[r,t]of Object.entries(s))if(t.inUse){let n=Tt(`Boilerplating ${r}...`).start();t.installer(e),n.succeed(Be.green(`Successfully setup boilerplate for ${Be.green.bold(r)}`))}l.info("")};import pe from"path";import*as z from"@clack/prompts";import U from"chalk";import B from"fs-extra";import Rt from"ora";var $e=async({projectName:e,projectDir:s,pkgManager:r,noInstall:t})=>{let n=pe.join(p,"template/base");t?l.info(""):l.info(`
Using: ${U.cyan.bold(r)}
`);let i=Rt(`Scaffolding in: ${s}...
`).start();if(B.existsSync(s))if(B.readdirSync(s).length===0)e!=="."&&i.info(`${U.cyan.bold(e)} exists but is empty, continuing...
`);else{i.stopAndPersist();let a=await z.select({message:`${U.redBright.bold("Warning:")} ${U.cyan.bold(e)} already exists and isn't empty. How would you like to proceed?`,options:[{label:"Abort installation (recommended)",value:"abort"},{label:"Clear the directory and continue installation",value:"clear"},{label:"Continue installation and overwrite conflicting files",value:"overwrite"}],initialValue:"abort"});(z.isCancel(a)||a==="abort")&&(i.fail("Aborting installation..."),process.exit(1));let c=await z.confirm({message:`Are you sure you want to ${a==="clear"?"clear the directory":"overwrite conflicting files"}?`,initialValue:!1});(z.isCancel(c)||!c)&&(i.fail("Aborting installation..."),process.exit(1)),a==="clear"&&(i.info(`Emptying ${U.cyan.bold(e)} and creating t3 app..
`),B.emptyDirSync(s))}i.start(),B.copySync(n,s),B.renameSync(pe.join(s,"_gitignore"),pe.join(s,".gitignore"));let o=e==="."?"App":U.cyan.bold(e);i.succeed(`${o} ${U.green("scaffolded successfully!")}
`)};import S from"path";import Z from"fs-extra";var qe=({projectDir:e,packages:s})=>{let r=S.join(p,"template/extras/src/pages/_app"),t=s.tailwind.inUse,n=s.trpc.inUse,i=s.nextAuth.inUse,o="base.tsx";n&&t&&i?o="with-auth-trpc-tw.tsx":n&&!t&&i?o="with-auth-trpc.tsx":n&&t?o="with-trpc-tw.tsx":n&&!t?o="with-trpc.tsx":!n&&t?o="with-tw.tsx":i&&t?o="with-auth-tw.tsx":i&&!t&&(o="with-auth.tsx");let a=S.join(r,o),c=S.join(e,"src/pages/_app.tsx");Z.copySync(a,c)},Ve=({projectDir:e,packages:s})=>{let r=S.join(p,"template/extras/src/app/layout"),t=s.tailwind.inUse,n=s.trpc.inUse,i="base.tsx";n&&t?i="with-trpc-tw.tsx":n&&!t?i="with-trpc.tsx":!n&&t&&(i="with-tw.tsx");let o=S.join(r,i),a=S.join(e,"src/app/layout.tsx");Z.copySync(o,a)},We=({projectDir:e,packages:s})=>{let r=S.join(p,"template/extras/src/pages/index"),t=s.trpc.inUse,n=s.tailwind.inUse,i=s.nextAuth.inUse,o="base.tsx";t&&n&&i?o="with-auth-trpc-tw.tsx":t&&!n&&i?o="with-auth-trpc.tsx":t&&n?o="with-trpc-tw.tsx":t&&!n?o="with-trpc.tsx":!t&&n&&(o="with-tw.tsx");let a=S.join(r,o),c=S.join(e,"src/pages/index.tsx");Z.copySync(a,c)},Je=({projectDir:e,packages:s})=>{let r=S.join(p,"template/extras/src/app/page"),t=s.trpc.inUse,n=s.tailwind.inUse,i=s.nextAuth.inUse,o="base.tsx";t&&n&&i?o="with-auth-trpc-tw.tsx":t&&!n&&i?o="with-auth-trpc.tsx":t&&n?o="with-trpc-tw.tsx":t&&!n?o="with-trpc.tsx":!t&&n&&(o="with-tw.tsx");let a=S.join(r,o),c=S.join(e,"src/app/page.tsx");Z.copySync(a,c)};var Ye=async({projectName:e,scopedAppName:s,packages:r,noInstall:t,appRouter:n,databaseProvider:i})=>{let o=w(),a=G.resolve(process.cwd(),e);if(await $e({projectName:e,projectDir:a,pkgManager:o,scopedAppName:s,noInstall:t,appRouter:n,databaseProvider:i}),Ge({projectName:e,scopedAppName:s,projectDir:a,pkgManager:o,packages:r,noInstall:t,appRouter:n,databaseProvider:i}),n?(He.copyFileSync(G.join(p,"template/extras/config/next-config-appdir.js"),G.join(a,"next.config.js")),Ve({projectDir:a,packages:r}),Je({projectDir:a,packages:r})):(qe({projectDir:a,packages:r}),We({projectDir:a,packages:r})),!r.tailwind.inUse){let c=G.join(p,"template/extras/src/index.module.css"),m=G.join(a,"src",n?"app":"pages","index.module.css");He.copyFileSync(c,m)}return a};import{execSync as me}from"child_process";import ue from"path";import*as de from"@clack/prompts";import $ from"chalk";import{execa as q}from"execa";import Ke from"fs-extra";import Ut from"ora";var zt=e=>{try{return me("git --version",{cwd:e}),!0}catch{return!1}},fe=e=>Ke.existsSync(ue.join(e,".git")),ge=async e=>{try{return await q("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},Nt=()=>{let s=me("git --version").toString().trim().split(" ")[2],r=s?.split(".")[0],t=s?.split(".")[1];return{major:Number(r),minor:Number(t)}},jt=()=>me("git config --global init.defaultBranch || echo main").toString().trim(),Xe=async e=>{if(l.info("Initializing Git..."),!zt(e)){l.warn("Git is not installed. Skipping Git initialization.");return}let s=Ut(`Creating a new git repo...
`).start(),r=fe(e),t=await ge(e),n=ue.parse(e).name;if(t&&r){if(s.stop(),!await de.confirm({message:`${$.redBright.bold("Warning:")} Git is already initialized in "${n}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,initialValue:!1})){s.info("Skipping Git initialization.");return}Ke.removeSync(ue.join(e,".git"))}else if(t&&!r&&(s.stop(),!await de.confirm({message:`${$.redBright.bold("Warning:")} "${n}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,initialValue:!1}))){s.info("Skipping Git initialization.");return}try{let i=jt(),{major:o,minor:a}=Nt();o<2||o==2&&a<28?(await q("git",["init"],{cwd:e}),await q("git",["symbolic-ref","HEAD",`refs/heads/${i}`],{cwd:e})):await q("git",["init",`--initial-branch=${i}`],{cwd:e}),await q("git",["add","."],{cwd:e}),s.succeed(`${$.green("Successfully initialized and staged")} ${$.green.bold("git")}
`)}catch{s.fail(`${$.bold.red("Failed:")} could not initialize git. Update git to the latest version!
`)}};var Ze=async({projectName:e=V,packages:s,noInstall:r,projectDir:t,databaseProvider:n})=>{let i=w();l.info("Next steps:"),e!=="."&&l.info(` cd ${e}`),r&&(i==="yarn"?l.info(` ${i}`):l.info(` ${i} install`)),["postgres","mysql"].includes(n)&&l.info(" Start up a database, if needed using './start-database.sh'"),(s?.prisma.inUse||s?.drizzle.inUse)&&(["npm","bun"].includes(i)?l.info(` ${i} run db:push`):l.info(` ${i} db:push`)),s?.nextAuth.inUse&&l.info(" Fill in your .env with necessary values. See https://create.t3.gg/en/usage/first-steps for more info."),["npm","bun"].includes(i)?l.info(` ${i} run dev`):l.info(` ${i} dev`),!await ge(t)&&!fe(t)&&l.info(" git init"),l.info(' git commit -m "initial commit"')};import Q from"fs";import Ot from"path";function Qe(e,s,r){Q.readdirSync(e).forEach(n=>{let i=Ot.join(e,n);if(Q.statSync(i).isDirectory())Qe(i,s,r);else{let a=Q.readFileSync(i,"utf8").replace(new RegExp(s,"g"),r);Q.writeFileSync(i,a,"utf8")}})}var et=(e,s)=>{let r=s.replace(/\*/g,"").replace(/[^/]$/,"$&/");Qe(e,"~/",r)};import Lt from"gradient-string";var Ft={blue:"#add7ff",cyan:"#89ddff",green:"#5de4c7",magenta:"#fae4fc",red:"#d0679d",yellow:"#fffac2"},tt=()=>{let e=Lt(Object.values(Ft)),s=w();(s==="yarn"||s==="pnpm")&&console.log(""),console.log(e.multiline(ye))};import Dt from"chalk";import{execa as st}from"execa";import Mt from"ora";var nt=async({pkgManager:e,projectDir:s,eslint:r,biome:t})=>{l.info(`Formatting project with ${r?"eslint":"biome"}...`);let n=Mt(`Running format command
`).start();r?await st(e,["run","format:write"],{cwd:s}):t&&await st(e,["run","check:unsafe"],{cwd:s}),n.succeed(`${Dt.green("Successfully formatted project")}`)};import Bt from"chalk";import{execa as it}from"execa";import rt from"ora";var he=async(e,s,r)=>{let{onDataHandle:t,args:n=["install"],stdout:i="pipe"}=r,o=rt(`Running ${s} install...`).start(),a=it(s,n,{cwd:e,stdout:i});return await new Promise((c,m)=>{t&&a.stdout?.on("data",t(o)),a.on("error",u=>m(u)),a.on("close",()=>c())}),o},Gt=async(e,s)=>{switch(e){case"npm":return await it(e,["install"],{cwd:s,stderr:"inherit"}),null;case"pnpm":return he(s,e,{onDataHandle:r=>t=>{let n=t.toString();n.includes("Progress")&&(r.text=n.includes("|")?n.split(" | ")[1]??"":n)}});case"yarn":return he(s,e,{onDataHandle:r=>t=>{r.text=t.toString()}});case"bun":return he(s,e,{stdout:"ignore"})}},ot=async({projectDir:e})=>{l.info("Installing dependencies...");let s=w();(await Gt(s,e)??rt()).succeed(Bt.green(`Successfully installed dependencies!
`))};import{execSync as $t}from"child_process";import qt from"https";var at=e=>{let s=O();s.includes("beta")?(l.warn(" You are using a beta version of create-t3-app."),l.warn(" Please report any bugs you encounter.")):s.includes("next")?(l.warn(" You are running create-t3-app with the @next tag which is no longer maintained."),l.warn(" Please run the CLI with @latest instead.")):s!==e&&(l.warn(" You are using an outdated version of create-t3-app."),l.warn(" Your version:",s+".","Latest version in the npm registry:",e),l.warn(" Please run the CLI with @latest to get the latest updates.")),console.log("")};function Vt(){return new Promise((e,s)=>{qt.get("https://registry.npmjs.org/-/package/create-t3-app/dist-tags",r=>{if(r.statusCode===200){let t="";r.on("data",n=>t+=n),r.on("end",()=>{e(JSON.parse(t).latest)})}else s()}).on("error",()=>{s()})})}var lt=()=>Vt().catch(()=>{try{return $t("npm view create-t3-app version").toString().trim()}catch{return null}});var Jt=async()=>{let e=await lt(),s=w();tt(),e&&at(e);let{appName:r,packages:t,flags:{noGit:n,noInstall:i,importAlias:o,appRouter:a},databaseProvider:c}=await Me(),m=Le(t,c),[u,x]=Y(r),h=await Ye({projectName:x,scopedAppName:u,packages:m,databaseProvider:c,importAlias:o,noInstall:i,appRouter:a}),v=pt.readJSONSync(ct.join(h,"package.json"));if(v.name=u,v.ct3aMetadata={initVersion:O()},s!=="bun"){let{stdout:k}=await Wt(s,["-v"],{cwd:h});v.packageManager=`${s}@${k.trim()}`}pt.writeJSONSync(ct.join(h,"package.json"),v,{spaces:2}),o!=="~/"&&et(h,o),i||(await ot({projectDir:h}),await nt({pkgManager:s,projectDir:h,eslint:t.includes("eslint"),biome:t.includes("biome")})),n||await Xe(h),await Ze({projectName:x,packages:m,appRouter:a,noInstall:i,projectDir:h,databaseProvider:c}),process.exit(0)};Jt().catch(e=>{l.error("Aborting installation..."),e instanceof Error?l.error(e):(l.error("An unknown error has occurred. Please open an issue on github with the below:"),console.log(e)),process.exit(1)});