create-tnt-stack 0.5.2 → 0.5.3-beta.253ed67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import Fe from"path";import{execa as wt}from"execa";import Le from"fs-extra";import{confirm as J,input as he,select as x}from"@inquirer/prompts";import{Command as nt}from"commander";import X from"path";import{fileURLToPath as Ve}from"url";var ze=Ve(import.meta.url),Ke=X.dirname(ze),c=X.join(Ke,"../"),Z=` ___ ___ ___ _ _____ ___ _____ _ _ _____ ___ _____ _ ___ _ __
2
+ import Be from"path";import{execa as xt}from"execa";import Je from"fs-extra";import{confirm as V,input as ke,select as A}from"@inquirer/prompts";import{Command as rt}from"commander";import ee from"path";import{fileURLToPath as Ye}from"url";var Qe=Ye(import.meta.url),Xe=ee.dirname(Qe),p=ee.join(Xe,"../"),te=` ___ ___ ___ _ _____ ___ _____ _ _ _____ ___ _____ _ ___ _ __
3
3
  / __| _ \\ __| /_\\_ _| __| |_ _| \\| |_ _| / __|_ _/_\\ / __| |/ /
4
4
  | (__| / _| / _ \\| | | _| | | | .\` | | | \\__ \\ | |/ _ \\ (__| ' <
5
5
  \\___|_|_\\___/_/ \\_\\_| |___| |_| |_|\\_| |_| |___/ |_/_/ \\_\\___|_|\\_\\
6
- `,j="my-tnt-app",U="create-tnt-stack";import y from"path";import k from"fs-extra";import te from"path";import ae from"fs-extra";import He from"sort-package-json";var ee={"next-auth":"^5.0.0-beta.25","@auth/prisma-adapter":"^2.8.0",prisma:"^6.5.0","@prisma/client":"^6.5.0","@t3-oss/env-nextjs":"^0.12.0",zod:"^3.24.2",prettier:"^3.5.3","prettier-plugin-tailwindcss":"^0.6.11","@ianvs/prettier-plugin-sort-imports":"^4.4.1",eslint:"^9","eslint-config-next":"^15.2.4","@eslint/eslintrc":"^3.3.1",typescript:"^5.8.2","@types/node":"^22","@types/react":"^19","@types/react-dom":"^19",payload:"^3.33.0","@payloadcms/next":"^3.33.0","@payloadcms/payload-cloud":"^3.33.0","@payloadcms/richtext-lexical":"^3.33.0","@payloadcms/db-vercel-postgres":"^3.33.0","@payloadcms/db-sqlite":"^3.33.0",graphql:"^16.10.0",sharp:"^0.34.1"};var f=e=>{let{dependencies:a,devMode:s,projectDir:o}=e,t=ae.readJsonSync(te.join(o,"package.json"));a.forEach(i=>{let l=ee[i];s&&t.devDependencies?t.devDependencies[i]=l:t.dependencies&&(t.dependencies[i]=l)});let n=He(t);ae.writeJsonSync(te.join(o,"package.json"),n,{spaces:2})};var ne=({projectDir:e,packages:a,databaseProvider:s})=>{let o=[],t=[];if(a?.payload.inUse)switch(o.push("payload"),o.push("@payloadcms/next"),o.push("@payloadcms/payload-cloud"),o.push("@payloadcms/richtext-lexical"),o.push("graphql"),s){case"sqlite":t.push("@payloadcms/db-sqlite");break;case"postgresql":t.push("@payloadcms/db-vercel-postgres");break}f({projectDir:e,dependencies:o,devMode:!1}),f({projectDir:e,dependencies:t,devMode:!1});let n=y.join(c,"template/packages"),i=y.join(n,"config/payload",`${s==="postgresql"?"with-postgres":"with-sqlite"}.ts`),l=y.join(e,"payload.config.ts");k.copyFileSync(i,l);let d=y.join(n,"src/payload/collections"),g=y.join(e,"src/collections");k.mkdirSync(g,{recursive:!0}),k.copyFileSync(y.join(d,"Media.ts"),y.join(g,"Media.ts")),k.copyFileSync(y.join(d,"Users.ts"),y.join(g,"Users.ts"));let u=y.join(n,"src/app/(payload)"),p=y.join(e,"src/app/(payload)");k.mkdirSync(p,{recursive:!0}),k.copySync(u,p);let b=y.join(e,"package.json"),_=k.readJSONSync(b);_.scripts={..._.scripts,payload:"payload","generate:importmap":"payload generate:importmap","generate:types":"payload generate:types"},k.writeJSONSync(b,_,{spaces:2})};import se from"path";import Ye from"fs-extra";var oe=({projectDir:e,packages:a})=>{let s=a?.typescript.inUse,o=a?.payload.inUse,t=[];s&&(t.push("typescript"),t.push("@types/node"),t.push("@types/react"),t.push("@types/react-dom")),f({projectDir:e,dependencies:t,devMode:!0});let n=se.join(c,"template/packages/config/tsconfig",`${o?"with-payload":"base"}.json`),i=se.join(e,"tsconfig.json");Ye.copyFileSync(n,i)};import S from"path";import G from"fs-extra";var ie=({projectDir:e,packages:a})=>{let s=a?.prisma.inUse,o=["next-auth"];s&&o.push("@auth/prisma-adapter"),f({projectDir:e,dependencies:o,devMode:!1});let t=S.join(c,"template/packages"),n="src/app/api/auth/[...nextauth]/route.ts",i=S.join(t,n),l=S.join(e,n),d=S.join(t,"src/server/auth/config",s?"authjs-with-prisma.ts":"authjs.ts"),g=S.join(e,"src/server/auth/config.ts"),u=S.join(t,"src/server/auth/authjs.ts"),p=S.join(e,"src/server/auth/index.ts");G.copySync(i,l),G.copySync(d,g),G.copySync(u,p)};import M from"path";import F from"fs-extra";var re=({projectDir:e,scopedAppName:a,packages:s,databaseProvider:o})=>{let t=s?.envVariables.inUse,n=s?.authjs.inUse,i=s?.prisma.inUse,l=s?.payload.inUse,d=[];t&&(d.push("@t3-oss/env-nextjs"),d.push("zod")),f({projectDir:e,dependencies:d,devMode:!1});let g=i||l,u=Qe(!!n,!!i,!!l,a,o),p="";if(g?n?p="with-authjs-db.js":l?p="with-payload.js":p="with-db.js":n&&(p="with-authjs.js"),p!==""){let We=M.join(c,"template/packages/src/env",p),Be=M.join(e,"src/env.js");F.copyFileSync(We,Be)}let b=M.join(e,".env"),_=M.join(e,".env.example"),qe=Xe+u,Q=Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64"),Je=u.replace('AUTH_SECRET=""',`AUTH_SECRET="${Q}" # Generated by create-tnt-stack`).replace('PAYLOAD_SECRET=""',`PAYLOAD_SECRET="${Q}" # Generated by create-tnt-stack`);F.writeFileSync(b,Je,"utf-8"),F.writeFileSync(_,qe,"utf-8")};function Qe(e,a,s,o,t){let n=`
6
+ `,C="my-tnt-app",F="create-tnt-stack";import w from"path";import T from"fs-extra";import se from"path";import ne from"fs-extra";import Ze from"sort-package-json";var ae={"next-auth":"^5.0.0-beta.25","@auth/prisma-adapter":"^2.8.0","better-auth":"^1.2.7","better-sqlite3":"^11.9.1","@types/better-sqlite3":"^7.6.13",prisma:"^6.5.0","@prisma/client":"^6.5.0","@t3-oss/env-nextjs":"^0.12.0",zod:"^3.24.2",prettier:"^3.5.3","prettier-plugin-tailwindcss":"^0.6.11","@ianvs/prettier-plugin-sort-imports":"^4.4.1",eslint:"^9","eslint-config-next":"^15.2.4","@eslint/eslintrc":"^3.3.1",typescript:"^5.8.2","@types/node":"^22","@types/react":"^19","@types/react-dom":"^19",payload:"^3.33.0","@payloadcms/next":"^3.33.0","@payloadcms/payload-cloud":"^3.33.0","@payloadcms/richtext-lexical":"^3.33.0","@payloadcms/db-vercel-postgres":"^3.33.0","@payloadcms/db-sqlite":"^3.33.0",graphql:"^16.10.0",sharp:"^0.34.1"};var g=e=>{let{dependencies:a,devMode:s,projectDir:o}=e,t=ne.readJsonSync(se.join(o,"package.json"));a.forEach(i=>{let r=ae[i];s&&t.devDependencies?t.devDependencies[i]=r:t.dependencies&&(t.dependencies[i]=r)});let n=Ze(t);ne.writeJsonSync(se.join(o,"package.json"),n,{spaces:2})};var oe=({projectDir:e,packages:a,databaseProvider:s})=>{let o=a?.prisma.inUse,t=["better-auth"],n=[];o||t.push("better-sqlite3"),o||n.push("@types/better-sqlite3"),g({projectDir:e,dependencies:t,devMode:!1}),g({projectDir:e,dependencies:n,devMode:!0});let i=w.join(p,"template/packages"),r=w.join(i,"src/app/api/auth/[...betterauth]/route.ts"),m=w.join(e,"src/app/api/auth/[...all]/route.ts"),u=w.join(i,"src/server/auth/config",o?"better-auth-with-prisma.ts":"better-auth.ts"),h=T.readFileSync(u,"utf-8");o&&s!=="sqlite"&&(h=h.replace('provider: "sqlite",',`provider: "${{mysql:"mysql",postgresql:"postgresql"}[s]}",`));let d=w.join(e,"src/server/auth/index.ts");T.mkdirSync(w.dirname(d),{recursive:!0}),T.writeFileSync(d,h);let c=w.join(i,"src/lib/auth/better-auth-client.ts"),v=w.join(e,"src/lib/auth/client.ts");T.copySync(r,m),T.copySync(c,v)};import y from"path";import _ from"fs-extra";var ie=({projectDir:e,packages:a,databaseProvider:s})=>{let o=[],t=[];if(a?.payload.inUse)switch(o.push("payload"),o.push("@payloadcms/next"),o.push("@payloadcms/payload-cloud"),o.push("@payloadcms/richtext-lexical"),o.push("graphql"),s){case"sqlite":t.push("@payloadcms/db-sqlite");break;case"postgresql":t.push("@payloadcms/db-vercel-postgres");break}g({projectDir:e,dependencies:o,devMode:!1}),g({projectDir:e,dependencies:t,devMode:!1});let n=y.join(p,"template/packages"),i=y.join(n,"config/payload",`${s==="postgresql"?"with-postgres":"with-sqlite"}.ts`),r=y.join(e,"payload.config.ts");_.copyFileSync(i,r);let m=y.join(n,"src/payload/collections"),u=y.join(e,"src/collections");_.mkdirSync(u,{recursive:!0}),_.copyFileSync(y.join(m,"Media.ts"),y.join(u,"Media.ts")),_.copyFileSync(y.join(m,"Users.ts"),y.join(u,"Users.ts"));let h=y.join(n,"src/app/(payload)"),d=y.join(e,"src/app/(payload)");_.mkdirSync(d,{recursive:!0}),_.copySync(h,d);let c=y.join(e,"package.json"),v=_.readJSONSync(c);v.scripts={...v.scripts,payload:"payload","generate:importmap":"payload generate:importmap","generate:types":"payload generate:types"},_.writeJSONSync(c,v,{spaces:2})};import re from"path";import et from"fs-extra";var le=({projectDir:e,packages:a})=>{let s=a?.typescript.inUse,o=a?.payload.inUse,t=[];s&&(t.push("typescript"),t.push("@types/node"),t.push("@types/react"),t.push("@types/react-dom")),g({projectDir:e,dependencies:t,devMode:!0});let n=re.join(p,"template/packages/config/tsconfig",`${o?"with-payload":"base"}.json`),i=re.join(e,"tsconfig.json");et.copyFileSync(n,i)};import x from"path";import L from"fs-extra";var pe=({projectDir:e,packages:a})=>{let s=a?.prisma.inUse,o=["next-auth"];s&&o.push("@auth/prisma-adapter"),g({projectDir:e,dependencies:o,devMode:!1});let t=x.join(p,"template/packages"),n="src/app/api/auth/[...nextauth]/route.ts",i=x.join(t,n),r=x.join(e,n),m=x.join(t,"src/server/auth/config",s?"authjs-with-prisma.ts":"authjs.ts"),u=x.join(e,"src/server/auth/config.ts"),h=x.join(t,"src/server/auth/authjs.ts"),d=x.join(e,"src/server/auth/index.ts");L.copySync(i,r),L.copySync(m,u),L.copySync(h,d)};import M from"path";import B from"fs-extra";var ce=({projectDir:e,scopedAppName:a,packages:s,databaseProvider:o})=>{let t=s?.envVariables.inUse,n=s?.authjs.inUse,i=s?.betterAuth.inUse,r=s?.prisma.inUse,m=s?.payload.inUse,u=[];t&&(u.push("@t3-oss/env-nextjs"),u.push("zod")),g({projectDir:e,dependencies:u,devMode:!1});let h=r||m,d=tt(!!n,!!i,!!r,!!m,a,o),c="";if(h?n?c="with-authjs-db.js":i?c="with-better-auth-db.js":m?c="with-payload.js":c="with-db.js":n?c="with-authjs.js":i&&(c="with-better-auth.js"),c!==""){let He=M.join(p,"template/packages/src/env",c),Ke=M.join(e,"src/env.js");B.copyFileSync(He,Ke)}let v=M.join(e,".env"),We=M.join(e,".env.example"),Ve=at+d,q=Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64"),ze=d.replace('AUTH_SECRET=""',`AUTH_SECRET="${q}" # Generated by create-tnt-stack`).replace('BETTER_AUTH_SECRET=""',`BETTER_AUTH_SECRET="${q}" # Generated by create-tnt-stack`).replace('PAYLOAD_SECRET=""',`PAYLOAD_SECRET="${q}" # Generated by create-tnt-stack`);B.writeFileSync(v,ze,"utf-8"),B.writeFileSync(We,Ve,"utf-8")};function tt(e,a,s,o,t,n){let i=`
7
7
  # When adding additional environment variables, the schema in "/src/env.js"
8
8
  # should be updated accordingly.
9
9
  `.trim().concat(`
10
- `);return a&&(n+=`
10
+ `);return s&&(i+=`
11
11
  # Prisma
12
12
  # https://www.prisma.io/docs/reference/database-reference/connection-urls#env
13
- `),a&&(t==="mysql"?n+=`DATABASE_URL="mysql://root:password@localhost:3306/${o}"`:t==="postgresql"?n+=`DATABASE_URL="postgresql://postgres:password@localhost:5432/${o}"`:t==="sqlite"&&(n+='DATABASE_URL="file:./db.sqlite"'),n+=`
14
- `),s&&(n+=`
13
+ `),s&&(n==="mysql"?i+=`DATABASE_URL="mysql://root:password@localhost:3306/${t}"`:n==="postgresql"?i+=`DATABASE_URL="postgresql://postgres:password@localhost:5432/${t}"`:n==="sqlite"&&(i+='DATABASE_URL="file:./db.sqlite"'),i+=`
14
+ `),o&&(i+=`
15
15
  # Payload CMS
16
16
  # https://payloadcms.com/docs/database/overview
17
- `,n+=`PAYLOAD_SECRET=""
18
- `,t==="postgresql"?n+=`DATABASE_URL="postgresql://postgres:password@localhost:5432/${o}"`:t==="sqlite"&&(n+='DATABASE_URL="file:./db.sqlite"'),n+=`
19
- `),e&&(n+=`
17
+ `,i+=`PAYLOAD_SECRET=""
18
+ `,n==="postgresql"?i+=`DATABASE_URL="postgresql://postgres:password@localhost:5432/${t}"`:n==="sqlite"&&(i+='DATABASE_URL="file:./db.sqlite"'),i+=`
19
+ `),e&&(i+=`
20
20
  # Next Auth
21
21
  # You can generate a new secret on the command line with:
22
22
  # npx auth secret
@@ -26,11 +26,21 @@ AUTH_SECRET=""
26
26
  # Next Auth Discord Provider
27
27
  DISCORD_CLIENT_ID=""
28
28
  DISCORD_CLIENT_SECRET=""
29
- `),!e&&!a&&!s&&(n+=`
29
+ `),a&&(i+=`
30
+ # Better Auth
31
+ # You can generate a new secret by going to the Better Auth docs:
32
+ # https://www.better-auth.com/docs/installation#set-environment-variables
33
+ BETTER_AUTH_SECRET=""
34
+ NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000" # Base URL of your app
35
+
36
+ # Better Auth Discord Provider
37
+ DISCORD_CLIENT_ID=""
38
+ DISCORD_CLIENT_SECRET=""
39
+ `),!e&&!s&&!o&&(i+=`
30
40
  # Example:
31
41
  # SERVERVAR="foo"
32
42
  # NEXT_PUBLIC_CLIENTVAR="bar"
33
- `),n}var Xe=`
43
+ `),i}var at=`
34
44
  # Since the ".env" file is gitignored, you can use the ".env.example" file to
35
45
  # build a new ".env" file when you clone the repo. Keep this file up-to-date
36
46
  # when you add new variables to \`.env\`.
@@ -40,19 +50,19 @@ DISCORD_CLIENT_SECRET=""
40
50
  # ".env" and populate it with your secrets.
41
51
  `.trim().concat(`
42
52
 
43
- `);import L from"path";import q from"fs-extra";var le=({projectDir:e,packages:a})=>{let s=a?.eslint.inUse,o=[];s&&(o.push("eslint"),o.push("eslint-config-next"),o.push("@eslint/eslintrc")),f({projectDir:e,dependencies:o,devMode:!0});let t=L.join(c,"template/packages/config","eslint.config.mjs"),n=L.join(e,"eslint.config.mjs"),i=L.join(e,"package.json"),l=q.readJSONSync(i);l.scripts={...l.scripts,lint:"next lint"},q.copyFileSync(t,n),q.writeJSONSync(i,l,{spaces:2})};import pe from"path";import Ze from"fs-extra";var ce=({projectDir:e,packages:a})=>{let s=a?.prettier.inUse,o=[];s&&(o.push("prettier"),o.push("prettier-plugin-tailwindcss"),o.push("@ianvs/prettier-plugin-sort-imports")),f({projectDir:e,dependencies:o,devMode:!0});let t=pe.join(c,"template/packages/config","prettier.config.mjs"),n=pe.join(e,"prettier.config.mjs");Ze.copyFileSync(t,n)};import w from"path";import P from"fs-extra";var me=({projectDir:e,packages:a,databaseProvider:s})=>{let o=a?.authjs.inUse;f({projectDir:e,dependencies:["prisma"],devMode:!0}),f({projectDir:e,dependencies:["@prisma/client"],devMode:!1});let t=w.join(c,"template/packages"),n=w.join(t,"prisma/schema",`${o?"with-authjs":"base"}.prisma`),i=P.readFileSync(n,"utf-8");s!=="sqlite"&&(i=i.replace('provider = "sqlite"',`provider = "${{mysql:"mysql",postgresql:"postgresql"}[s]}"`),["mysql"].includes(s)&&(i=i.replace("// @db.Text","@db.Text")));let l=w.join(e,"prisma/schema.prisma");P.mkdirSync(w.dirname(l),{recursive:!0}),P.writeFileSync(l,i);let d=w.join(t,"src/server/db/db-prisma.ts"),g=w.join(e,"src/server/db/index.ts");P.mkdirSync(w.dirname(g),{recursive:!0}),P.writeFileSync(g,P.readFileSync(d,"utf-8"));let u=w.join(e,"package.json"),p=P.readJSONSync(u);p.scripts={...p.scripts,postinstall:"prisma generate","db:push":"prisma db push","db:studio":"prisma studio","db:generate":"prisma migrate dev","db:migrate":"prisma migrate deploy"},P.writeJSONSync(u,p,{spaces:2})};var de=["none","authjs"],fe=["none","prisma"],R=["sqlite","mysql","postgresql"];var ge=e=>({authjs:{inUse:e.includes("authjs"),installer:ie},prisma:{inUse:e.includes("prisma"),installer:me},envVariables:{inUse:!0,installer:re},prettier:{inUse:e.includes("prettier"),installer:ce},eslint:{inUse:e.includes("eslint"),installer:le},typescript:{inUse:!0,installer:oe},payload:{inUse:e.includes("payload"),installer:ne}});import et from"path";import tt from"fs-extra";function A(){let e=et.join(c,"package.json");return tt.readJSONSync(e).version??"1.0.0"}var h=()=>{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 C=class extends Error{constructor(a){super(a)}};import E from"chalk";var r={error(...e){console.log(E.red(...e))},warn(...e){console.log(E.yellow(...e))},info(...e){console.log(E.cyan(...e))},success(...e){console.log(E.green(...e))}};var $=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e);var at=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;function ue(e){let a=$(e),s=a.split("/"),o=s.findIndex(n=>n.startsWith("@")),t=s[s.length-1];return s.findIndex(n=>n.startsWith("@"))!==-1&&(t=s.slice(o).join("/")),a=="."||at.test(t??"")?!0:"App name must consist of only lowercase alphanumeric characters, '-', and '_'"}var ye=e=>e.startsWith(".")||e.startsWith("/")?"Import alias can't start with '.' or '/'":!0;var m={appName:j,packages:[],flags:{noGit:!1,noInstall:!1,default:!1,CI:!1,authentication:"none",orm:"none",formatter:"prettier",linter:"eslint",importAlias:"@/",dbProvider:"sqlite",backend:"nextjs"},databaseProvider:"sqlite"};async function be(){let e=m,a=new nt().name(U).description("CLI for scaffolding new web apps with the TNT-Powered stack").version(A(),"-v, --version","Output the current version of Create TNT 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 tnt-stack",!1).option("--CI","Boolean value if we're running in CI",!1).option("--backend [framework]",`Choose a backend framework to use. Possible values: ${m.flags.backend}`,m.flags.backend).option("--authProvider [provider]",`Choose an authentication provider to use. Possible values: ${de.join(", ")}`,m.flags.authentication).option("--databaseORM [orm]",`Choose a database ORM to use. Possible values: ${fe.join(", ")}`,m.flags.orm).option("--formatter [formatter]",`Choose a formatter to use. Possible values: ${m.flags.formatter}`,m.flags.formatter).option("--linter [linter]",`Choose a linter to use. Possible values: ${m.flags.linter}`,m.flags.linter).option("-i, --import-alias [alias]","Explicitly tell the CLI to use a custom import alias",m.flags.importAlias).option("--dbProvider [provider]",`Choose a database provider to use. Possible values: ${R.join(", ")}`,m.flags.dbProvider).parse(process.argv);process.env.npm_config_user_agent?.startsWith("yarn/3")&&r.warn(` WARNING: It looks like you are using Yarn 3. This is currently not supported,
53
+ `);import J from"path";import W from"fs-extra";var me=({projectDir:e,packages:a})=>{let s=a?.eslint.inUse,o=[];s&&(o.push("eslint"),o.push("eslint-config-next"),o.push("@eslint/eslintrc")),g({projectDir:e,dependencies:o,devMode:!0});let t=J.join(p,"template/packages/config","eslint.config.mjs"),n=J.join(e,"eslint.config.mjs"),i=J.join(e,"package.json"),r=W.readJSONSync(i);r.scripts={...r.scripts,lint:"next lint"},W.copyFileSync(t,n),W.writeJSONSync(i,r,{spaces:2})};import de from"path";import st from"fs-extra";var fe=({projectDir:e,packages:a})=>{let s=a?.prettier.inUse,o=[];s&&(o.push("prettier"),o.push("prettier-plugin-tailwindcss"),o.push("@ianvs/prettier-plugin-sort-imports")),g({projectDir:e,dependencies:o,devMode:!0});let t=de.join(p,"template/packages/config","prettier.config.mjs"),n=de.join(e,"prettier.config.mjs");st.copyFileSync(t,n)};import P from"path";import S from"fs-extra";var ue=({projectDir:e,packages:a,databaseProvider:s})=>{let o=a?.authjs.inUse,t=a?.betterAuth.inUse;g({projectDir:e,dependencies:["prisma"],devMode:!0}),g({projectDir:e,dependencies:["@prisma/client"],devMode:!1});let n=P.join(p,"template/packages"),i=P.join(n,"prisma/schema",`${o?"with-authjs":t?"with-better-auth":"base"}.prisma`),r=S.readFileSync(i,"utf-8");s!=="sqlite"&&(r=r.replace('provider = "sqlite"',`provider = "${{mysql:"mysql",postgresql:"postgresql"}[s]}"`),["mysql"].includes(s)&&(r=r.replace("// @db.Text","@db.Text")));let m=P.join(e,"prisma/schema.prisma");S.mkdirSync(P.dirname(m),{recursive:!0}),S.writeFileSync(m,r);let u=P.join(n,"src/server/db/db-prisma.ts"),h=P.join(e,"src/server/db/index.ts");S.mkdirSync(P.dirname(h),{recursive:!0}),S.writeFileSync(h,S.readFileSync(u,"utf-8"));let d=P.join(e,"package.json"),c=S.readJSONSync(d);c.scripts={...c.scripts,postinstall:"prisma generate","db:push":"prisma db push","db:studio":"prisma studio","db:generate":"prisma migrate dev","db:migrate":"prisma migrate deploy"},S.writeJSONSync(d,c,{spaces:2})};var ge=["none","authjs","betterAuth"],he=["none","prisma"],U=["sqlite","mysql","postgresql"];var ye=e=>({authjs:{inUse:e.includes("authjs"),installer:pe},betterAuth:{inUse:e.includes("betterAuth"),installer:oe},prisma:{inUse:e.includes("prisma"),installer:ue},envVariables:{inUse:!0,installer:ce},prettier:{inUse:e.includes("prettier"),installer:fe},eslint:{inUse:e.includes("eslint"),installer:me},typescript:{inUse:!0,installer:le},payload:{inUse:e.includes("payload"),installer:ie}});import nt from"path";import ot from"fs-extra";function j(){let e=nt.join(p,"package.json");return ot.readJSONSync(e).version??"1.0.0"}var b=()=>{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 O=class extends Error{constructor(a){super(a)}};import N from"chalk";var l={error(...e){console.log(N.red(...e))},warn(...e){console.log(N.yellow(...e))},info(...e){console.log(N.cyan(...e))},success(...e){console.log(N.green(...e))}};var $=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e);var it=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;function be(e){let a=$(e),s=a.split("/"),o=s.findIndex(n=>n.startsWith("@")),t=s[s.length-1];return s.findIndex(n=>n.startsWith("@"))!==-1&&(t=s.slice(o).join("/")),a=="."||it.test(t??"")?!0:"App name must consist of only lowercase alphanumeric characters, '-', and '_'"}var ve=e=>e.startsWith(".")||e.startsWith("/")?"Import alias can't start with '.' or '/'":!0;var f={appName:C,packages:[],flags:{noGit:!1,noInstall:!1,default:!1,CI:!1,authentication:"none",orm:"none",formatter:"prettier",linter:"eslint",importAlias:"@/",dbProvider:"sqlite",backend:"nextjs"},databaseProvider:"sqlite"};async function we(){let e=f,a=new rt().name(F).description("CLI for scaffolding new web apps with the TNT-Powered stack").version(j(),"-v, --version","Output the current version of Create TNT 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 tnt-stack",!1).option("--CI","Boolean value if we're running in CI",!1).option("--backend [framework]",`Choose a backend framework to use. Possible values: ${f.flags.backend}`,f.flags.backend).option("--authProvider [provider]",`Choose an authentication provider to use. Possible values: ${ge.join(", ")}`,f.flags.authentication).option("--databaseORM [orm]",`Choose a database ORM to use. Possible values: ${he.join(", ")}`,f.flags.orm).option("--formatter [formatter]",`Choose a formatter to use. Possible values: ${f.flags.formatter}`,f.flags.formatter).option("--linter [linter]",`Choose a linter to use. Possible values: ${f.flags.linter}`,f.flags.linter).option("-i, --import-alias [alias]","Explicitly tell the CLI to use a custom import alias",f.flags.importAlias).option("--dbProvider [provider]",`Choose a database provider to use. Possible values: ${U.join(", ")}`,f.flags.dbProvider).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,
44
54
  and likely to result in a crash. Please run create-tnt-stack with another
45
55
  package manager such as pnpm, npm, or Yarn Classic.
46
- See: https://github.com/t3-oss/create-t3-app/issues/57`);let s=a.args[0];if(s&&(e.appName=s),e.flags=a.opts(),e.flags.CI){switch(e.packages=[],e.flags.backend){case"payload":e.packages.push("payload");break;default:break}switch(e.flags.authentication){case"authjs":e.packages.push("authjs");break;default:break}switch(e.flags.orm){case"prisma":e.packages.push("prisma");break;default:break}switch(e.flags.formatter){case"prettier":e.packages.push("prettier");break;default:break}switch(e.flags.linter){case"eslint":e.packages.push("eslint");break;default:break}return R.includes(e.flags.dbProvider)===!1&&(r.warn(`Incompatible database provided. Use: ${R.join(", ")}. Exiting.`),process.exit(0)),e.flags.backend==="payload"&&e.flags.dbProvider==="mysql"&&(r.warn("Payload CMS does not support MySQL. Exiting."),process.exit(0)),e}if(e.flags.default)return e;try{if(process.env.TERM_PROGRAM?.toLowerCase().includes("mintty"))throw r.warn(` WARNING: It looks like you are using MinTTY, which is non-interactive. This is most likely because you are
56
+ See: https://github.com/t3-oss/create-t3-app/issues/57`);let s=a.args[0];if(s&&(e.appName=s),e.flags=a.opts(),e.flags.CI){switch(e.packages=[],e.flags.backend){case"payload":e.packages.push("payload");break;default:break}switch(e.flags.authentication){case"authjs":e.packages.push("authjs");break;case"betterAuth":e.packages.push("betterAuth");break;default:break}switch(e.flags.orm){case"prisma":e.packages.push("prisma");break;default:break}switch(e.flags.formatter){case"prettier":e.packages.push("prettier");break;default:break}switch(e.flags.linter){case"eslint":e.packages.push("eslint");break;default:break}return U.includes(e.flags.dbProvider)===!1&&(l.warn(`Incompatible database provided. Use: ${U.join(", ")}. Exiting.`),process.exit(0)),e.flags.backend==="payload"&&e.flags.dbProvider==="mysql"&&(l.warn("Payload CMS does not support MySQL. Exiting."),process.exit(0)),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
47
57
  using Git Bash. If that's that case, please use Git Bash from another terminal, such as Windows Terminal. Alternatively, you
48
- can provide the arguments from the CLI directly: https://create.tntstack.org/getting-started#experimental-ci-flags to skip the prompts.`),new C("Non-interactive environment");let o=h(),t={};s||(t.name=await he({message:"What will your project be called?",default:j,validate:i=>ue(i)})),t.backend=await x({message:"What backend framework would you like to use?",choices:[{value:"nextjs",name:"Next.js"},{value:"payload",name:"Payload CMS"}],default:!m.flags.backend}),t.backend==="payload"&&(t.databaseProvider=await x({message:"What database provider would you like to use?",choices:[{value:"sqlite",name:"SQLite"},{value:"postgresql",name:"PostgreSQL"}],default:!m.flags.dbProvider})),t.backend==="nextjs"&&(t.authentication=await x({message:"What authentication provider would you like to use?",choices:[{value:"none",name:"None"},{value:"authjs",name:"Auth.js"}],default:!m.flags.authentication}),t.orm=await x({message:"What database ORM would you like to use?",choices:[{value:"none",name:"None"},{value:"prisma",name:"Prisma"}],default:!m.flags.orm}),t.orm!=="none"&&(t.databaseProvider=await x({message:"What database provider would you like to use?",choices:[{value:"sqlite",name:"SQLite"},{value:"mysql",name:"MySQL"},{value:"postgresql",name:"PostgreSQL"}],default:!m.flags.dbProvider}))),t.formatter=await x({message:"What formatter would you like to use?",choices:[{value:"prettier",name:"Prettier"}],default:!m.flags.formatter}),t.linter=await x({message:"What linter would you like to use?",choices:[{value:"eslint",name:"ESLint"}],default:!m.flags.linter}),e.flags.noGit||(t.noGit=await J({message:"Should we initialize a Git repository and stage the changes?",default:!m.flags.noGit})),e.flags.noInstall||(t.noInstall=await J({message:`Should we run '${o}`+(o==="yarn"?"'?":" install' for you?"),default:!m.flags.noInstall})),t.importAlias=await he({message:"What import alias would you like to use?",default:m.flags.importAlias,validate:ye});let n=[];switch(t.backend){case"payload":n.push("payload");break;default:break}switch(t.authentication){case"authjs":n.push("authjs");break;default:break}switch(t.orm){case"prisma":n.push("prisma");break;default:break}switch(t.formatter){case"prettier":n.push("prettier");break;default:break}switch(t.linter){case"eslint":n.push("eslint");break;default:break}return{appName:t.name??e.appName,packages:n,flags:{...e.flags,noGit:!t.noGit||e.flags.noGit,noInstall:!t.noInstall||e.flags.noInstall,importAlias:t.importAlias??e.flags.importAlias},databaseProvider:t.databaseProvider||"sqlite"}}catch(o){if(o instanceof C)r.warn(`${U} needs an interactive terminal to run.`),await J({message:"Continue scaffolding with default options?",default:!0})||(r.info("Exiting..."),process.exit(0)),r.info(`Scaffolding default tnt app in ./${e.appName}`);else throw o}return e}import lt from"path";import ve from"chalk";import st from"ora";function ke(e){let{packages:a}=e;r.info("Adding boilerplate...");for(let[s,o]of Object.entries(a))if(o.inUse){let t=st(`Boilerplating ${s}...`).start();o.installer(e),t.succeed(ve.green(`${ve.green.bold(s)}`))}r.info("")}import W from"path";import{confirm as ot,select as it}from"@inquirer/prompts";import I from"chalk";import O from"fs-extra";import rt from"ora";async function we({projectName:e,projectDir:a,pkgManager:s,noInstall:o}){let t=W.join(c,"template/base");o?r.info(""):r.info(`
58
+ can provide the arguments from the CLI directly: https://create.tntstack.org/getting-started#experimental-ci-flags to skip the prompts.`),new O("Non-interactive environment");let o=b(),t={};s||(t.name=await ke({message:"What will your project be called?",default:C,validate:i=>be(i)})),t.backend=await A({message:"What backend framework would you like to use?",choices:[{value:"nextjs",name:"Next.js"},{value:"payload",name:"Payload CMS"}],default:!f.flags.backend}),t.backend==="payload"&&(t.databaseProvider=await A({message:"What database provider would you like to use?",choices:[{value:"sqlite",name:"SQLite"},{value:"postgresql",name:"PostgreSQL"}],default:!f.flags.dbProvider})),t.backend==="nextjs"&&(t.authentication=await A({message:"What authentication provider would you like to use?",choices:[{value:"none",name:"None"},{value:"authjs",name:"Auth.js"},{value:"betterAuth",name:"Better Auth"}],default:!f.flags.authentication}),t.orm=await A({message:"What database ORM would you like to use?",choices:[{value:"none",name:"None"},{value:"prisma",name:"Prisma"}],default:!f.flags.orm}),t.orm!=="none"&&(t.databaseProvider=await A({message:"What database provider would you like to use?",choices:[{value:"sqlite",name:"SQLite"},{value:"mysql",name:"MySQL"},{value:"postgresql",name:"PostgreSQL"}],default:!f.flags.dbProvider}))),t.formatter=await A({message:"What formatter would you like to use?",choices:[{value:"prettier",name:"Prettier"}],default:!f.flags.formatter}),t.linter=await A({message:"What linter would you like to use?",choices:[{value:"eslint",name:"ESLint"}],default:!f.flags.linter}),e.flags.noGit||(t.noGit=await V({message:"Should we initialize a Git repository and stage the changes?",default:!f.flags.noGit})),e.flags.noInstall||(t.noInstall=await V({message:`Should we run '${o}`+(o==="yarn"?"'?":" install' for you?"),default:!f.flags.noInstall})),t.importAlias=await ke({message:"What import alias would you like to use?",default:f.flags.importAlias,validate:ve});let n=[];switch(t.backend){case"payload":n.push("payload");break;default:break}switch(t.authentication){case"authjs":n.push("authjs");break;case"betterAuth":n.push("betterAuth");break;default:break}switch(t.orm){case"prisma":n.push("prisma");break;default:break}switch(t.formatter){case"prettier":n.push("prettier");break;default:break}switch(t.linter){case"eslint":n.push("eslint");break;default:break}return{appName:t.name??e.appName,packages:n,flags:{...e.flags,noGit:!t.noGit||e.flags.noGit,noInstall:!t.noInstall||e.flags.noInstall,importAlias:t.importAlias??e.flags.importAlias},databaseProvider:t.databaseProvider||"sqlite"}}catch(o){if(o instanceof O)l.warn(`${F} needs an interactive terminal to run.`),await V({message:"Continue scaffolding with default options?",default:!0})||(l.info("Exiting..."),process.exit(0)),l.info(`Scaffolding default tnt app in ./${e.appName}`);else throw o}return e}import dt from"path";import _e from"chalk";import lt from"ora";function Pe(e){let{packages:a}=e;l.info("Adding boilerplate...");for(let[s,o]of Object.entries(a))if(o.inUse){let t=lt(`Boilerplating ${s}...`).start();o.installer(e),t.succeed(_e.green(`${_e.green.bold(s)}`))}l.info("")}import z from"path";import{confirm as pt,select as ct}from"@inquirer/prompts";import I from"chalk";import D from"fs-extra";import mt from"ora";async function Se({projectName:e,projectDir:a,pkgManager:s,noInstall:o}){let t=z.join(p,"template/base");o?l.info(""):l.info(`
49
59
  Using: ${I.cyan.bold(s)}
50
- `);let n=rt(`Scaffolding in: ${a}...
51
- `).start();if(O.existsSync(a))if(O.readdirSync(a).length===0)e!=="."&&n.info(`${I.cyan.bold(e)} exists but is empty, continuing...
52
- `);else{n.stopAndPersist();let l=await it({message:`${I.redBright.bold("Warning:")} ${I.cyan.bold(e)} already exists and isn't empty. How would you like to proceed?`,choices:[{value:"abort",name:"Abort installation (recommended)"},{value:"clear",name:"Clear the directory and continue installation"},{value:"overwrite",name:"Continue installation and overwrite conflicting files"}],default:"abort"});l==="abort"&&(n.fail("Aborting installation..."),process.exit(1)),await ot({message:`Are you sure you want to ${l==="clear"?"clear the directory":"overwrite conflicting files"}`,default:!1})||(n.fail("Aborting installation..."),process.exit(1)),l==="clear"&&(n.info(`Emptying ${I.cyan.bold(e)} and creating tnt app...
53
- `),O.emptyDirSync(a))}n.start(),O.copySync(t,a),O.renameSync(W.join(a,"_gitignore"),W.join(a,".gitignore"));let i=e==="."?"App":I.cyan.bold(e);n.succeed(`${i} ${I.green.bold("scaffolded successfully!")}
54
- `)}import v from"path";import B from"fs-extra";function Pe({packages:e,projectDir:a}){let s=v.join(c,"template/packages/src/app/layout"),o=e.payload.inUse,n=v.join(s,"base.tsx"),i=v.join(a,`src/app/${o?"(frontend)":""}/layout.tsx`);B.copySync(n,i)}function _e({packages:e,projectDir:a}){let s=v.join(c,"template/packages/src/app/page"),o=e.payload.inUse,t=e.authjs.inUse,n=e.prisma.inUse,i="base.tsx";o&&(i="with-payload.tsx"),t&&(i="with-authjs.tsx"),n&&(i="with-prisma.tsx"),t&&n&&(i="with-authjs-prisma.tsx");let l=v.join(s,i),d=v.join(a,`src/app/${o?"(frontend)":""}/page.tsx`);B.copySync(l,d)}function Se({packages:e,projectDir:a}){let s=v.join(c,"template/packages/src/app/globals"),o=e.payload.inUse,n=v.join(s,"base.css"),i=v.join(a,`src/app/${o?"(frontend)":""}/globals.css`);B.copySync(n,i)}async function xe({projectName:e,scopedAppName:a,packages:s,noInstall:o,databaseProvider:t}){let n=h(),i=lt.resolve(process.cwd(),e);return await we({projectName:e,projectDir:i,pkgManager:n,scopedAppName:a,noInstall:o,databaseProvider:t}),ke({projectName:e,scopedAppName:a,projectDir:i,pkgManager:n,packages:s,noInstall:o,databaseProvider:t}),Pe({packages:s,projectDir:i}),_e({packages:s,projectDir:i}),Se({packages:s,projectDir:i}),i}import{execSync as z}from"child_process";import V from"path";import{confirm as Ie}from"@inquirer/prompts";import T from"chalk";import{execa as D}from"execa";import Ae from"fs-extra";import pt from"ora";function ct(e){try{return z("git --version",{cwd:e}),!0}catch{return!1}}function K(e){return Ae.existsSync(V.join(e,".git"))}async function H(e){try{return await D("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}}function mt(){let a=z("git --version").toString().trim().split(" ")[2],s=a?.split(".")[0],o=a?.split(".")[1];return{major:Number(s),minor:Number(o)}}function dt(){return z("git config --global init.defaultBranch || echo main").toString().trim()}async function je(e){if(r.info("Initializing Git..."),!ct(e)){r.warn("Git is not installed. Skipping Git initialization.");return}let a=pt(`Creating a new git repo...
55
- `).start(),s=K(e),o=await H(e),t=V.parse(e).name;if(o&&s){if(a.stop(),!await Ie({message:`${T.redBright.bold("Warning:")} Git is already initialized in "${t}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,default:!1})){a.info("Skipping Git initialization.");return}Ae.removeSync(V.join(e,".git"))}else if(o&&!s&&(a.stop(),!await Ie({message:`${T.redBright.bold("Warning:")} "${t}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,default:!1}))){a.info("Skipping Git initialization.");return}try{let n=dt(),{major:i,minor:l}=mt();i<2||i==2&&l<28?(await D("git",["init"],{cwd:e}),await D("git",["symbolic-ref","HEAD",`refs/heads/${n}`],{cwd:e})):await D("git",["init",`--initial-branch=${n}`],{cwd:e}),await D("git",["add","."],{cwd:e}),a.succeed(`${T.green("Successfully initialized and staged")} ${T.green.bold("git")}
56
- `)}catch{a.fail(`${T.bold.red("Failed:")} could not initialize git. Update git to the latest version!
57
- `)}}import ft from"chalk";import{execa as Ce}from"execa";import Oe from"ora";var Y=async(e,a,s)=>{let{onDataHandle:o,args:t=["install"],stdout:n="pipe"}=s,i=Oe(`Running ${a} install...`).start(),l=Ce(a,t,{cwd:e,stdout:n});return await new Promise((d,g)=>{o&&l.stdout?.on("data",o(i)),l.on("error",u=>g(u)),l.on("close",()=>d())}),i},gt=async(e,a)=>{switch(e){case"npm":return await Ce(e,["install"],{cwd:a,stderr:"inherit"}),null;case"pnpm":return Y(a,e,{onDataHandle:s=>o=>{let t=o.toString();t.includes("Progress")&&(s.text=t.includes("|")?t.split(" | ")[1]??"":t)}});case"yarn":return Y(a,e,{onDataHandle:s=>o=>{s.text=o.toString()}});case"bun":return Y(a,e,{stdout:"ignore"})}},Te=async({projectDir:e})=>{r.info("Installing dependencies...");let a=h();(await gt(a,e)??Oe()).succeed(ft.green(`Successfully installed dependencies!
58
- `))};var De=async({projectName:e=j,packages:a,noInstall:s,projectDir:o,databaseProvider:t})=>{let n=h();r.info("Next steps:"),e!=="."&&r.info(` cd ${e}`),s&&(n==="yarn"?r.info(` ${n}`):r.info(` ${n} install`)),["postgresql","mysql"].includes(t)&&r.info(" Add your database connection string to .env"),a?.authjs.inUse&&r.info(" Fill in your .env with necessary values. See https://create.tntstack.org/first-steps for more info."),a?.prisma.inUse&&r.info(` Run "${n} db:push" to create you database tables.`),["npm"].includes(n)?r.info(` ${n} run dev`):r.info(` ${n} dev`),!await H(o)&&!K(o)&&r.info(" git init"),r.info(' git commit -m "initial commit"')};import N from"fs";import ut from"path";function Me(e,a,s){N.readdirSync(e).forEach(t=>{let n=ut.join(e,t);if(N.statSync(n).isDirectory())Me(n,a,s);else{let l=N.readFileSync(n,"utf8").replace(new RegExp(a,"g"),s);N.writeFileSync(n,l,"utf8")}})}function Re(e,a){let s=a.replace(/\*/g,"").replace(/[^\/]$/,"$&/");Me(e,"@/",s)}import Ee from"path";function $e(e){let s=$(e).split("/"),o=s[s.length-1];if(o==="."){let i=Ee.resolve(process.cwd());o=Ee.basename(i)}let t=s.findIndex(i=>i.startsWith("@"));s.findIndex(i=>i.startsWith("@"))!==-1&&(o=s.slice(t).join("/"));let n=s.filter(i=>!i.startsWith("@")).join("/");return[o,n]}import yt from"gradient-string";var ht={magenta:"#765bc8",pink:"#a48897",yellow:"#c7b561",green:"#8bb8a0",blue:"#4b97d5",cyan:"#22b6d2"};function Ne(){let e=yt(Object.values(ht)),a=h();(a==="yarn"||a==="pnpm")&&console.log(""),console.log(e.multiline(Z))}import{execSync as bt}from"child_process";import vt from"https";function Ue(e){let a=A();a.includes("beta")?(r.warn(" You are using a beta version of create-tnt-stack."),r.warn(" Please report any bugs you encounter.")):a!==e&&(r.warn(" You are using an outdated version of create-tnt-stack."),r.warn(" Your version:",a+".","Latest version in the npm registry:",e),r.warn(" Please run the CLI with @latest to get the latest updates.")),console.log("")}function kt(){return new Promise((e,a)=>{vt.get("https://registry.npmjs.org/-/package/tnt-stack/dist-tags",s=>{if(s.statusCode===200){let o="";s.on("data",t=>o+=t),s.on("end",()=>{e(JSON.parse(o).latest)})}else a()}).on("error",()=>{a()})})}var Ge=()=>kt().catch(()=>{try{return bt("npm view create-tnt-stack version").toString().trim()}catch{return null}});async function Pt(){let e=await Ge(),a=h();Ne(),e&&Ue(e);let{appName:s,packages:o,flags:{noGit:t,noInstall:n,importAlias:i},databaseProvider:l}=await be(),d=ge(o),[g,u]=$e(s),p=await xe({projectName:u,scopedAppName:g,packages:d,noInstall:n,databaseProvider:l}),b=Le.readJsonSync(Fe.join(p,"package.json"));b.name=g,b.ctntaMetadata={initVersion:A()};let{stdout:_}=await wt(a,["-v"],{cwd:p});b.packageManager=`${a}@${_.trim()}`,Le.writeJSONSync(Fe.join(p,"package.json"),b,{spaces:2}),i!=="@/"&&Re(p,i),n||await Te({projectDir:p}),t||await je(p),await De({projectDir:p,projectName:u,packages:d,noInstall:n,databaseProvider:l}),process.exit(0)}Pt().catch(e=>{r.error("Aborting installation..."),e instanceof Error?r.error(e.message):(r.error("An unknown error occurred. Please open an issue on GitHub with the below:"),console.error(e)),process.exit(1)});
60
+ `);let n=mt(`Scaffolding in: ${a}...
61
+ `).start();if(D.existsSync(a))if(D.readdirSync(a).length===0)e!=="."&&n.info(`${I.cyan.bold(e)} exists but is empty, continuing...
62
+ `);else{n.stopAndPersist();let r=await ct({message:`${I.redBright.bold("Warning:")} ${I.cyan.bold(e)} already exists and isn't empty. How would you like to proceed?`,choices:[{value:"abort",name:"Abort installation (recommended)"},{value:"clear",name:"Clear the directory and continue installation"},{value:"overwrite",name:"Continue installation and overwrite conflicting files"}],default:"abort"});r==="abort"&&(n.fail("Aborting installation..."),process.exit(1)),await pt({message:`Are you sure you want to ${r==="clear"?"clear the directory":"overwrite conflicting files"}`,default:!1})||(n.fail("Aborting installation..."),process.exit(1)),r==="clear"&&(n.info(`Emptying ${I.cyan.bold(e)} and creating tnt app...
63
+ `),D.emptyDirSync(a))}n.start(),D.copySync(t,a),D.renameSync(z.join(a,"_gitignore"),z.join(a,".gitignore"));let i=e==="."?"App":I.cyan.bold(e);n.succeed(`${i} ${I.green.bold("scaffolded successfully!")}
64
+ `)}import k from"path";import H from"fs-extra";function xe({packages:e,projectDir:a}){let s=k.join(p,"template/packages/src/app/layout"),o=e.payload.inUse,n=k.join(s,"base.tsx"),i=k.join(a,`src/app/${o?"(frontend)":""}/layout.tsx`);H.copySync(n,i)}function Ae({packages:e,projectDir:a}){let s=k.join(p,"template/packages/src/app/page"),o=e.payload.inUse,t=e.authjs.inUse,n=e.betterAuth.inUse,i=e.prisma.inUse,r="base.tsx";o&&(r="with-payload.tsx"),t&&(r="with-authjs.tsx"),n&&(r="with-better-auth.tsx"),i&&(r="with-prisma.tsx"),t&&i&&(r="with-authjs-prisma.tsx"),n&&i&&(r="with-better-auth-prisma.tsx");let m=k.join(s,r),u=k.join(a,`src/app/${o?"(frontend)":""}/page.tsx`);H.copySync(m,u)}function Ie({packages:e,projectDir:a}){let s=k.join(p,"template/packages/src/app/globals"),o=e.payload.inUse,n=k.join(s,"base.css"),i=k.join(a,`src/app/${o?"(frontend)":""}/globals.css`);H.copySync(n,i)}async function je({projectName:e,scopedAppName:a,packages:s,noInstall:o,databaseProvider:t}){let n=b(),i=dt.resolve(process.cwd(),e);return await Se({projectName:e,projectDir:i,pkgManager:n,scopedAppName:a,noInstall:o,databaseProvider:t}),Pe({projectName:e,scopedAppName:a,projectDir:i,pkgManager:n,packages:s,noInstall:o,databaseProvider:t}),xe({packages:s,projectDir:i}),Ae({packages:s,projectDir:i}),Ie({packages:s,projectDir:i}),i}import{execSync as Y}from"child_process";import K from"path";import{confirm as Ce}from"@inquirer/prompts";import E from"chalk";import{execa as R}from"execa";import Te from"fs-extra";import ft from"ora";function ut(e){try{return Y("git --version",{cwd:e}),!0}catch{return!1}}function Q(e){return Te.existsSync(K.join(e,".git"))}async function X(e){try{return await R("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}}function gt(){let a=Y("git --version").toString().trim().split(" ")[2],s=a?.split(".")[0],o=a?.split(".")[1];return{major:Number(s),minor:Number(o)}}function ht(){return Y("git config --global init.defaultBranch || echo main").toString().trim()}async function Oe(e){if(l.info("Initializing Git..."),!ut(e)){l.warn("Git is not installed. Skipping Git initialization.");return}let a=ft(`Creating a new git repo...
65
+ `).start(),s=Q(e),o=await X(e),t=K.parse(e).name;if(o&&s){if(a.stop(),!await Ce({message:`${E.redBright.bold("Warning:")} Git is already initialized in "${t}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,default:!1})){a.info("Skipping Git initialization.");return}Te.removeSync(K.join(e,".git"))}else if(o&&!s&&(a.stop(),!await Ce({message:`${E.redBright.bold("Warning:")} "${t}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,default:!1}))){a.info("Skipping Git initialization.");return}try{let n=ht(),{major:i,minor:r}=gt();i<2||i==2&&r<28?(await R("git",["init"],{cwd:e}),await R("git",["symbolic-ref","HEAD",`refs/heads/${n}`],{cwd:e})):await R("git",["init",`--initial-branch=${n}`],{cwd:e}),await R("git",["add","."],{cwd:e}),a.succeed(`${E.green("Successfully initialized and staged")} ${E.green.bold("git")}
66
+ `)}catch{a.fail(`${E.bold.red("Failed:")} could not initialize git. Update git to the latest version!
67
+ `)}}import yt from"chalk";import{execa as De}from"execa";import Ee from"ora";var Z=async(e,a,s)=>{let{onDataHandle:o,args:t=["install"],stdout:n="pipe"}=s,i=Ee(`Running ${a} install...`).start(),r=De(a,t,{cwd:e,stdout:n});return await new Promise((m,u)=>{o&&r.stdout?.on("data",o(i)),r.on("error",h=>u(h)),r.on("close",()=>m())}),i},bt=async(e,a)=>{switch(e){case"npm":return await De(e,["install"],{cwd:a,stderr:"inherit"}),null;case"pnpm":return Z(a,e,{onDataHandle:s=>o=>{let t=o.toString();t.includes("Progress")&&(s.text=t.includes("|")?t.split(" | ")[1]??"":t)}});case"yarn":return Z(a,e,{onDataHandle:s=>o=>{s.text=o.toString()}});case"bun":return Z(a,e,{stdout:"ignore"})}},Re=async({projectDir:e})=>{l.info("Installing dependencies...");let a=b();(await bt(a,e)??Ee()).succeed(yt.green(`Successfully installed dependencies!
68
+ `))};var Me=async({projectName:e=C,packages:a,noInstall:s,projectDir:o,databaseProvider:t})=>{let n=b();l.info("Next steps:"),e!=="."&&l.info(` cd ${e}`),s&&(n==="yarn"?l.info(` ${n}`):l.info(` ${n} install`)),["postgresql","mysql"].includes(t)&&l.info(" Add your database connection string to .env"),a?.authjs.inUse&&l.info(" Fill in your .env with necessary values. See https://create.tntstack.org/first-steps for more info."),a?.betterAuth.inUse&&l.info(" Fill in your .env with necessary values. See https://create.tntstack.org/first-steps for more info."),a?.prisma.inUse&&l.info(` Run "${n} db:push" to create you database tables.`),["npm"].includes(n)?l.info(` ${n} run dev`):l.info(` ${n} dev`),!await X(o)&&!Q(o)&&l.info(" git init"),l.info(' git commit -m "initial commit"')};import G from"fs";import vt from"path";function Ue(e,a,s){G.readdirSync(e).forEach(t=>{let n=vt.join(e,t);if(G.statSync(n).isDirectory())Ue(n,a,s);else{let r=G.readFileSync(n,"utf8").replace(new RegExp(a,"g"),s);G.writeFileSync(n,r,"utf8")}})}function Ne(e,a){let s=a.replace(/\*/g,"").replace(/[^\/]$/,"$&/");Ue(e,"@/",s)}import $e from"path";function Ge(e){let s=$(e).split("/"),o=s[s.length-1];if(o==="."){let i=$e.resolve(process.cwd());o=$e.basename(i)}let t=s.findIndex(i=>i.startsWith("@"));s.findIndex(i=>i.startsWith("@"))!==-1&&(o=s.slice(t).join("/"));let n=s.filter(i=>!i.startsWith("@")).join("/");return[o,n]}import kt from"gradient-string";var wt={magenta:"#765bc8",pink:"#a48897",yellow:"#c7b561",green:"#8bb8a0",blue:"#4b97d5",cyan:"#22b6d2"};function qe(){let e=kt(Object.values(wt)),a=b();(a==="yarn"||a==="pnpm")&&console.log(""),console.log(e.multiline(te))}import{execSync as _t}from"child_process";import Pt from"https";function Fe(e){let a=j();a.includes("beta")?(l.warn(" You are using a beta version of create-tnt-stack."),l.warn(" Please report any bugs you encounter.")):a!==e&&(l.warn(" You are using an outdated version of create-tnt-stack."),l.warn(" Your version:",a+".","Latest version in the npm registry:",e),l.warn(" Please run the CLI with @latest to get the latest updates.")),console.log("")}function St(){return new Promise((e,a)=>{Pt.get("https://registry.npmjs.org/-/package/tnt-stack/dist-tags",s=>{if(s.statusCode===200){let o="";s.on("data",t=>o+=t),s.on("end",()=>{e(JSON.parse(o).latest)})}else a()}).on("error",()=>{a()})})}var Le=()=>St().catch(()=>{try{return _t("npm view create-tnt-stack version").toString().trim()}catch{return null}});async function At(){let e=await Le(),a=b();qe(),e&&Fe(e);let{appName:s,packages:o,flags:{noGit:t,noInstall:n,importAlias:i},databaseProvider:r}=await we(),m=ye(o),[u,h]=Ge(s),d=await je({projectName:h,scopedAppName:u,packages:m,noInstall:n,databaseProvider:r}),c=Je.readJsonSync(Be.join(d,"package.json"));c.name=u,c.ctntaMetadata={initVersion:j()};let{stdout:v}=await xt(a,["-v"],{cwd:d});c.packageManager=`${a}@${v.trim()}`,Je.writeJSONSync(Be.join(d,"package.json"),c,{spaces:2}),i!=="@/"&&Ne(d,i),n||await Re({projectDir:d}),t||await Oe(d),await Me({projectDir:d,projectName:h,packages:m,noInstall:n,databaseProvider:r}),process.exit(0)}At().catch(e=>{l.error("Aborting installation..."),e instanceof Error?l.error(e.message):(l.error("An unknown error occurred. Please open an issue on GitHub with the below:"),console.error(e)),process.exit(1)});
package/package.json CHANGED
@@ -1,89 +1,92 @@
1
1
  {
2
- "name": "create-tnt-stack",
3
- "version": "0.5.2",
4
- "description": "Create web application with the TNT-Powered stack",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/slickyeet/create-tnt-stack",
9
- "directory": "cli"
10
- },
11
- "keywords": [
12
- "create-tnt-stack",
13
- "tnt-stack",
14
- "typescript",
15
- "next.js",
16
- "tailwind"
17
- ],
18
- "type": "module",
19
- "exports": "./dist/index.js",
20
- "bin": {
21
- "create-tnt-stack": "./dist/index.js"
22
- },
23
- "files": [
24
- "dist",
25
- "template",
26
- "README.md",
27
- "LICENSE",
28
- "package.json"
29
- ],
30
- "engines": {
31
- "node": ">=18"
32
- },
33
- "scripts": {
34
- "dev": "tsup --watch",
35
- "build": "tsup",
36
- "start": "node dist/index.js",
37
- "format": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern --write",
38
- "format:check": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern --check",
39
- "typecheck": "tsc",
40
- "clean": "rm -rf dist .turbo node_modules",
41
- "link": "bun run build && npm link",
42
- "release": "changeset version",
43
- "pub:beta": "bun run build && npm publish --tag beta",
44
- "pub:release": "bun run build && npm publish"
45
- },
46
- "dependencies": {
47
- "@inquirer/prompts": "^7.4.0",
48
- "chalk": "^5.4.1",
49
- "commander": "^13.1.0",
50
- "execa": "^9.5.2",
51
- "fs-extra": "^11.3.0",
52
- "gradient-string": "^3.0.0",
53
- "ora": "^8.2.0",
54
- "sort-package-json": "^3.0.0"
55
- },
56
- "devDependencies": {
57
- "@auth/prisma-adapter": "^2.8.0",
58
- "@eslint/eslintrc": "^3.3.1",
59
- "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
60
- "@payloadcms/db-sqlite": "^3.33.0",
61
- "@payloadcms/db-vercel-postgres": "^3.33.0",
62
- "@payloadcms/next": "^3.33.0",
63
- "@payloadcms/payload-cloud": "^3.33.0",
64
- "@payloadcms/richtext-lexical": "^3.33.0",
65
- "@prisma/client": "^6.5.0",
66
- "@t3-oss/env-nextjs": "^0.12.0",
67
- "@types/fs-extra": "^11.0.4",
68
- "@types/node": "^22",
69
- "@types/react": "^19",
70
- "@types/react-dom": "^19",
71
- "eslint": "^9.22.0",
72
- "eslint-config-next": "^15.2.4",
73
- "graphql": "^16.10.0",
74
- "next": "^15.2.4",
75
- "next-auth": "^5.0.0-beta.25",
76
- "payload": "^3.33.0",
77
- "prettier": "^3.5.3",
78
- "prettier-plugin-tailwindcss": "^0.6.11",
79
- "prisma": "^6.5.0",
80
- "react": "^19.0.0",
81
- "react-dom": "^19.0.0",
82
- "sharp": "^0.34.1",
83
- "tailwindcss": "^4.0.17",
84
- "tsup": "^8.4.0",
85
- "type-fest": "^4.37.0",
86
- "typescript": "^5.8.2",
87
- "zod": "^3.24.2"
88
- }
2
+ "name": "create-tnt-stack",
3
+ "version": "0.5.3-beta.253ed67",
4
+ "description": "Create web application with the TNT-Powered stack",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/slickyeet/create-tnt-stack",
9
+ "directory": "cli"
10
+ },
11
+ "keywords": [
12
+ "create-tnt-stack",
13
+ "tnt-stack",
14
+ "typescript",
15
+ "next.js",
16
+ "tailwind"
17
+ ],
18
+ "type": "module",
19
+ "exports": "./dist/index.js",
20
+ "bin": {
21
+ "create-tnt-stack": "./dist/index.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "template",
26
+ "README.md",
27
+ "LICENSE",
28
+ "package.json"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "scripts": {
34
+ "dev": "tsup --watch",
35
+ "build": "tsup",
36
+ "start": "node dist/index.js",
37
+ "format": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern --write",
38
+ "format:check": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern --check",
39
+ "typecheck": "tsc",
40
+ "clean": "rm -rf dist .turbo node_modules",
41
+ "link": "bun run build && npm link",
42
+ "release": "changeset version",
43
+ "pub:beta": "bun run build && npm publish --tag beta",
44
+ "pub:release": "bun run build && npm publish"
45
+ },
46
+ "dependencies": {
47
+ "@inquirer/prompts": "^7.4.0",
48
+ "better-auth": "^1.2.7",
49
+ "better-sqlite3": "^11.9.1",
50
+ "chalk": "^5.4.1",
51
+ "commander": "^13.1.0",
52
+ "execa": "^9.5.2",
53
+ "fs-extra": "^11.3.0",
54
+ "gradient-string": "^3.0.0",
55
+ "ora": "^8.2.0",
56
+ "sort-package-json": "^3.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@auth/prisma-adapter": "^2.8.0",
60
+ "@eslint/eslintrc": "^3.3.1",
61
+ "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
62
+ "@payloadcms/db-sqlite": "^3.33.0",
63
+ "@payloadcms/db-vercel-postgres": "^3.33.0",
64
+ "@payloadcms/next": "^3.33.0",
65
+ "@payloadcms/payload-cloud": "^3.33.0",
66
+ "@payloadcms/richtext-lexical": "^3.33.0",
67
+ "@prisma/client": "^6.5.0",
68
+ "@t3-oss/env-nextjs": "^0.12.0",
69
+ "@types/better-sqlite3": "^7.6.13",
70
+ "@types/fs-extra": "^11.0.4",
71
+ "@types/node": "^22",
72
+ "@types/react": "^19",
73
+ "@types/react-dom": "^19",
74
+ "eslint": "^9.22.0",
75
+ "eslint-config-next": "^15.2.4",
76
+ "graphql": "^16.10.0",
77
+ "next": "^15.2.4",
78
+ "next-auth": "^5.0.0-beta.25",
79
+ "payload": "^3.33.0",
80
+ "prettier": "^3.5.3",
81
+ "prettier-plugin-tailwindcss": "^0.6.11",
82
+ "prisma": "^6.5.0",
83
+ "react": "^19.0.0",
84
+ "react-dom": "^19.0.0",
85
+ "sharp": "^0.34.1",
86
+ "tailwindcss": "^4.0.17",
87
+ "tsup": "^8.4.0",
88
+ "type-fest": "^4.37.0",
89
+ "typescript": "^5.8.2",
90
+ "zod": "^3.24.2"
91
+ }
89
92
  }
@@ -0,0 +1,85 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "sqlite"
10
+ url = env("DATABASE_URL")
11
+ }
12
+
13
+ model Post {
14
+ id String @id @default(cuid())
15
+ name String
16
+
17
+ createdBy User @relation(fields: [createdById], references: [id])
18
+ createdById String
19
+
20
+ createdAt DateTime @default(now())
21
+ updatedAt DateTime @updatedAt
22
+
23
+ @@index([name])
24
+ }
25
+
26
+ model User {
27
+ id String @id
28
+ name String
29
+ email String
30
+ emailVerified Boolean
31
+ image String?
32
+ createdAt DateTime
33
+ updatedAt DateTime
34
+ sessions Session[]
35
+ accounts Account[]
36
+ Post Post[]
37
+
38
+ @@unique([email])
39
+ @@map("user")
40
+ }
41
+
42
+ model Session {
43
+ id String @id
44
+ expiresAt DateTime
45
+ token String
46
+ createdAt DateTime
47
+ updatedAt DateTime
48
+ ipAddress String?
49
+ userAgent String?
50
+ userId String
51
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
52
+
53
+ @@unique([token])
54
+ @@map("session")
55
+ }
56
+
57
+ model Account {
58
+ id String @id
59
+ accountId String
60
+ providerId String
61
+ userId String
62
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
63
+ accessToken String?
64
+ refreshToken String?
65
+ idToken String?
66
+ accessTokenExpiresAt DateTime?
67
+ refreshTokenExpiresAt DateTime?
68
+ scope String?
69
+ password String?
70
+ createdAt DateTime
71
+ updatedAt DateTime
72
+
73
+ @@map("account")
74
+ }
75
+
76
+ model Verification {
77
+ id String @id
78
+ identifier String
79
+ value String
80
+ expiresAt DateTime
81
+ createdAt DateTime?
82
+ updatedAt DateTime?
83
+
84
+ @@map("verification")
85
+ }
@@ -0,0 +1,5 @@
1
+ import { toNextJsHandler } from "better-auth/next-js"
2
+
3
+ import { auth } from "@/server/auth"
4
+
5
+ export const { GET, POST } = toNextJsHandler(auth.handler)
@@ -0,0 +1,245 @@
1
+ import { fileURLToPath } from "url"
2
+ import { revalidatePath } from "next/cache"
3
+ import { headers } from "next/headers"
4
+ import { redirect } from "next/navigation"
5
+
6
+ import { auth } from "@/server/auth"
7
+ import { db } from "@/server/db"
8
+
9
+ export default async function HomePage() {
10
+ const session = await auth.api.getSession({
11
+ headers: await headers(),
12
+ })
13
+ const user = session?.user
14
+
15
+ const posts = await db.post.findMany()
16
+
17
+ const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
18
+
19
+ return (
20
+ <main className="mx-auto flex h-screen max-w-5xl flex-col items-center justify-between overflow-hidden p-6 sm:p-[45px]">
21
+ <header className="ml-auto">
22
+ {user ? (
23
+ <button
24
+ onClick={async () => {
25
+ "use server"
26
+ await auth.api.signOut({
27
+ headers: await headers(),
28
+ })
29
+ }}
30
+ className="cursor-pointer rounded-md bg-rose-400 px-4 py-2"
31
+ >
32
+ Sign Out
33
+ </button>
34
+ ) : (
35
+ <button
36
+ onClick={async () => {
37
+ "use server"
38
+ const response = await auth.api.signInSocial({
39
+ body: {
40
+ provider: "discord",
41
+ },
42
+ })
43
+ if (!response) {
44
+ throw new Error("Failed to sign in")
45
+ }
46
+ if (response.url) {
47
+ redirect(response.url)
48
+ }
49
+ }}
50
+ className="cursor-pointer rounded-md bg-purple-400 px-4 py-2"
51
+ >
52
+ Sign In
53
+ </button>
54
+ )}
55
+ </header>
56
+
57
+ <div className="flex grow flex-col items-center justify-center">
58
+ {/* Logo */}
59
+ <picture className="relative">
60
+ <div className="absolute inset-0 animate-pulse rounded-xl bg-gradient-to-r from-purple-500 to-cyan-500 opacity-20 blur-xl dark:from-purple-800 dark:to-cyan-800" />
61
+ <source srcSet="https://github.com/SlickYeet/create-tnt-stack/blob/main/docs/public/logo.light.png?raw=true" />
62
+ <img
63
+ src="https://github.com/SlickYeet/create-tnt-stack/blob/main/docs/public/logo.light.png?raw=true"
64
+ alt="Logo"
65
+ width={65}
66
+ height={65}
67
+ className="block h-auto max-w-full"
68
+ />
69
+ </picture>
70
+
71
+ {user ? (
72
+ <h1 className="mt-6 bg-gradient-to-r from-purple-500 to-cyan-500 bg-clip-text text-center text-4xl leading-10 text-transparent sm:text-5xl sm:leading-14 md:text-6xl md:leading-20 lg:mt-10 lg:text-7xl lg:font-bold">
73
+ Welcome, <span className="capitalize">{user.name}</span>!
74
+ </h1>
75
+ ) : (
76
+ <>
77
+ <h1 className="mt-6 bg-gradient-to-r from-purple-500 to-cyan-500 bg-clip-text text-center text-4xl leading-10 text-transparent sm:text-5xl sm:leading-14 md:text-6xl md:leading-20 lg:mt-10 lg:text-7xl lg:font-bold">
78
+ TNT-Powered Next.js App
79
+ </h1>
80
+ <p className="mt-4 text-center text-lg text-neutral-700 md:text-xl lg:mt-6 dark:text-neutral-300">
81
+ Build modern web applications with today&apos;s most popular tools
82
+ </p>
83
+ </>
84
+ )}
85
+
86
+ <div className="mt-12 flex items-center gap-3">
87
+ <a
88
+ href="https://create.tntstack.org"
89
+ target="_blank"
90
+ rel="noopener noreferrer"
91
+ className="flex items-center rounded-md border border-white px-2 py-1 outline-none focus:opacity-80 active:opacity-70"
92
+ >
93
+ Website
94
+ <svg
95
+ xmlns="http://www.w3.org/2000/svg"
96
+ viewBox="0 0 24 24"
97
+ strokeLinecap="round"
98
+ strokeLinejoin="round"
99
+ className="mb-1.5 size-4 fill-none stroke-current stroke-2"
100
+ >
101
+ <path d="M7 7h10v10" />
102
+ <path d="M7 17 17 7" />
103
+ </svg>
104
+ </a>
105
+ <a
106
+ href="https://create.tntstack.org/introduction"
107
+ target="_blank"
108
+ rel="noopener noreferrer"
109
+ className="flex items-center rounded-md border border-white px-2 py-1 outline-none focus:opacity-80 active:opacity-70"
110
+ >
111
+ Docs
112
+ <svg
113
+ xmlns="http://www.w3.org/2000/svg"
114
+ viewBox="0 0 24 24"
115
+ strokeLinecap="round"
116
+ strokeLinejoin="round"
117
+ className="mb-1.5 size-4 fill-none stroke-current stroke-2"
118
+ >
119
+ <path d="M7 7h10v10" />
120
+ <path d="M7 17 17 7" />
121
+ </svg>
122
+ </a>
123
+ <a
124
+ href="https://github.com/SlickYeet/create-tnt-stack"
125
+ target="_blank"
126
+ rel="noopener noreferrer"
127
+ className="flex items-center rounded-md border border-white px-2 py-1 outline-none focus:opacity-80 active:opacity-70"
128
+ >
129
+ GitHub
130
+ <svg
131
+ xmlns="http://www.w3.org/2000/svg"
132
+ viewBox="0 0 24 24"
133
+ strokeLinecap="round"
134
+ strokeLinejoin="round"
135
+ className="mb-1.5 size-4 fill-none stroke-current stroke-2"
136
+ >
137
+ <path d="M7 7h10v10" />
138
+ <path d="M7 17 17 7" />
139
+ </svg>
140
+ </a>
141
+ </div>
142
+
143
+ <div className="mt-12 flex flex-col items-center gap-3">
144
+ <div className="mb-4">
145
+ <h1 className="mb-4 text-center">
146
+ <span className="text-2xl text-neutral-700 dark:text-neutral-300">
147
+ Posts {posts.length}
148
+ </span>
149
+ </h1>
150
+
151
+ {user && (
152
+ <form
153
+ action={async (formData: FormData) => {
154
+ "use server"
155
+
156
+ if (!user) throw new Error("Unauthorized")
157
+
158
+ const name =
159
+ formData.get("name")?.toString() ||
160
+ `New Post ${posts.length + 1}`
161
+
162
+ await db.post.create({
163
+ data: {
164
+ id: crypto.randomUUID(),
165
+ name,
166
+ createdBy: {
167
+ connect: {
168
+ id: user.id,
169
+ },
170
+ },
171
+ },
172
+ })
173
+
174
+ revalidatePath("/")
175
+ }}
176
+ >
177
+ <input
178
+ type="text"
179
+ name="name"
180
+ placeholder="New Post"
181
+ className="h-8 rounded-md border border-neutral-300 px-2 outline-none dark:border-neutral-700 dark:bg-neutral-800"
182
+ />
183
+ <button
184
+ type="submit"
185
+ className="ml-2 size-8 cursor-pointer rounded-md bg-neutral-200 outline-none hover:opacity-80 focus:opacity-80 dark:bg-neutral-800"
186
+ >
187
+ +
188
+ </button>
189
+ </form>
190
+ )}
191
+ </div>
192
+
193
+ <div className="grid w-full grid-cols-1 gap-2 space-y-2 sm:grid-cols-2">
194
+ {posts.map((post) => (
195
+ <div
196
+ key={post.id}
197
+ className="flex h-10 max-w-40 items-center rounded-md bg-neutral-200 px-2 py-1 dark:bg-neutral-800"
198
+ >
199
+ <span className="truncate text-sm text-neutral-700 dark:text-neutral-300">
200
+ {post.name}
201
+ </span>
202
+ {user && (
203
+ <form
204
+ action={async () => {
205
+ "use server"
206
+
207
+ if (!user) throw new Error("Unauthorized")
208
+
209
+ await db.post.delete({
210
+ where: {
211
+ id: post.id,
212
+ createdById: user.id,
213
+ },
214
+ })
215
+
216
+ revalidatePath("/")
217
+ }}
218
+ className="ml-auto"
219
+ >
220
+ <button
221
+ type="submit"
222
+ className="ml-2 cursor-pointer rounded-md text-rose-500 outline-none hover:opacity-80 focus:opacity-80"
223
+ >
224
+ x
225
+ </button>
226
+ </form>
227
+ )}
228
+ </div>
229
+ ))}
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <div className="flex flex-col items-center gap-1 text-sm text-neutral-600 lg:flex-row lg:gap-2 dark:text-neutral-400">
235
+ <p className="m-0">Get started by editing </p>
236
+ <a
237
+ href={fileURL}
238
+ className="rounded-md bg-neutral-200 px-2 py-1 dark:bg-neutral-800"
239
+ >
240
+ <code>src/app/page.tsx</code>
241
+ </a>
242
+ </div>
243
+ </main>
244
+ )
245
+ }
@@ -0,0 +1,151 @@
1
+ import { fileURLToPath } from "url"
2
+ import { headers } from "next/headers"
3
+ import { redirect } from "next/navigation"
4
+
5
+ import { auth } from "@/server/auth"
6
+
7
+ export default async function HomePage() {
8
+ const session = await auth.api.getSession({
9
+ headers: await headers(),
10
+ })
11
+ const user = session?.user
12
+
13
+ const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
14
+
15
+ return (
16
+ <main className="mx-auto flex h-screen max-w-5xl flex-col items-center justify-between overflow-hidden p-6 sm:p-[45px]">
17
+ <header className="ml-auto">
18
+ {user ? (
19
+ <button
20
+ onClick={async () => {
21
+ "use server"
22
+ await auth.api.signOut({
23
+ headers: await headers(),
24
+ })
25
+ }}
26
+ className="cursor-pointer rounded-md bg-rose-400 px-4 py-2"
27
+ >
28
+ Sign Out
29
+ </button>
30
+ ) : (
31
+ <button
32
+ onClick={async () => {
33
+ "use server"
34
+ const response = await auth.api.signInSocial({
35
+ body: {
36
+ provider: "discord",
37
+ },
38
+ })
39
+ if (!response) {
40
+ throw new Error("Failed to sign in")
41
+ }
42
+ if (response.url) {
43
+ redirect(response.url)
44
+ }
45
+ }}
46
+ className="cursor-pointer rounded-md bg-purple-400 px-4 py-2"
47
+ >
48
+ Sign In
49
+ </button>
50
+ )}
51
+ </header>
52
+
53
+ <div className="flex grow flex-col items-center justify-center">
54
+ {/* Logo */}
55
+ <picture className="relative">
56
+ <div className="absolute inset-0 animate-pulse rounded-xl bg-gradient-to-r from-purple-500 to-cyan-500 opacity-20 blur-xl dark:from-purple-800 dark:to-cyan-800" />
57
+ <source srcSet="https://github.com/SlickYeet/create-tnt-stack/blob/main/docs/public/logo.light.png?raw=true" />
58
+ <img
59
+ src="https://github.com/SlickYeet/create-tnt-stack/blob/main/docs/public/logo.light.png?raw=true"
60
+ alt="Logo"
61
+ width={65}
62
+ height={65}
63
+ className="block h-auto max-w-full"
64
+ />
65
+ </picture>
66
+
67
+ {user ? (
68
+ <h1 className="mt-6 bg-gradient-to-r from-purple-500 to-cyan-500 bg-clip-text text-center text-4xl leading-10 text-transparent sm:text-5xl sm:leading-14 md:text-6xl md:leading-20 lg:mt-10 lg:text-7xl lg:font-bold">
69
+ Welcome, <span className="capitalize">{user.name}</span>!
70
+ </h1>
71
+ ) : (
72
+ <>
73
+ <h1 className="mt-6 bg-gradient-to-r from-purple-500 to-cyan-500 bg-clip-text text-center text-4xl leading-10 text-transparent sm:text-5xl sm:leading-14 md:text-6xl md:leading-20 lg:mt-10 lg:text-7xl lg:font-bold">
74
+ TNT-Powered Next.js App
75
+ </h1>
76
+ <p className="mt-4 text-center text-lg text-neutral-700 md:text-xl lg:mt-6 dark:text-neutral-300">
77
+ Build modern web applications with today&apos;s most popular tools
78
+ </p>
79
+ </>
80
+ )}
81
+
82
+ <div className="mt-12 flex items-center gap-3">
83
+ <a
84
+ href="https://create.tntstack.org"
85
+ target="_blank"
86
+ rel="noopener noreferrer"
87
+ className="flex items-center rounded-md border border-white px-2 py-1 outline-none focus:opacity-80 active:opacity-70"
88
+ >
89
+ Website
90
+ <svg
91
+ xmlns="http://www.w3.org/2000/svg"
92
+ viewBox="0 0 24 24"
93
+ strokeLinecap="round"
94
+ strokeLinejoin="round"
95
+ className="mb-1.5 size-4 fill-none stroke-current stroke-2"
96
+ >
97
+ <path d="M7 7h10v10" />
98
+ <path d="M7 17 17 7" />
99
+ </svg>
100
+ </a>
101
+ <a
102
+ href="https://create.tntstack.org/introduction"
103
+ target="_blank"
104
+ rel="noopener noreferrer"
105
+ className="flex items-center rounded-md border border-white px-2 py-1 outline-none focus:opacity-80 active:opacity-70"
106
+ >
107
+ Docs
108
+ <svg
109
+ xmlns="http://www.w3.org/2000/svg"
110
+ viewBox="0 0 24 24"
111
+ strokeLinecap="round"
112
+ strokeLinejoin="round"
113
+ className="mb-1.5 size-4 fill-none stroke-current stroke-2"
114
+ >
115
+ <path d="M7 7h10v10" />
116
+ <path d="M7 17 17 7" />
117
+ </svg>
118
+ </a>
119
+ <a
120
+ href="https://github.com/SlickYeet/create-tnt-stack"
121
+ target="_blank"
122
+ rel="noopener noreferrer"
123
+ className="flex items-center rounded-md border border-white px-2 py-1 outline-none focus:opacity-80 active:opacity-70"
124
+ >
125
+ GitHub
126
+ <svg
127
+ xmlns="http://www.w3.org/2000/svg"
128
+ viewBox="0 0 24 24"
129
+ strokeLinecap="round"
130
+ strokeLinejoin="round"
131
+ className="mb-1.5 size-4 fill-none stroke-current stroke-2"
132
+ >
133
+ <path d="M7 7h10v10" />
134
+ <path d="M7 17 17 7" />
135
+ </svg>
136
+ </a>
137
+ </div>
138
+ </div>
139
+
140
+ <div className="flex flex-col items-center gap-1 text-sm text-neutral-600 lg:flex-row lg:gap-2 dark:text-neutral-400">
141
+ <p className="m-0">Get started by editing </p>
142
+ <a
143
+ href={fileURL}
144
+ className="rounded-md bg-neutral-200 px-2 py-1 dark:bg-neutral-800"
145
+ >
146
+ <code>src/app/page.tsx</code>
147
+ </a>
148
+ </div>
149
+ </main>
150
+ )
151
+ }
@@ -0,0 +1,51 @@
1
+ import { createEnv } from "@t3-oss/env-nextjs"
2
+ import { z } from "zod"
3
+
4
+ export const env = createEnv({
5
+ /**
6
+ * Specify your server-side environment variables schema here. This way you can ensure the app
7
+ * isn't built with invalid env vars.
8
+ */
9
+ server: {
10
+ NODE_ENV: z
11
+ .enum(["development", "test", "production"])
12
+ .default("development"),
13
+ DATABASE_URL: z.string().url(),
14
+ BETTER_AUTH_SECRET: z.string(),
15
+ DISCORD_CLIENT_ID: z.string(),
16
+ DISCORD_CLIENT_SECRET: z.string(),
17
+ },
18
+
19
+ /**
20
+ * Specify your client-side environment variables schema here. This way you can ensure the app
21
+ * isn't built with invalid env vars. To expose them to the client, prefix them with
22
+ * `NEXT_PUBLIC_`.
23
+ */
24
+ client: {
25
+ // NEXT_PUBLIC_CLIENTVAR: z.string(),
26
+ NEXT_PUBLIC_BETTER_AUTH_URL: z.string().url(),
27
+ },
28
+
29
+ /**
30
+ * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
31
+ * middlewares) or client-side so we need to destruct manually.
32
+ */
33
+ runtimeEnv: {
34
+ NODE_ENV: process.env.NODE_ENV,
35
+ DATABASE_URL: process.env.DATABASE_URL,
36
+ BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
37
+ NEXT_PUBLIC_BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
38
+ DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
39
+ DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
40
+ },
41
+ /**
42
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
43
+ * useful for Docker builds.
44
+ */
45
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
46
+ /**
47
+ * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
48
+ * `SOME_VAR=''` will throw an error.
49
+ */
50
+ emptyStringAsUndefined: true,
51
+ })
@@ -0,0 +1,48 @@
1
+ import { createEnv } from "@t3-oss/env-nextjs"
2
+ import { z } from "zod"
3
+
4
+ export const env = createEnv({
5
+ /**
6
+ * Specify your server-side environment variables schema here. This way you can ensure the app
7
+ * isn't built with invalid env vars.
8
+ */
9
+ server: {
10
+ NODE_ENV: z.enum(["development", "test", "production"]),
11
+ BETTER_AUTH_SECRET: z.string(),
12
+ DISCORD_CLIENT_ID: z.string(),
13
+ DISCORD_CLIENT_SECRET: z.string(),
14
+ },
15
+
16
+ /**
17
+ * Specify your client-side environment variables schema here. This way you can ensure the app
18
+ * isn't built with invalid env vars. To expose them to the client, prefix them with
19
+ * `NEXT_PUBLIC_`.
20
+ */
21
+ client: {
22
+ // NEXT_PUBLIC_CLIENTVAR: z.string(),
23
+ NEXT_PUBLIC_BETTER_AUTH_URL: z.string().url(),
24
+ },
25
+
26
+ /**
27
+ * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
28
+ * middlewares) or client-side so we need to destruct manually.
29
+ */
30
+ runtimeEnv: {
31
+ NODE_ENV: process.env.NODE_ENV,
32
+ // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
33
+ BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
34
+ NEXT_PUBLIC_BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
35
+ DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
36
+ DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
37
+ },
38
+ /**
39
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
40
+ * useful for Docker builds.
41
+ */
42
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
43
+ /**
44
+ * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
45
+ * `SOME_VAR=''` will throw an error.
46
+ */
47
+ emptyStringAsUndefined: true,
48
+ })
@@ -0,0 +1,9 @@
1
+ import { createAuthClient } from "better-auth/react"
2
+
3
+ import { env } from "@/env"
4
+
5
+ export const authClient = createAuthClient({
6
+ baseURL: env.NEXT_PUBLIC_BETTER_AUTH_URL,
7
+ })
8
+
9
+ export const { signIn, signOut, useSession } = authClient
@@ -0,0 +1,21 @@
1
+ import { PrismaClient } from "@prisma/client"
2
+ import { betterAuth } from "better-auth"
3
+ import { prismaAdapter } from "better-auth/adapters/prisma"
4
+ import { nextCookies } from "better-auth/next-js"
5
+
6
+ import { env } from "@/env"
7
+
8
+ const prisma = new PrismaClient()
9
+
10
+ export const auth = betterAuth({
11
+ database: prismaAdapter(prisma, {
12
+ provider: "sqlite",
13
+ }),
14
+ socialProviders: {
15
+ discord: {
16
+ clientId: env.DISCORD_CLIENT_ID,
17
+ clientSecret: env.DISCORD_CLIENT_SECRET,
18
+ },
19
+ },
20
+ plugins: [nextCookies()], // make sure nextCookies() is the last plugin in the array
21
+ })
@@ -0,0 +1,16 @@
1
+ import { betterAuth } from "better-auth"
2
+ import { nextCookies } from "better-auth/next-js"
3
+ import Database from "better-sqlite3"
4
+
5
+ import { env } from "@/env"
6
+
7
+ export const auth = betterAuth({
8
+ database: new Database("./db.sqlite"),
9
+ socialProviders: {
10
+ discord: {
11
+ clientId: env.DISCORD_CLIENT_ID,
12
+ clientSecret: env.DISCORD_CLIENT_SECRET,
13
+ },
14
+ },
15
+ plugins: [nextCookies()], // make sure nextCookies() is the last plugin in the array
16
+ })