diff --git a/package.json b/package.json index a894b525..b4a507bd 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/parser": "^6.6.0", "eslint": "^8.48.0", + "prettier": "^3.2.5", "typescript": "^5.1.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66c23900..f5d55869 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ devDependencies: eslint: specifier: ^8.48.0 version: 8.48.0 + prettier: + specifier: ^3.2.5 + version: 3.2.5 typescript: specifier: ^5.1.6 version: 5.1.6 @@ -1173,6 +1176,12 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} diff --git a/src/commands/add/auth/clerk/index.ts b/src/commands/add/auth/clerk/index.ts index cc5909c3..43a253ae 100644 --- a/src/commands/add/auth/clerk/index.ts +++ b/src/commands/add/auth/clerk/index.ts @@ -6,11 +6,9 @@ // 6. Add lib/auth/utils.ts // 7. install package - @clerk/nextjs -import { consola } from "consola"; import { addPackageToConfig, createFile, - installPackages, readConfigFile, replaceFile, updateConfigFile, @@ -23,11 +21,10 @@ import { } from "../../utils.js"; import { clerkGenerators } from "./generators.js"; import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; -import { libAuthUtilsTs } from "../next-auth/generators.js"; import { updateTrpcWithSessionIfInstalled } from "../shared/index.js"; export const addClerk = async () => { - const { rootPath, preferredPackageManager, componentLib } = readConfigFile(); + const { rootPath, componentLib } = readConfigFile(); const { clerk: { middleware, signInPage, signUpPage }, shared: { @@ -35,6 +32,7 @@ export const addClerk = async () => { init, }, } = getFilePaths(); + const { generateAuthUtilsTs, generateMiddlewareTs, @@ -42,9 +40,11 @@ export const addClerk = async () => { generateSignUpPageTs, homePageWithUserButton, } = clerkGenerators; - addContextProviderToAuthLayout("ClerkProvider"); - addContextProviderToAppLayout("ClerkProvider"); - addToDotEnv( + + await addContextProviderToAuthLayout("ClerkProvider"); + await addContextProviderToAppLayout("ClerkProvider"); + + await addToDotEnv( [ { key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", value: "", public: true }, { key: "CLERK_SECRET_KEY", value: "" }, @@ -55,21 +55,21 @@ export const addClerk = async () => { ], rootPath ); - createFile( + await createFile( formatFilePath(middleware, { prefix: "rootPath", removeExtension: false }), generateMiddlewareTs() ); - createFile( + await createFile( formatFilePath(signInPage, { removeExtension: false, prefix: "rootPath" }), generateSignInPageTs() ); - createFile( + await createFile( formatFilePath(signUpPage, { removeExtension: false, prefix: "rootPath" }), generateSignUpPageTs() ); - replaceFile( + await replaceFile( formatFilePath(init.dashboardRoute, { removeExtension: false, prefix: "rootPath", @@ -77,7 +77,7 @@ export const addClerk = async () => { homePageWithUserButton(componentLib) ); - createFile( + await createFile( formatFilePath(authUtils, { prefix: "rootPath", removeExtension: false, @@ -86,15 +86,15 @@ export const addClerk = async () => { ); // If trpc installed, add protectedProcedure - updateTrpcWithSessionIfInstalled(); + await updateTrpcWithSessionIfInstalled(); addToInstallList({ regular: ["@clerk/nextjs"], dev: [] }); // await installPackages( // { regular: "@clerk/nextjs", dev: "" }, // preferredPackageManager, // ); - addPackageToConfig("clerk"); - updateConfigFile({ auth: "clerk" }); + await addPackageToConfig("clerk"); + await updateConfigFile({ auth: "clerk" }); // consola.success("Successfully added Clerk to your project!"); // consola.info( // "Head over to https://dashboard.clerk.com/apps/new to create a new Clerk app" diff --git a/src/commands/add/auth/clerk/utils.ts b/src/commands/add/auth/clerk/utils.ts index b28541b4..c88fbea2 100644 --- a/src/commands/add/auth/clerk/utils.ts +++ b/src/commands/add/auth/clerk/utils.ts @@ -1,5 +1,5 @@ import fs from "fs"; -import { createFile, replaceFile } from "../../../../utils.js"; +import { replaceFile } from "../../../../utils.js"; import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; // export const updateClerkMiddlewareForStripe = (rootPath: string) => { @@ -14,13 +14,14 @@ import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; // if (mwExists) { // const mwContent = fs.readFileSync(mwPath, "utf-8"); // const newUtilsContent = mwContent.replace(initMWContent, updatedMWContent); -// replaceFile(mwPath, newUtilsContent); +// await replaceFile(mwPath, newUtilsContent); // } else { // console.error("Middleware does not exist"); // } // }; -export const addToClerkIgnoredRoutes = (newPath: string) => { +// TODO: Shouldn't this be "matcher" instead of "ignoredRoutes"? +export const addToClerkIgnoredRoutes = async (newPath: string) => { const { clerk } = getFilePaths(); const initMWContent = "ignoredRoutes: ["; const updatedMWContent = "ignoredRoutes: [" + ` "${newPath}", `; @@ -32,7 +33,7 @@ export const addToClerkIgnoredRoutes = (newPath: string) => { if (mwExists) { const mwContent = fs.readFileSync(mwPath, "utf-8"); const newUtilsContent = mwContent.replace(initMWContent, updatedMWContent); - replaceFile(mwPath, newUtilsContent); + await replaceFile(mwPath, newUtilsContent); } else { console.error("Middleware does not exist"); } diff --git a/src/commands/add/auth/kinde/index.ts b/src/commands/add/auth/kinde/index.ts index 0aabe0cc..ca421e19 100644 --- a/src/commands/add/auth/kinde/index.ts +++ b/src/commands/add/auth/kinde/index.ts @@ -1,8 +1,6 @@ -import { consola } from "consola"; import { addPackageToConfig, createFile, - installPackages, readConfigFile, updateConfigFile, } from "../../../../utils.js"; @@ -20,33 +18,36 @@ import { addToInstallList } from "../../utils.js"; export const addKinde = async () => { const { kinde, shared } = getFilePaths(); - const { preferredPackageManager } = readConfigFile(); + // add api route - createFile( + await createFile( formatFilePath(kinde.routeHandler, { prefix: "rootPath", removeExtension: false, }), generateKindeRouteHandler() ); + // create signin button component - createFile( + await createFile( formatFilePath(shared.auth.signInComponent, { prefix: "rootPath", removeExtension: false, }), generateSignInComponent() ); + // create auth/utils.ts - createFile( + await createFile( formatFilePath(shared.auth.authUtils, { prefix: "rootPath", removeExtension: false, }), generateAuthUtils() ); + // update root page - createFile( + await createFile( formatFilePath(shared.init.dashboardRoute, { prefix: "rootPath", removeExtension: false, @@ -55,7 +56,7 @@ export const addKinde = async () => { ); // generate sign in page - createFile( + await createFile( formatFilePath(kinde.signInPage, { prefix: "rootPath", removeExtension: false, @@ -64,10 +65,10 @@ export const addKinde = async () => { ); // If trpc installed, add protectedProcedure - updateTrpcWithSessionIfInstalled(); + await updateTrpcWithSessionIfInstalled(); // add env variables - addToDotEnv([ + await addToDotEnv([ { key: "KINDE_CLIENT_ID", value: "", @@ -91,7 +92,7 @@ export const addKinde = async () => { // ); addToInstallList({ regular: ["@kinde-oss/kinde-auth-nextjs"], dev: [] }); - addPackageToConfig("kinde"); - updateConfigFile({ auth: "kinde" }); + await addPackageToConfig("kinde"); + await updateConfigFile({ auth: "kinde" }); // consola.success("Successfully installed Kinde auth"); }; diff --git a/src/commands/add/auth/lucia/index.ts b/src/commands/add/auth/lucia/index.ts index fef3c3f1..2e25c543 100644 --- a/src/commands/add/auth/lucia/index.ts +++ b/src/commands/add/auth/lucia/index.ts @@ -56,40 +56,39 @@ export const addLucia = async () => { } else { viewsAndComponents = generateViewsAndComponents(false); } - createFile( + await createFile( formatFilePath(lucia.signInPage, { removeExtension: false, prefix: "rootPath", }), viewsAndComponents.signInPage ); - createFile( + await createFile( formatFilePath(lucia.signUpPage, { removeExtension: false, prefix: "rootPath", }), viewsAndComponents.signUpPage ); - createFile( + await createFile( formatFilePath(lucia.formErrorComponent, { removeExtension: false, prefix: "rootPath", }), viewsAndComponents.authFormErrorComponent ); - replaceFile( + await replaceFile( formatFilePath(shared.init.dashboardRoute, { removeExtension: false, prefix: "rootPath", }), viewsAndComponents.homePage ); - createFile( + await createFile( rootPath.concat("app/loading.tsx"), viewsAndComponents.loadingPage ); - - createFile( + await createFile( formatFilePath(lucia.signOutButtonComponent, { removeExtension: false, prefix: "rootPath", @@ -98,7 +97,7 @@ export const addLucia = async () => { ); // add server actions - createFile( + await createFile( formatFilePath(lucia.usersActions, { removeExtension: false, prefix: "rootPath", @@ -108,7 +107,7 @@ export const addLucia = async () => { const authDirFiles = generateAuthDirFiles(orm, driver, provider); // create auth/utils.ts - createFile( + await createFile( formatFilePath(shared.auth.authUtils, { removeExtension: false, prefix: "rootPath", @@ -117,7 +116,7 @@ export const addLucia = async () => { ); // create auth/lucia.ts - createFile( + await createFile( formatFilePath(lucia.libAuthLucia, { removeExtension: false, prefix: "rootPath", @@ -176,7 +175,7 @@ export type UsernameAndPassword = z.infer; /\.references\(\(\) => user\.id\)/g, "" ); - createFile( + await createFile( formatFilePath(shared.auth.authSchema, { removeExtension: false, prefix: "rootPath", @@ -184,7 +183,7 @@ export type UsernameAndPassword = z.infer; schemaWithoutReferences ); } else { - createFile( + await createFile( formatFilePath(shared.auth.authSchema, { removeExtension: false, prefix: "rootPath", @@ -213,7 +212,7 @@ export type UsernameAndPassword = z.infer; const contentsWithPool = contentsImportsUpdated.concat( "\nexport const pool = new Pool({ connectionString: env.DATABASE_URL });" ); - replaceFile(dbTsPath, contentsWithPool); + await replaceFile(dbTsPath, contentsWithPool); } // install packages (lucia, and adapter) will have to pull in specific package @@ -226,12 +225,12 @@ export type UsernameAndPassword = z.infer; if (t3 && orm === "drizzle") { // replace server/db/index.ts to have connection exported - updateDrizzleDbIndex(provider); + await updateDrizzleDbIndex(provider); // updates to make sure shcmea is included in dbindex too } // If trpc installed, add protectedProcedure - updateTrpcWithSessionIfInstalled(); + await updateTrpcWithSessionIfInstalled(); // update next config mjs addNodeRsFlagsToNextConfig(); @@ -248,7 +247,7 @@ export type UsernameAndPassword = z.infer; }); // add package to config - addPackageToConfig("lucia"); - updateConfigFile({ auth: "lucia" }); + await addPackageToConfig("lucia"); + await updateConfigFile({ auth: "lucia" }); // consola.success("Successfully installed Lucia!"); }; diff --git a/src/commands/add/auth/lucia/utils.ts b/src/commands/add/auth/lucia/utils.ts index 8d8a4c50..896b9ab2 100644 --- a/src/commands/add/auth/lucia/utils.ts +++ b/src/commands/add/auth/lucia/utils.ts @@ -180,14 +180,14 @@ export const addLuciaToPrismaSchema = async () => { // write logic to check if model already exists -> if so replace const newContent = schemaContents.concat("\n", PrismaLuciaSchema); - replaceFile(schemaPath, newContent); + await replaceFile(schemaPath, newContent); // consola.success(`Added auth to Prisma schema`); } else { consola.info(`Prisma schema file does not exist`); } }; -export const updateDrizzleDbIndex = (provider: DBProvider) => { +export const updateDrizzleDbIndex = async (provider: DBProvider) => { const { shared, drizzle } = getFilePaths(); // what is it like to type like this' // functions intended use if with t3 so assumed provider is pscale @@ -207,7 +207,7 @@ export const connection = connect({ export const db = drizzle(connection, { schema }); `; - replaceFile( + await replaceFile( formatFilePath(drizzle.dbIndex, { prefix: "rootPath", removeExtension: false, @@ -216,7 +216,7 @@ export const db = drizzle(connection, { schema }); ); } // TODO: NOW - updateRootSchema("auth", true, "lucia"); + await updateRootSchema("auth", true, "lucia"); }; export const addNodeRsFlagsToNextConfig = () => { diff --git a/src/commands/add/auth/next-auth/generators.ts b/src/commands/add/auth/next-auth/generators.ts index a4ee7052..9343a559 100644 --- a/src/commands/add/auth/next-auth/generators.ts +++ b/src/commands/add/auth/next-auth/generators.ts @@ -13,6 +13,7 @@ import { getDbIndexPath, getFilePaths, } from "../../../filePaths/index.js"; +import { formatFileContentWithPrettier } from "../../../init/utils.js"; // 1. Create app/api/auth/[...nextauth].ts export const apiAuthNextAuthTsOld = ( @@ -512,7 +513,7 @@ export default function SignIn() { }; // 6. updateTrpcTs -export const updateTrpcTs = () => { +export const updateTrpcTs = async () => { const { trpc } = getFilePaths(); const filePath = formatFilePath(trpc.serverTrpc, { removeExtension: false, @@ -547,14 +548,17 @@ export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); `; const modifiedRouterContent = fileContent.concat(protectedProcedureContent); - fs.writeFileSync(filePath, modifiedRouterContent); + fs.writeFileSync( + filePath, + await formatFileContentWithPrettier(modifiedRouterContent,filePath) + ); // consola.success( // "TRPC Router updated successfully to add protectedProcedure." // ); }; -export const enableSessionInContext = () => { +export const enableSessionInContext = async () => { const { trpc } = getFilePaths(); const filePath = formatFilePath(trpc.trpcContext, { prefix: "rootPath", @@ -564,13 +568,16 @@ export const enableSessionInContext = () => { const fileContent = fs.readFileSync(filePath, "utf-8"); const updatedContent = fileContent.replace(/\/\//g, ""); - fs.writeFileSync(filePath, updatedContent); + fs.writeFileSync( + filePath, + await formatFileContentWithPrettier(updatedContent,filePath) + ); // consola.success("TRPC Context updated successfully to add Session data."); }; // no longer necessary -export const enableSessionInTRPCApi_DEPRECATED = () => { +export const enableSessionInTRPCApi_DEPRECATED = async () => { const { trpc } = getFilePaths(); const filePath = formatFilePath(trpc.trpcApiTs, { prefix: "rootPath", @@ -580,7 +587,10 @@ export const enableSessionInTRPCApi_DEPRECATED = () => { const fileContent = fs.readFileSync(filePath, "utf-8"); const updatedContent = fileContent.replace(/\/\//g, ""); - fs.writeFileSync(filePath, updatedContent); + fs.writeFileSync( + filePath, + await formatFileContentWithPrettier(updatedContent,filePath) + ); // consola.success("TRPC Server API updated successfully to add Session data."); }; diff --git a/src/commands/add/auth/next-auth/index.ts b/src/commands/add/auth/next-auth/index.ts index 50254717..e35da1d8 100644 --- a/src/commands/add/auth/next-auth/index.ts +++ b/src/commands/add/auth/next-auth/index.ts @@ -1,8 +1,6 @@ -import { consola } from "consola"; import { addPackageToConfig, createFile, - installPackages, readConfigFile, replaceFile, updateConfigFile, @@ -17,7 +15,7 @@ import { libAuthProviderTsx, libAuthUtilsTs, } from "./generators.js"; -import { AuthDriver, AuthProvider, AuthProviders } from "./utils.js"; +import { AuthDriver, AuthProvider } from "./utils.js"; import { addContextProviderToAppLayout, // addContextProviderToAuthLayout, @@ -39,17 +37,15 @@ export const addNextAuth = async ( hasSrc, preferredPackageManager, driver, - packages, orm, componentLib, provider: dbProvider, t3, } = readConfigFile(); - const rootPath = `${hasSrc ? "src/" : ""}`; const { "next-auth": nextAuth, shared } = getFilePaths(); // 1. Create app/api/auth/[...nextauth].ts - createFile( + await createFile( formatFilePath(nextAuth.nextAuthApiRoute, { removeExtension: false, prefix: "rootPath", @@ -58,7 +54,7 @@ export const addNextAuth = async ( ); // 2. create lib/auth/Provider.tsx - createFile( + await createFile( formatFilePath(nextAuth.authProviderComponent, { removeExtension: false, prefix: "rootPath", @@ -67,7 +63,7 @@ export const addNextAuth = async ( ); // 3. create lib/auth/utils.ts - createFile( + await createFile( formatFilePath(shared.auth.authUtils, { removeExtension: false, prefix: "rootPath", @@ -78,7 +74,7 @@ export const addNextAuth = async ( // 4. create lib/db/schema/auth.ts if (orm !== null) { if (orm === "drizzle") { - createFile( + await createFile( formatFilePath(shared.auth.authSchema, { removeExtension: false, prefix: "rootPath", @@ -86,11 +82,11 @@ export const addNextAuth = async ( createDrizzleAuthSchema(driver) ); if (t3) { - updateRootSchema("auth", true, "next-auth"); + await updateRootSchema("auth", true, "next-auth"); } } if (orm === "prisma") { - addToPrismaSchema( + await addToPrismaSchema( createPrismaAuthSchema( driver, dbProvider === "planetscale", @@ -102,7 +98,7 @@ export const addNextAuth = async ( } // 5. create components/auth/SignIn.tsx - TODO - may be causing problems - createFile( + await createFile( formatFilePath(shared.auth.signInComponent, { removeExtension: false, prefix: "rootPath", @@ -111,9 +107,9 @@ export const addNextAuth = async ( ); // 6. If trpc installed, add protectedProcedure // this wont run because it is installed before trpc - updateTrpcWithSessionIfInstalled(); + await updateTrpcWithSessionIfInstalled(); - replaceFile( + await replaceFile( formatFilePath(shared.init.dashboardRoute, { removeExtension: false, prefix: "rootPath", @@ -122,7 +118,7 @@ export const addNextAuth = async ( ); // generate sign in page - createFile( + await createFile( formatFilePath(nextAuth.signInPage, { removeExtension: false, prefix: "rootPath", @@ -131,7 +127,7 @@ export const addNextAuth = async ( ); // add to env - addToDotEnv( + await addToDotEnv( [ { key: "NEXTAUTH_SECRET", @@ -182,11 +178,11 @@ export const addNextAuth = async ( if (orm !== null) addToInstallList({ regular: [AuthDriver[orm].package], dev: [] }); - addPackageToConfig("next-auth"); - updateConfigFile({ auth: "next-auth" }); + await addPackageToConfig("next-auth"); + await updateConfigFile({ auth: "next-auth" }); // 9. Instruct user to add the to their root layout. - // addContextProviderToAuthLayout("NextAuthProvider"); - addContextProviderToAppLayout("NextAuthProvider"); + // await addContextProviderToAuthLayout("NextAuthProvider"); + await addContextProviderToAppLayout("NextAuthProvider"); // if (orm === "prisma") await prismaGenerate(preferredPackageManager); // consola.success("Successfully added Next Auth to your project!"); }; diff --git a/src/commands/add/auth/next-auth/utils.ts b/src/commands/add/auth/next-auth/utils.ts index 770100b5..1c0834c2 100644 --- a/src/commands/add/auth/next-auth/utils.ts +++ b/src/commands/add/auth/next-auth/utils.ts @@ -58,7 +58,7 @@ export const AuthDriver: { }, }; -export const checkAndAddAuthUtils = () => { +export const checkAndAddAuthUtils = async () => { const { shared } = getFilePaths(); const authUtilsPath = formatFilePath(shared.auth.authUtils, { removeExtension: false, @@ -93,5 +93,5 @@ export const checkAuth = async () => { if (!session) redirect("/api/auth/signin"); }; `; - createFile(authUtilsPath, t3AuthUtilsContent); + await createFile(authUtilsPath, t3AuthUtilsContent); }; diff --git a/src/commands/add/auth/shared/generators.ts b/src/commands/add/auth/shared/generators.ts index 8d0b7784..1da6a4e1 100644 --- a/src/commands/add/auth/shared/generators.ts +++ b/src/commands/add/auth/shared/generators.ts @@ -692,6 +692,7 @@ export const createNavbar = ( usingClerk = false, auth: AuthType ) => { + // TODO: Add supabase as auth provider const { shared, "next-auth": nextAuth } = getFilePaths(); const { alias } = readConfigFile(); let logOutRoute: string; diff --git a/src/commands/add/auth/shared/index.ts b/src/commands/add/auth/shared/index.ts index f894d181..79bd3e32 100644 --- a/src/commands/add/auth/shared/index.ts +++ b/src/commands/add/auth/shared/index.ts @@ -1,9 +1,4 @@ -import { consola } from "consola"; -import { - createFile, - installShadcnUIComponents, - readConfigFile, -} from "../../../../utils.js"; +import { createFile, readConfigFile } from "../../../../utils.js"; import { addToShadcnComponentList } from "../../utils.js"; import { createAccountApiTs, @@ -12,7 +7,7 @@ import { createUserSettingsComponent, createUpdateNameCard, createUpdateEmailCard, - createNavbar, + // createNavbar, createSignOutBtn, } from "./generators.js"; import { AuthType, ORMType } from "../../../../types.js"; @@ -26,9 +21,10 @@ export const createAccountSettingsPage = async () => { const { orm, rootPath, componentLib, auth } = readConfigFile(); const { shared } = getFilePaths(); const withShadCn = componentLib === "shadcn-ui" ? true : false; - // create account api - clerk has managed component so no need - if (auth !== "clerk" && auth !== "lucia") { - createFile( + + // create account api - clerk has managed components so no need - supabase has its own client to update user details + if (auth !== "supabase" && auth !== "clerk" && auth !== "lucia") { + await createFile( formatFilePath(shared.auth.accountApiRoute, { prefix: "rootPath", removeExtension: false, @@ -38,7 +34,7 @@ export const createAccountSettingsPage = async () => { } // create account page - createFile( + await createFile( formatFilePath(shared.auth.accountPage, { prefix: "rootPath", removeExtension: false, @@ -47,7 +43,7 @@ export const createAccountSettingsPage = async () => { ); // create usersettings component - createFile( + await createFile( formatFilePath(shared.auth.userSettingsComponent, { prefix: "rootPath", removeExtension: false, @@ -64,8 +60,9 @@ export const scaffoldAccountSettingsUI = async ( auth: AuthType ) => { const { shared, lucia } = getFilePaths(); + // create updatenamecard - createFile( + await createFile( formatFilePath(shared.auth.updateNameCardComponent, { prefix: "rootPath", removeExtension: false, @@ -74,7 +71,7 @@ export const scaffoldAccountSettingsUI = async ( ); // create updatenamecard - createFile( + await createFile( formatFilePath(shared.auth.updateEmailCardComponent, { prefix: "rootPath", removeExtension: false, @@ -83,7 +80,7 @@ export const scaffoldAccountSettingsUI = async ( ); // create accountcard components - createFile( + await createFile( formatFilePath(shared.auth.accountCardComponent, { prefix: "rootPath", removeExtension: false, @@ -92,7 +89,7 @@ export const scaffoldAccountSettingsUI = async ( ); // create navbar component - // createFile( + // await createFile( // formatFilePath(shared.init.navbarComponent, { // prefix: "rootPath", // removeExtension: false, @@ -101,8 +98,8 @@ export const scaffoldAccountSettingsUI = async ( // ); // TODO FIX THIS - if (withShadCn && auth !== "lucia") { - createFile( + if (withShadCn && auth !== "lucia" && auth !== "supabase") { + await createFile( formatFilePath(lucia.signOutButtonComponent, { prefix: "rootPath", removeExtension: false, @@ -119,12 +116,12 @@ export const scaffoldAccountSettingsUI = async ( } }; -export const updateTrpcWithSessionIfInstalled = () => { +export const updateTrpcWithSessionIfInstalled = async () => { const { packages, t3 } = readConfigFile(); if (packages.includes("trpc")) { if (!t3) { - updateTrpcTs(); - enableSessionInContext(); + await updateTrpcTs(); + await enableSessionInContext(); } } }; diff --git a/src/commands/add/auth/supabase/generators.ts b/src/commands/add/auth/supabase/generators.ts new file mode 100644 index 00000000..4e14190a --- /dev/null +++ b/src/commands/add/auth/supabase/generators.ts @@ -0,0 +1,825 @@ +import { readConfigFile } from "../../../../utils.js"; +import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; + +const generateViewsAndComponents = (withShadCn: boolean) => { + const signUpPage = generateSignUpPage(withShadCn); + const signInPage = generateSignInPage(withShadCn); + const authFormComponent = generateAuthFormComponent(withShadCn); + const signOutButtonComponent = generateSignOutButtonComponent(withShadCn); + const homePage = generateHomePage(); + const loadingPage = generateLoadingPage(); + + return { + signUpPage, + signInPage, + signOutButtonComponent, + authFormComponent, + homePage, + loadingPage, + }; +}; + +export const generateSupabaseHelpers = () => { + const { shared } = getFilePaths(); + return ` + import { createBrowserClient, createServerClient, type CookieOptions } from '@supabase/ssr' + import { env } from "${formatFilePath(shared.init.envMjs, { + removeExtension: false, + prefix: "alias", + })}"; + import { cookies } from 'next/headers' + import { type NextRequest, NextResponse } from "next/server"; + + /* Only use this for client components ("use client" at the top of the file) */ + export const createSupabaseBrowserClient = createBrowserClient( + env.NEXT_PUBLIC_SUPABASE_URL, + env.NEXT_PUBLIC_SUPABASE_ANON_KEY + ) + + /* Only use this for server only components */ + export const createSupabaseServerComponentClient = () => { + return createServerClient( + env.NEXT_PUBLIC_SUPABASE_URL, + env.NEXT_PUBLIC_SUPABASE_ANON_KEY, + { + cookies: { + get(name: string) { + return cookies().get(name)?.value + } + } + } + ) + } + + /* Use this inside Server Actions or Route Handlers bc only in those u can set cookies */ + export const createSupabaseServerActionClient = () => { + return createServerClient( + env.NEXT_PUBLIC_SUPABASE_URL, + env.NEXT_PUBLIC_SUPABASE_ANON_KEY, + { + cookies: { + get(name: string) { + return cookies().get(name)?.value + }, + set(name: string, value: string, options: CookieOptions) { + cookies().set({ name, value, ...options }) + }, + remove(name: string, options: CookieOptions) { + cookies().set({ name, value: '', ...options }) + } + } + } + ) + } + + /* Use this inside Server Actions or Route Handlers bc only in those u can set cookies */ + export const createSupabaseApiRouteClient = () => { + return createSupabaseServerActionClient() + } + + /* Use this inside the middleware */ + export const createSupabaseMiddlewareClient = (request: NextRequest) => { + // Create an unmodified response + let response = NextResponse.next({ + request: { + headers: request.headers + } + }) + + const supabase = createServerClient( + env.NEXT_PUBLIC_SUPABASE_URL, + env.NEXT_PUBLIC_SUPABASE_ANON_KEY, + { + cookies: { + get(name: string) { + return request.cookies.get(name)?.value + }, + set(name: string, value: string, options: CookieOptions) { + // If the cookie is updated, update the cookies for the request and response + request.cookies.set({ + name, + value, + ...options + }) + response = NextResponse.next({ + request: { + headers: request.headers + } + }) + response.cookies.set({ + name, + value, + ...options + }) + }, + remove(name: string, options: CookieOptions) { + // If the cookie is removed, update the cookies for the request and response + request.cookies.set({ + name, + value: '', + ...options + }) + response = NextResponse.next({ + request: { + headers: request.headers + } + }) + response.cookies.set({ + name, + value: '', + ...options + }) + } + }} + ) + return { supabase, response } + } + + export const updateSession = async (request: NextRequest) => { + try { + const { supabase, response } = createSupabaseMiddlewareClient(request) + // This will refresh session if expired - required for Server Components + // https://supabase.com/docs/guides/auth/server-side/nextjs + await supabase.auth.getUser() + + return response + } catch (e) { + // If you are here, a Supabase client could not be created! + // This is likely because you have not set up environment variables. + // Check out http://localhost:3000 for Next Steps. + return NextResponse.next({ + request: { + headers: request.headers + } + }) + } + } +`; +}; + +export const generateSignInPage = (withShadCn: boolean) => { + const { supabase, shared } = getFilePaths(); + const { alias } = readConfigFile(); + if (withShadCn) { + return ` + import AuthForm from "${formatFilePath(supabase.authFormComponent, { + removeExtension: true, + prefix: "alias", + })}"; + import { signIn } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + import { Input } from "${alias}/components/ui/input"; + import { Label } from "${alias}/components/ui/label"; + import Link from "next/link"; + + const Page = async () => { + return ( +
+

+ Sign in to your account +

+ + + +
+ + +
+
+ Don't have an account yet?{" "} + + Create an account + +
+
+ ); + }; + export default Page; + `; + } else { + return ` + import AuthForm from "${formatFilePath(supabase.authFormComponent, { + removeExtension: true, + prefix: "alias", + })}"; + import { signIn } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + import Link from "next/link"; + + const Page = async () => { + return ( +
+

+ Sign in to your account +

+ + + +
+ + +
+
+ Don't have an account yet?{" "} + + Create an account + +
+
+ ); + }; + export default Page; + `; + } +}; + +export const generateSignUpPage = (withShadCn: boolean) => { + const { supabase, shared } = getFilePaths(); + const { alias } = readConfigFile(); + if (withShadCn) { + return ` + import AuthForm from "${formatFilePath(supabase.authFormComponent, { + removeExtension: true, + prefix: "alias", + })}"; + import { signUp } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + import { Input } from "${alias}/components/ui/input"; + import { Label } from "${alias}/components/ui/label"; + import Link from "next/link"; + + const Page = async () => { + return ( +
+

Create an account

+ + + +
+ + +
+
+ Already have an account?{" "} + + Sign in + +
+
+ ); + }; + export default Page; + `; + } else { + return ` + import AuthForm from "${formatFilePath(supabase.authFormComponent, { + removeExtension: true, + prefix: "alias", + })}"; + import { signUp } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + import Link from "next/link"; + + const Page = async () => { + return ( +
+

Create an account

+ + + +
+ + +
+
+ Already have an account?{" "} + + Sign in + +
+
+ ); + }; + export default Page; + `; + } +}; + +export const generateSignOutButtonComponent = (withShadCn: boolean) => { + const { alias } = readConfigFile(); + const { shared } = getFilePaths(); + if (withShadCn) { + return ` + "use client"; + import { Button } from "${alias}/components/ui/button"; + import { signOut } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + + export const SignOutButton = () => { + return ( +
+ +
+ ); + }; + `; + } else { + return ` + "use client"; + + import { signOut } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + + export const SignOutButton = () => { + return ( +
+ +
+ ) + } + `; + } +}; + +export const generateAuthFormComponent = (withShadCn: boolean) => { + const { alias } = readConfigFile(); + const { shared } = getFilePaths(); + const authForm = ` + "use client"; + + import { useFormState, useFormStatus } from "react-dom"; + import { redirect } from 'next/navigation'; + import { Button } from "${alias}/components/ui/button"; + import { type State } from "${formatFilePath(shared.auth.authActions, { + prefix: "alias", + removeExtension: true, + })}"; + + type AuthType = 'sign-in' | 'sign-up' + type ServerAction = (state: State, formData: FormData) => State | Promise + + function AuthForm({ + action, + authType, + children + }: { + action: ServerAction + authType: AuthType + children?: React.ReactNode + }) { + const [state, formAction] = useFormState(action, null) + + if (state?.redirectTo) redirect(state.redirectTo) + + return ( +
+
+ {children} + + {state?.error && {state.error}} + + {state?.message && {state.message}} + + + +
+ ) + } + + export default AuthForm + + const SubmitButton = ({ authType }: { authType: AuthType }) => { + const buttonSuffix = authType === 'sign-in' ? 'in' : 'up' + + const { pending } = useFormStatus() + `; + + if (withShadCn) { + return ( + authForm + + ` + return ( + + )}` + ); + } else { + return ( + authForm + + ` + return ( + + )}` + ); + } +}; + +export const generateHomePage = () => { + const { supabase, shared } = getFilePaths(); + const { componentLib } = readConfigFile(); + return ` + import { SignOutButton } from "${formatFilePath( + supabase.signOutButtonComponent, + { + removeExtension: true, + prefix: "alias", + } + )}"; + import { getUserAuth } from "${formatFilePath(shared.auth.authUtils, { + prefix: "alias", + removeExtension: true, + })}"; + + export default async function Home() { + const { session } = await getUserAuth(); + return ( +
+

Profile

+
+                {JSON.stringify(session, null, 2)}
+            
+ +
+ ); + } +`; +}; +export const generateLoadingPage = () => { + const { componentLib } = readConfigFile(); + const withShadCn = componentLib === "shadcn-ui"; + return ` + export default function Loading() { + return ( +
+
+ + Loading... +
+
+ ); + } + `; +}; + +export const generateApiRoutes = () => { + const { supabase } = getFilePaths(); + return ` + import { createSupabaseApiRouteClient } from "${formatFilePath( + supabase.libSupabaseAuthHelpers, + { + removeExtension: true, + prefix: "alias", + } + )}"; + import { NextRequest, NextResponse } from "next/server"; + + export async function GET(request: NextRequest) { + const requestUrl = new URL(request.url); + const code = requestUrl.searchParams.get("code"); + + if (code) { + const supabase = createSupabaseApiRouteClient(); + await supabase.auth.exchangeCodeForSession(code); + } + + // URL to redirect to after sign in process completes + return NextResponse.redirect(requestUrl.origin); + } + `; +}; + +const generateAuthDirFiles = () => { + const { supabase } = getFilePaths(); + const utilsTs = ` + import { redirect } from "next/navigation"; + import { createSupabaseServerComponentClient } from "${formatFilePath( + supabase.libSupabaseAuthHelpers, + { + removeExtension: true, + prefix: "alias", + } + )}"; + + export const getServerSession = async () => { + const supabase = createSupabaseServerComponentClient(); + const { + data: { session } + } = await supabase.auth.getSession() + + return { session }; + }; + + export const getServerUser = async () => { + const supabase = createSupabaseServerComponentClient() + const { + data: { user } + } = await supabase.auth.getUser() + + return { user } + } + + export type AuthSession = { + session: { + user: { + id: string; + name?: string; + email?: string; + }; + } | null; + }; + + export const getUserAuth = async () => { + const { user } = await getServerUser(); + + if (user) { + return { + session: { + user: { + id: user.id, + name: user.user_metadata?.name ?? '', // user.user_metadata.name is only populated after the user has updated their name once. Supabase doesn't store the users name by default. + email: user.email, + }, + }, + } as AuthSession; + } else { + return { session: null }; + } + }; + + export const checkAuth = async () => { + const { session } = await getUserAuth(); + + if (!session) redirect("/sign-in"); + }; +`; + + const actionTs = ` + "use server" + import { redirect } from "next/navigation"; + import { createSupabaseServerActionClient } from "${formatFilePath( + supabase.libSupabaseAuthHelpers, + { + removeExtension: true, + prefix: "alias", + } + )}" + import { zfd } from 'zod-form-data' + import { z } from 'zod' + + export type State = { + error?: string + message?: string + redirectTo?: string + } | null + + export const signOut = async () => { + const supabase = createSupabaseServerActionClient() + await supabase.auth.signOut() + redirect('/') + } + + const signInSchema = zfd.formData({ + email: zfd.text( + z + .string({ + required_error: 'You have to enter an email address.', + }) + .email({ message: 'Please provide a valid email address' }) + ), + password: zfd.text( + z + .string({ required_error: 'You have to enter a password' }) + .min(8, 'Password must be longer than 8 characters.') + ), + }) + + export const signIn = async (state: State, formData: FormData) => { + const supabase = createSupabaseServerActionClient() + const res = signUpSchema.safeParse(formData) + + if (!res.success) { + const errors = res.error.flatten() + const errorMessage = Object.values(errors.fieldErrors) + .join('\\n') + .replace(',', '\\n') + return { error: errorMessage } + } + + const { email, password } = signInSchema.parse(formData) + + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + + if (error) return { error: error.message } + + return { redirectTo: '/' } + } + + const signUpSchema = zfd.formData({ + email: zfd.text( + z + .string({ + required_error: 'You have to enter an email address.', + }) + .email({ message: 'Please provide a valid email address' }) + ), + password: zfd.text( + z + .string({ required_error: 'You have to enter a password' }) + .min(8, 'Password must be longer than 8 characters.') + ), + }) + + export const signUp = async (state: State, formData: FormData) => { + const supabase = createSupabaseServerActionClient() + const res = signUpSchema.safeParse(formData) + + if (!res.success) { + const errors = res.error.flatten() + const errorMessage = Object.values(errors.fieldErrors) + .join('\\n') + .replace(',', '\\n') + return { error: errorMessage } + } + + const { email, password } = signUpSchema.parse(formData) + + const { error } = await supabase.auth.signUp({ email, password }) + + if (error) return { error: error.message } + + return { redirectTo: '/' } + } + + export const updateUserName = async (state: State, formData: FormData) => { + const supabase = createSupabaseServerActionClient() + const username = formData.get('name') as string + + const { error } = await supabase.auth.updateUser({ data: { username } }) + + if (error) return { error: error.message } + + return { message: 'Successfully updated username!', ...state } + } + + export const updateEmail = async (state: State, formData: FormData) => { + const supabase = createSupabaseServerActionClient() + const email = formData.get('email') as string + + const { error } = await supabase.auth.updateUser({ email }) + + if (error) return { error: error.message } + + return { message: 'Successfully updated email!', ...state } + }`; + + return { utilsTs, actionTs }; +}; + +const generateMiddleware = () => { + const { supabase } = getFilePaths(); + return ` + import { type NextRequest } from "next/server"; + import { updateSession } from "${formatFilePath( + supabase.libSupabaseAuthHelpers, + { + removeExtension: true, + prefix: "alias", + } + )}"; + + export async function middleware(request: NextRequest) { + return await updateSession(request); + } + + export const config = { + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - images - .svg, .png, .jpg, .jpeg, .gif, .webp + * Feel free to modify this pattern to include more paths. + */ + "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], + }; `; +}; + +export const supabaseGenerators = { + generateMiddleware, + generateViewsAndComponents, + generateSupabaseHelpers, + generateApiRoutes, + generateAuthDirFiles, +}; diff --git a/src/commands/add/auth/supabase/index.ts b/src/commands/add/auth/supabase/index.ts new file mode 100644 index 00000000..c48de523 --- /dev/null +++ b/src/commands/add/auth/supabase/index.ts @@ -0,0 +1,170 @@ +/* + 1. Add Env keys in env.mjs too + 2. Add middleware + 3. Scaffold Signup and Signin pages + 4. Create Signout button + 5. Create AuthForm component + 6. Create Home page + 7. Create Loading page + 8. Add API routes + 9. Add Supabase helpers + 10. Add Supabase packages + 11. Add package to config + 12. Update config file +*/ + +import { + addPackageToConfig, + createFile, + readConfigFile, + updateConfigFile, +} from "../../../../utils.js"; +import { supabaseGenerators } from "./generators.js"; +import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; +import { addToInstallList } from "../../utils.js"; +import { addToDotEnv } from "../../orm/drizzle/generators.js"; +export const addSupabase = async () => { + const { orm, rootPath, componentLib } = readConfigFile(); + const { supabase, shared } = getFilePaths(); + + const { + generateMiddleware, + generateViewsAndComponents, + generateSupabaseHelpers, + generateApiRoutes, + generateAuthDirFiles, + } = supabaseGenerators; + + await addToDotEnv( + [ + { + key: "NEXT_PUBLIC_SUPABASE_URL", + value: "your-project-url", + public: true, + }, + { + key: "NEXT_PUBLIC_SUPABASE_ANON_KEY", + value: "your-anon-key", + public: true, + }, + ], + rootPath + ); + + await createFile( + formatFilePath(supabase.middleware, { + prefix: "rootPath", + removeExtension: false, + }), + generateMiddleware() + ); + + const useShadCnUI = componentLib === "shadcn-ui"; + + // create auth form component + // generate sign-in and sign-up pages + const viewsAndComponents = generateViewsAndComponents(useShadCnUI); + + // create signIn Page + await createFile( + formatFilePath(supabase.signInPage, { + prefix: "rootPath", + removeExtension: false, + }), + viewsAndComponents.signInPage + ); + + // create signUp Page + await createFile( + formatFilePath(supabase.signUpPage, { + prefix: "rootPath", + removeExtension: false, + }), + viewsAndComponents.signUpPage + ); + + // create signOut Button + await createFile( + formatFilePath(supabase.signOutButtonComponent, { + prefix: "rootPath", + removeExtension: false, + }), + viewsAndComponents.signOutButtonComponent + ); + + // create auth form component + await createFile( + formatFilePath(supabase.authFormComponent, { + prefix: "rootPath", + removeExtension: false, + }), + viewsAndComponents.authFormComponent + ); + + // create home Page + await createFile( + formatFilePath(shared.init.dashboardRoute, { + prefix: "rootPath", + removeExtension: false, + }), + viewsAndComponents.homePage + ); + + await createFile( + rootPath.concat("app/loading.tsx"), + viewsAndComponents.loadingPage + ); + + // add API routes + const apiRoute = generateApiRoutes(); + + await createFile( + formatFilePath(supabase.callbackApiRoute, { + removeExtension: false, + prefix: "rootPath", + }), + apiRoute + ); + + const authDirFiles = generateAuthDirFiles(); + + // create auth utility functions + await createFile( + formatFilePath(shared.auth.authUtils, { + removeExtension: false, + prefix: "rootPath", + }), + authDirFiles.utilsTs + ); + + // create auth actions + await createFile( + formatFilePath(shared.auth.authActions, { + removeExtension: false, + prefix: "rootPath", + }), + authDirFiles.actionTs + ); + + const helpers = generateSupabaseHelpers(); + + // generate supabase helpers + await createFile( + formatFilePath(supabase.libSupabaseAuthHelpers, { + removeExtension: false, + prefix: "rootPath", + }), + helpers + ); + + const supabasePackages = ["@supabase/supabase-js", "@supabase/ssr"]; + const zodPackages = ["zod", "zod-form-data"]; + + const packagesToInstall = [...supabasePackages, ...zodPackages]; + + addToInstallList({ regular: packagesToInstall, dev: [] }); + + // add package to config + await addPackageToConfig("supabase"); + await updateConfigFile({ auth: "supabase" }); +}; diff --git a/src/commands/add/auth/supabase/utils.ts b/src/commands/add/auth/supabase/utils.ts new file mode 100644 index 00000000..faf395ea --- /dev/null +++ b/src/commands/add/auth/supabase/utils.ts @@ -0,0 +1,29 @@ +import { replaceFile } from "../../../../utils.js"; +import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; +import fs from "fs"; + +// TODO: Find out if this is actually necessary for Supabase +export const addToSupabaseIgnoredRoutes = async (newPath: string) => { + const { supabase } = getFilePaths(); + + const initMWContent = "matcher: ["; + + const updatedMWContent = "matcher: [" + ` "${newPath}", `; + + const mwPath = formatFilePath(supabase.middleware, { + prefix: "rootPath", + removeExtension: false, + }); + + const mwExists = fs.existsSync(mwPath); + + if (mwExists) { + const mwContent = fs.readFileSync(mwPath, "utf-8"); + + const newUtilsContent = mwContent.replace(initMWContent, updatedMWContent); + + await replaceFile(mwPath, newUtilsContent); + } else { + console.error("Middleware does not exist"); + } +}; diff --git a/src/commands/add/componentLib/shadcn-ui/index.ts b/src/commands/add/componentLib/shadcn-ui/index.ts index 4d705f5c..6e803d27 100644 --- a/src/commands/add/componentLib/shadcn-ui/index.ts +++ b/src/commands/add/componentLib/shadcn-ui/index.ts @@ -58,9 +58,9 @@ const manualInstallShadCn = async ( }); // add tailwind.config.ts - createFile("tailwind.config.ts", generateTailwindConfig(rootPath)); + await createFile("tailwind.config.ts", generateTailwindConfig(rootPath)); // update globals.css - replaceFile( + await replaceFile( formatFilePath(shared.init.globalCss, { prefix: "rootPath", removeExtension: false, @@ -68,25 +68,25 @@ const manualInstallShadCn = async ( generateGlobalsCss() ); // add cn helper (lib/utils.ts) - createFile(rootPath.concat("lib/utils.ts"), generateLibUtilsTs()); + await createFile(rootPath.concat("lib/utils.ts"), generateLibUtilsTs()); // create components.json - createFile("components.json", generateComponentsJson(rootPath)); + await createFile("components.json", generateComponentsJson(rootPath)); - createFile(rootPath.concat("app/loading.tsx"), generateLoadingPage()); + await createFile(rootPath.concat("app/loading.tsx"), generateLoadingPage()); // todo: install theme switcher // create theme provider - createFile( + await createFile( rootPath.concat("components/ThemeProvider.tsx"), generateThemeProvider() ); //generate theme toggler - createFile( + await createFile( rootPath.concat("components/ui/ThemeToggle.tsx"), generateThemeToggler() ); // add context provider to layout - addContextProviderToRootLayout("ThemeProvider"); + await addContextProviderToRootLayout("ThemeProvider"); }; export const installShadcnUI = async ( @@ -107,8 +107,8 @@ export const installShadcnUI = async ( if (existsSync(filePath)) { consola.info("Shadcn is already installed. Adding Shadcn UI to config..."); - addPackageToConfig("shadcn-ui"); - updateConfigFile({ componentLib: "shadcn-ui" }); + await addPackageToConfig("shadcn-ui"); + await updateConfigFile({ componentLib: "shadcn-ui" }); } else { try { // await execa(pmInstallCommand[preferredPackageManager], installArgs, { @@ -116,8 +116,8 @@ export const installShadcnUI = async ( // }); await manualInstallShadCn(preferredPackageManager, rootPath); // consola.success("Shadcn initialized successfully."); - addPackageToConfig("shadcn-ui"); - updateConfigFile({ componentLib: "shadcn-ui" }); + await addPackageToConfig("shadcn-ui"); + await updateConfigFile({ componentLib: "shadcn-ui" }); } catch (error) { consola.error(`Failed to initialize Shadcn: ${error.message}`); } @@ -139,12 +139,12 @@ export const installShadcnUI = async ( "dropdown-menu", ]); - addContextProviderToAppLayout("ShadcnToast"); + await addContextProviderToAppLayout("ShadcnToast"); // if (packages.includes("next-auth")) updateSignInComponentWithShadcnUI(); }; -export const updateSignInComponentWithShadcnUI = () => { +export const updateSignInComponentWithShadcnUI = async () => { const { hasSrc, alias } = readConfigFile(); const filepath = "components/auth/SignIn.tsx"; const updatedContent = `"use client"; @@ -171,5 +171,5 @@ export default function SignIn() { ); }`; - replaceFile(`${hasSrc ? "src/" : ""}${filepath}`, updatedContent); + await replaceFile(`${hasSrc ? "src/" : ""}${filepath}`, updatedContent); }; diff --git a/src/commands/add/index.ts b/src/commands/add/index.ts index feace154..60e9bc56 100644 --- a/src/commands/add/index.ts +++ b/src/commands/add/index.ts @@ -16,6 +16,7 @@ import { initProject } from "../init/index.js"; import { addPrisma } from "./orm/prisma/index.js"; import { ORMType, InitOptions } from "../../types.js"; import { addClerk } from "./auth/clerk/index.js"; +import { addSupabase } from "./auth/supabase/index.js"; import { addResend } from "./misc/resend/index.js"; import { addLucia } from "./auth/lucia/index.js"; import { createAccountSettingsPage } from "./auth/shared/index.js"; @@ -140,8 +141,8 @@ export const addPackage = async ( spinner.start(); spinner.text = "Beginning Configuration Process"; - createAppLayoutFile(); - createLandingPage(); + await createAppLayoutFile(); + await createLandingPage(); if (config.componentLib === undefined) { if (promptResponse.componentLib === "shadcn-ui") { @@ -158,20 +159,20 @@ export const addPackage = async ( // config.preferredPackageManager // ); // add to tailwindconfig - replaceFile("tailwind.config.ts", generateUpdatedTWConfig()); + await replaceFile("tailwind.config.ts", generateUpdatedTWConfig()); // add to globalcss colors - replaceFile( + await replaceFile( formatFilePath(shared.init.globalCss, { removeExtension: false, prefix: "rootPath", }), generateGlobalsCss() ); - updateConfigFile({ componentLib: null }); + await updateConfigFile({ componentLib: null }); } if (!config.t3) { - addContextProviderToAppLayout("Navbar"); + await addContextProviderToAppLayout("Navbar"); } } @@ -197,7 +198,7 @@ export const addPackage = async ( ); } if (promptResponse === null) - updateConfigFile({ orm: null, driver: null, provider: null }); + await updateConfigFile({ orm: null, driver: null, provider: null }); } // check if auth if (config.auth === undefined) { @@ -208,28 +209,29 @@ export const addPackage = async ( promptResponse.orm.slice(1); if (promptResponse.auth !== null && promptResponse.auth !== undefined) - createAuthLayoutFile(); + await createAuthLayoutFile(); if (promptResponse.auth === "next-auth") await addNextAuth(promptResponse.authProviders, options); if (promptResponse.auth === "clerk") await addClerk(); if (promptResponse.auth === "lucia") await addLucia(); if (promptResponse.auth === "kinde") await addKinde(); + if (promptResponse.auth === "supabase") await addSupabase(); if (promptResponse.auth === null || promptResponse.auth === undefined) { - replaceFile( + await replaceFile( formatFilePath(shared.init.dashboardRoute, { prefix: "rootPath", removeExtension: false, }), generateGenericHomepage() ); - updateConfigFile({ auth: null }); + await updateConfigFile({ auth: null }); } else { // add account page await createAccountSettingsPage(); addAuthCheckToAppLayout(); } - addNavbarAndSettings(); + await addNavbarAndSettings(); } // check if misc @@ -250,7 +252,7 @@ export const addPackage = async ( } if (config.t3 && config.auth === "next-auth") { - checkAndAddAuthUtils(); + await checkAndAddAuthUtils(); } spinner.text = "Finishing configuration"; diff --git a/src/commands/add/misc/defaultStyles/generators.ts b/src/commands/add/misc/defaultStyles/generators.ts index 5a785130..3278f62d 100644 --- a/src/commands/add/misc/defaultStyles/generators.ts +++ b/src/commands/add/misc/defaultStyles/generators.ts @@ -1,4 +1,3 @@ -import { warn } from "console"; import { existsSync, readFileSync } from "fs"; import { createFile, replaceFile } from "../../../../utils.js"; import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; @@ -149,7 +148,7 @@ const defaultAppLayout = `export default async function AppLayout({ return (
{children}
) }`; -export const createAppLayoutFile = () => { +export const createAppLayoutFile = async () => { const { shared } = getFilePaths(); const layoutPath = formatFilePath(shared.init.appLayout, { @@ -158,7 +157,7 @@ export const createAppLayoutFile = () => { }); const layoutExists = existsSync(layoutPath); - if (!layoutExists) createFile(layoutPath, defaultAppLayout); + if (!layoutExists) await createFile(layoutPath, defaultAppLayout); }; const defaultAuthLayout = ( @@ -181,7 +180,7 @@ export default async function AuthLayout({ } `; -export const createAuthLayoutFile = () => { +export const createAuthLayoutFile = async () => { const { shared } = getFilePaths(); const layoutPath = formatFilePath(shared.auth.layoutPage, { prefix: "rootPath", @@ -191,17 +190,25 @@ export const createAuthLayoutFile = () => { const layoutExists = existsSync(layoutPath); if (!layoutExists) - createFile(layoutPath, defaultAuthLayout(shared.auth.authUtils)); + await createFile(layoutPath, defaultAuthLayout(shared.auth.authUtils)); }; -const landingPage = `/** +const landingPage = () => { + const { shared } = getFilePaths(); + + return `/** * v0 by Vercel. * @see https://v0.dev/t/PmwTvNfrVgf * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app */ import Link from "next/link"; +import { getUserAuth } from "${formatFilePath(shared.auth.authUtils, { + removeExtension: true, + prefix: "alias", + })}"; -export default function LandingPage() { +export default async function LandingPage() { + const { session } = await getUserAuth(); return (
@@ -218,9 +225,9 @@ export default function LandingPage() { - Sign In + {session ? "Dashboard" : "Sign In"}
@@ -378,8 +385,9 @@ function MountainIcon(props: any) { ); } `; +}; -export const createLandingPage = () => { +export const createLandingPage = async () => { // need to make a check here to not replace things const rootPath = formatFilePath("app/page.tsx", { prefix: "rootPath", @@ -389,5 +397,5 @@ export const createLandingPage = () => { const alreadyUpdated = lpContent.indexOf("v0 by Vercel") === -1 ? false : true; - if (alreadyUpdated === false) replaceFile(rootPath, landingPage); + if (alreadyUpdated === false) await replaceFile(rootPath, landingPage()); }; diff --git a/src/commands/add/misc/navbar/generators.ts b/src/commands/add/misc/navbar/generators.ts index e0bcb7a8..46ef677c 100644 --- a/src/commands/add/misc/navbar/generators.ts +++ b/src/commands/add/misc/navbar/generators.ts @@ -6,11 +6,11 @@ import { createFile, readConfigFile } from "../../../../utils.js"; import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; -export const addNavbarAndSettings = () => { +export const addNavbarAndSettings = async () => { const { componentLib } = readConfigFile(); // create navbar - createFile( + await createFile( formatFilePath("components/Navbar.tsx", { removeExtension: false, prefix: "rootPath", @@ -19,7 +19,7 @@ export const addNavbarAndSettings = () => { ); // create sidebar - createFile( + await createFile( formatFilePath("components/Sidebar.tsx", { removeExtension: false, prefix: "rootPath", @@ -28,7 +28,7 @@ export const addNavbarAndSettings = () => { ); // create sidebaritems - createFile( + await createFile( formatFilePath("components/SidebarItems.tsx", { removeExtension: false, prefix: "rootPath", @@ -37,7 +37,7 @@ export const addNavbarAndSettings = () => { ); // create sidebaritems - createFile( + await createFile( formatFilePath("config/nav.ts", { removeExtension: false, prefix: "rootPath", @@ -48,7 +48,7 @@ export const addNavbarAndSettings = () => { // create settings page if (componentLib === "shadcn-ui") - createFile( + await createFile( formatFilePath("app/(app)/settings/page.tsx", { removeExtension: false, prefix: "rootPath", diff --git a/src/commands/add/misc/resend/index.ts b/src/commands/add/misc/resend/index.ts index 1c7e784a..a302e6cc 100644 --- a/src/commands/add/misc/resend/index.ts +++ b/src/commands/add/misc/resend/index.ts @@ -1,8 +1,6 @@ -import { consola } from "consola"; import { addPackageToConfig, createFile, - installPackages, readConfigFile, } from "../../../../utils.js"; import { AvailablePackage } from "../../../../types.js"; @@ -19,6 +17,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { rootPath, } = readConfigFile(); const { resend } = getFilePaths(); + // const packages = packagesBeingInstalled.concat(installedPackages); // consola.start("Installing Resend..."); @@ -31,7 +30,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { } = resendGenerators; // 1. Add page at app/resend/page.tsx - createFile( + await createFile( formatFilePath(resend.resendPage, { prefix: "rootPath", removeExtension: false, @@ -40,7 +39,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { ); // 2. Add component at components/emails/FirstEmailTemplate.tsx - createFile( + await createFile( formatFilePath(resend.firstEmailComponent, { prefix: "rootPath", removeExtension: false, @@ -48,7 +47,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { generateEmailTemplateComponent() ); // 3. Add route handler at app/api/email/route.ts - createFile( + await createFile( formatFilePath(resend.emailApiRoute, { prefix: "rootPath", removeExtension: false, @@ -57,7 +56,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { ); // 4. Add email utils - createFile( + await createFile( formatFilePath(resend.emailUtils, { prefix: "rootPath", removeExtension: false, @@ -66,7 +65,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { ); // 5. add email index.ts - createFile( + await createFile( formatFilePath(resend.libEmailIndex, { prefix: "rootPath", removeExtension: false, @@ -75,7 +74,7 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { ); // 6. Add items to .env - addToDotEnv([{ key: "RESEND_API_KEY", value: "" }], rootPath, true); + await addToDotEnv([{ key: "RESEND_API_KEY", value: "" }], rootPath, true); // 7. Install packages (resend) // await installPackages( // { @@ -89,6 +88,6 @@ export const addResend = async (packagesBeingInstalled: AvailablePackage[]) => { if (orm === null || orm === undefined) addToInstallList({ regular: ["zod", "@t3-oss/env-nextjs"], dev: [] }); - addPackageToConfig("resend"); + await addPackageToConfig("resend"); // consola.success("Resend successfully installed and configured."); }; diff --git a/src/commands/add/misc/stripe/index.ts b/src/commands/add/misc/stripe/index.ts index 5f1466a1..e3943cf3 100644 --- a/src/commands/add/misc/stripe/index.ts +++ b/src/commands/add/misc/stripe/index.ts @@ -16,8 +16,6 @@ import path from "path"; import { addPackageToConfig, createFile, - getFileLocations, - installPackages, readConfigFile, replaceFile, updateConfigFile, @@ -52,11 +50,11 @@ import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; import { libAuthUtilsTsWithoutAuthOptions } from "../../auth/next-auth/generators.js"; import { updateRootSchema } from "../../../generate/generators/model/utils.js"; import { AuthSubTypeMapping, addToInstallList } from "../../utils.js"; +import { addToSupabaseIgnoredRoutes } from "../../auth/supabase/utils.js"; export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { const { componentLib, - preferredPackageManager, rootPath, orm, driver, @@ -71,7 +69,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { if (orm === null || orm === undefined || driver === undefined) { consola.warn("You cannot install Stripe without an ORM installed."); - updateConfigFile({ orm: undefined }); + await updateConfigFile({ orm: undefined }); await addPackage(); return; } @@ -83,7 +81,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { const authUtilsExist = existsSync(authUtilsPath); if (!authUtilsExist) { - createFile(authUtilsPath, libAuthUtilsTsWithoutAuthOptions()); + await createFile(authUtilsPath, libAuthUtilsTsWithoutAuthOptions()); } } @@ -91,9 +89,14 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { addToClerkIgnoredRoutes("/api/webhooks/stripe"); } + // TODO: Find out if this is actually necessary for Supabase + if (auth === "supabase") { + addToSupabaseIgnoredRoutes("/api/webhooks/stripe"); + } + // add attributes to usermodel if (orm === "prisma") { - addToPrismaSchema( + await addToPrismaSchema( `model Subscription { userId String @unique${ authSubtype !== "managed" @@ -115,7 +118,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { } } if (orm === "drizzle") { - createFile( + await createFile( formatFilePath(stripe.subscriptionSchema, { prefix: "rootPath", removeExtension: false, @@ -123,12 +126,12 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateSubscriptionsDrizzleSchema(driver, auth) ); if (t3) { - updateRootSchema("subscriptions"); + await updateRootSchema("subscriptions"); } } // create stripe/index file - createFile( + await createFile( formatFilePath(stripe.stripeIndex, { prefix: "rootPath", removeExtension: false, @@ -136,7 +139,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateStripeIndexTs() ); // create stripe/subscription file - createFile( + await createFile( formatFilePath(stripe.stripeSubscription, { prefix: "rootPath", removeExtension: false, @@ -144,7 +147,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateStripeSubscriptionTs() ); // create config/subscriptions.ts - createFile( + await createFile( formatFilePath(stripe.configSubscription, { prefix: "rootPath", removeExtension: false, @@ -152,7 +155,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateConfigSubscriptionsTs() ); // components: create billing card - createFile( + await createFile( formatFilePath(stripe.accountPlanSettingsComponent, { prefix: "rootPath", removeExtension: false, @@ -160,7 +163,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateBillingCard() ); // components: create manage subscription button - createFile( + await createFile( formatFilePath(stripe.billingManageSubscriptionComponent, { prefix: "rootPath", removeExtension: false, @@ -169,7 +172,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { ); // components: create success toast if (componentLib === "shadcn-ui") - createFile( + await createFile( formatFilePath(stripe.billingSuccessToast, { prefix: "rootPath", removeExtension: false, @@ -178,7 +181,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { ); // add billingcard to accountpage with billing card TODO - replaceFile( + await replaceFile( formatFilePath(shared.auth.accountPage, { prefix: "rootPath", removeExtension: false, @@ -186,7 +189,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { createAccountPage(true) ); // create account/billing/page.tsx - createFile( + await createFile( formatFilePath(stripe.accountBillingPage, { prefix: "rootPath", removeExtension: false, @@ -194,7 +197,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateBillingPage() ); // create api/webhooks/route.ts - createFile( + await createFile( formatFilePath(stripe.stripeWebhooksApiRoute, { prefix: "rootPath", removeExtension: false, @@ -202,7 +205,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { generateStripeWebhook() ); // create api/billing/manage-subscription/route.ts - createFile( + await createFile( formatFilePath(stripe.manageSubscriptionApiRoute, { prefix: "rootPath", removeExtension: false, @@ -213,7 +216,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { addUtilToUtilsTs(rootPath); // add to dotenv - addToDotEnv( + await addToDotEnv( [ { key: "STRIPE_SECRET_KEY", value: "" }, { key: "STRIPE_WEBHOOK_SECRET", value: "" }, @@ -226,7 +229,7 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { ); // misc script updates - addListenScriptToPackageJson(); + await addListenScriptToPackageJson(); // install packages // await installPackages( // { dev: "", regular: "stripe @stripe/stripe-js lucide-react" }, @@ -238,10 +241,10 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { dev: [], }); - addPackageToConfig("stripe"); + await addPackageToConfig("stripe"); if (packages.includes("trpc")) { - createFile( + await createFile( formatFilePath(stripe.accountRouterTrpc, { removeExtension: false, prefix: "rootPath", @@ -249,11 +252,11 @@ export const addStripe = async (packagesBeingInstalled: AvailablePackage[]) => { createAccountTRPCRouter() ); // add to main trpc router - updateTRPCRouter("account"); + await updateTRPCRouter("account"); } }; -const addListenScriptToPackageJson = () => { +const addListenScriptToPackageJson = async () => { // Define the path to package.json const packageJsonPath = path.resolve("package.json"); @@ -276,12 +279,12 @@ const addListenScriptToPackageJson = () => { const updatedPackageJsonData = JSON.stringify(packageJson, null, 2); // Write the updated content back to package.json - replaceFile(packageJsonPath, updatedPackageJsonData); + await replaceFile(packageJsonPath, updatedPackageJsonData); // consola.success("Stripe listen script added to package.json"); }; -const addUtilToUtilsTs = (rootPath: string) => { +const addUtilToUtilsTs = async (rootPath: string) => { const { shared } = getFilePaths(); const utilContentToAdd = `export function absoluteUrl(path: string) { return \`\${ @@ -297,9 +300,9 @@ const addUtilToUtilsTs = (rootPath: string) => { const utilsContent = fs.readFileSync(utilsPath, "utf-8"); if (!utilsContent.includes(utilContentToAdd)) { const newUtilsContent = utilsContent.concat(`\n${utilContentToAdd}`); - replaceFile(utilsPath, newUtilsContent); + await replaceFile(utilsPath, newUtilsContent); } else return; } else { - createFile(utilsPath, utilContentToAdd); + await createFile(utilsPath, utilContentToAdd); } }; diff --git a/src/commands/add/misc/trpc/index.ts b/src/commands/add/misc/trpc/index.ts index 7e025a57..8ff23ced 100644 --- a/src/commands/add/misc/trpc/index.ts +++ b/src/commands/add/misc/trpc/index.ts @@ -24,7 +24,7 @@ export const addTrpc = async () => { const { orm } = readConfigFile(); const { trpc } = getFilePaths(); // 1. Create lib/server/index.ts - createFile( + await createFile( formatFilePath(trpc.rootRouter, { prefix: "rootPath", removeExtension: false, @@ -33,7 +33,7 @@ export const addTrpc = async () => { ); // 2. create lib/server/trpc.ts - createFile( + await createFile( formatFilePath(trpc.serverTrpc, { prefix: "rootPath", removeExtension: false, @@ -42,7 +42,7 @@ export const addTrpc = async () => { ); // 3. create lib/server/router/ directory and maybe a users file // TODO : T3 COMPATABILITY - createFile( + await createFile( formatFilePath(`lib/server/routers/computers.ts`, { prefix: "rootPath", removeExtension: false, @@ -50,7 +50,7 @@ export const addTrpc = async () => { serverRouterComputersTs() ); // 4. create app/api/trpc/[trpc]/route.ts - createFile( + await createFile( formatFilePath(trpc.trpcApiRoute, { prefix: "rootPath", removeExtension: false, @@ -58,7 +58,7 @@ export const addTrpc = async () => { apiTrpcRouteTs() ); // 5. create lib/trpc/client.ts - createFile( + await createFile( formatFilePath(trpc.trpcClient, { prefix: "rootPath", removeExtension: false, @@ -66,7 +66,7 @@ export const addTrpc = async () => { libTrpcClientTs() ); // 6. create lib/trpc/Provider.tsx - createFile( + await createFile( formatFilePath(trpc.trpcProvider, { prefix: "rootPath", removeExtension: false, @@ -74,8 +74,8 @@ export const addTrpc = async () => { libTrpcProviderTsx() ); // 7. create lib/trpc/serverClient.ts -> updated to lib/trpc/api.ts using server invoker - // createFile(`${rootPath}/lib/trpc/serverClient.ts`, libTrpcServerClientTs()); - createFile( + // await createFile(`${rootPath}/lib/trpc/serverClient.ts`, libTrpcServerClientTs()); + await createFile( formatFilePath(trpc.trpcApiTs, { prefix: "rootPath", removeExtension: false, @@ -85,7 +85,7 @@ export const addTrpc = async () => { ); // 7.5. create context file and update to include context file above - createFile( + await createFile( formatFilePath(trpc.trpcContext, { prefix: "rootPath", removeExtension: false, @@ -94,7 +94,7 @@ export const addTrpc = async () => { ); // create trpc utils file lib/trpc/utils.ts - createFile( + await createFile( formatFilePath(trpc.trpcUtils, { prefix: "rootPath", removeExtension: false, @@ -127,10 +127,10 @@ export const addTrpc = async () => { }); if (orm === null) addToInstallList({ regular: ["zod"], dev: [] }); - addPackageToConfig("trpc"); + await addPackageToConfig("trpc"); // 9. Instruct user to add the to their root layout. - addContextProviderToAppLayout("TrpcProvider"); - // addToDotEnv( + await addContextProviderToAppLayout("TrpcProvider"); + // await addToDotEnv( // [ // { // key: "VERCEL_URL", diff --git a/src/commands/add/orm/drizzle/generators.ts b/src/commands/add/orm/drizzle/generators.ts index cabf2014..9c0891a0 100644 --- a/src/commands/add/orm/drizzle/generators.ts +++ b/src/commands/add/orm/drizzle/generators.ts @@ -1,12 +1,6 @@ -import { consola } from "consola"; import fs from "fs"; import path from "path"; -import { - createFile, - installPackages, - readConfigFile, - replaceFile, -} from "../../../../utils.js"; +import { createFile, readConfigFile, replaceFile } from "../../../../utils.js"; import { DBProvider, DBType, @@ -18,10 +12,11 @@ import { formatFilePath, getDbIndexPath, getFilePaths, - removeFileExtension, } from "../../../filePaths/index.js"; import stripJsonComments from "strip-json-comments"; import { addToInstallList } from "../../utils.js"; +import { formatFileContentWithPrettier } from "../../../init/utils.js"; +// import consola from "consola"; type ConfigDriver = "pg" | "turso" | "libsql" | "mysql" | "better-sqlite"; @@ -38,13 +33,16 @@ const configDriverMappings = { turso: "turso", }; -export const createDrizzleConfig = (libPath: string, provider: DBProvider) => { +export const createDrizzleConfig = async ( + libPath: string, + provider: DBProvider +) => { const { shared: { init: { envMjs }, }, } = getFilePaths(); - createFile( + await createFile( "drizzle.config.ts", `import type { Config } from "drizzle-kit"; import { env } from "${formatFilePath(envMjs, { @@ -72,12 +70,11 @@ export default { ); }; -export const createIndexTs = (dbProvider: DBProvider) => { +export const createIndexTs = async (dbProvider: DBProvider) => { const { shared: { init: { envMjs }, }, - drizzle, } = getFilePaths(); const dbIndex = getDbIndexPath("drizzle"); let indexTS = ""; @@ -228,13 +225,13 @@ export const db = drizzle(sqlite); break; } - createFile( + await createFile( formatFilePath(dbIndex, { prefix: "rootPath", removeExtension: false }), indexTS ); }; -export const createMigrateTs = ( +export const createMigrateTs = async ( libPath: string, dbType: DBType, dbProvider: DBProvider @@ -424,13 +421,13 @@ runMigrate().catch((err) => { process.exit(1); });`; - createFile( + await createFile( formatFilePath(dbMigrate, { prefix: "rootPath", removeExtension: false }), template ); }; -export const createInitSchema = (libPath?: string, dbType?: DBType) => { +export const createInitSchema = async (libPath?: string, dbType?: DBType) => { const { packages, driver, rootPath } = readConfigFile(); const { shared: { @@ -518,10 +515,10 @@ export type NewComputer = z.infer; export type ComputerId = z.infer["id"];`; const finalDoc = `${sharedImports}\n${initModel}\n${sharedSchemas}`; - createFile(path, finalDoc); + await createFile(path, finalDoc); }; -export const addScriptsToPackageJson = ( +export const addScriptsToPackageJson = async ( libPath: string, driver: DBType, preferredPackageManager: PMType @@ -553,7 +550,10 @@ export const addScriptsToPackageJson = ( const updatedPackageJsonData = JSON.stringify(packageJson, null, 2); // Write the updated content back to package.json - fs.writeFileSync(packageJsonPath, updatedPackageJsonData); + fs.writeFileSync( + packageJsonPath, + await formatFileContentWithPrettier(updatedPackageJsonData, packageJsonPath) + ); // consola.success("Scripts added to package.json"); }; @@ -604,7 +604,7 @@ export const installDependencies = async ( } }; -export const createDotEnv = ( +export const createDotEnv = async ( orm: ORMType, preferredPackageManager: PMType, databaseUrl?: string, @@ -622,25 +622,32 @@ export const createDotEnv = ( const envPath = path.resolve(".env"); const envExists = fs.existsSync(envPath); if (!envExists) - createFile( + await createFile( ".env", `${ orm === "drizzle" && usingPlanetscale ? `# When using the PlanetScale driver with Drizzle, your connection string must end with ?ssl={"rejectUnauthorized":true} instead of ?sslaccept=strict.\n` : "" - }DATABASE_URL=${dburl}` + }DATABASE_URL=${dburl}`, + true ); const envmjsfilePath = formatFilePath(envMjs, { prefix: "rootPath", removeExtension: false, }); + const envMjsExists = fs.existsSync(envmjsfilePath); + if (!envMjsExists) - createFile(envmjsfilePath, generateEnvMjs(preferredPackageManager, orm)); + await createFile( + envmjsfilePath, + generateEnvMjs(preferredPackageManager, orm), + false + ); }; -export const addToDotEnv = ( +export const addToDotEnv = async ( items: DotEnvItem[], rootPathOld?: string, excludeDbUrlIfBlank = false @@ -651,17 +658,23 @@ export const addToDotEnv = ( init: { envMjs }, }, } = getFilePaths(); + // handling dotenv const envPath = path.resolve(".env"); const envExists = fs.existsSync(envPath); const newData = items.map((item) => `${item.key}=${item.value}`).join("\n"); + let content = newData; + if (envExists) { const envData = fs.readFileSync(envPath, "utf-8"); - const updatedEnvData = `${envData}\n${newData}`; - fs.writeFileSync(envPath, updatedEnvData); - } else { - fs.writeFileSync(envPath, newData); + content = `${envData}\n${newData}`; } + + fs.writeFileSync( + envPath, + await formatFileContentWithPrettier(content, envPath, true) + ); + // handling env.mjs const envmjsfilePath = formatFilePath(envMjs, { removeExtension: false, @@ -672,10 +685,11 @@ export const addToDotEnv = ( return; } if (!envMjsExists) - createFile( + await createFile( envmjsfilePath, generateEnvMjs(preferredPackageManager, orm, excludeDbUrlIfBlank) ); + let envmjsfileContents = fs.readFileSync(envmjsfilePath, "utf-8"); const formatItemForDotEnvMjs = (item: DotEnvItem) => @@ -700,20 +714,33 @@ export const addToDotEnv = ( .map(formatPublicItemForRuntimeEnv) .join("\n "); + const regex = /\s*},\n\s*client:\s*{\n/; + + const endofClientIndex = envmjsfileContents.match(regex); + const beforeClientBlockEnd = envmjsfileContents.lastIndexOf( + "\n", + endofClientIndex.index + ); + const endOfClientBlock = envmjsfileContents.slice(0, beforeClientBlockEnd); + const hasCommaBeforeClient = endOfClientBlock.endsWith(","); + // Construct the replacement string for both server and client sections - const replacementStr = ` ${serverItems}\n },\n client: {\n ${clientItems}`; + const replacementStr = `${hasCommaBeforeClient ? "" : ","}\n ${serverItems}\n },\n client: {\n ${clientItems}\n`; // Replace content using the known pattern - const regex = / },\n client: {\n/s; envmjsfileContents = envmjsfileContents.replace(regex, replacementStr); const runtimeEnvRegex = /experimental__runtimeEnv: {\n/s; envmjsfileContents = envmjsfileContents.replace( runtimeEnvRegex, - `experimental__runtimeEnv: {\n ${runtimeEnvItems}` + `experimental__runtimeEnv: {\n ${runtimeEnvItems}\n` ); + // Write the updated contents back to the file - fs.writeFileSync(envmjsfilePath, envmjsfileContents); + fs.writeFileSync( + envmjsfilePath, + await formatFileContentWithPrettier(envmjsfileContents, envmjsfilePath) + ); }; export async function updateTsConfigTarget() { @@ -721,7 +748,7 @@ export async function updateTsConfigTarget() { const tsConfigPath = path.join(process.cwd(), "tsconfig.json"); // Read the file - fs.readFile(tsConfigPath, "utf8", (err, data) => { + fs.readFile(tsConfigPath, "utf8", async (err, data) => { if (err) { console.error( `An error occurred while reading the tsconfig.json file: ${err}` @@ -740,14 +767,14 @@ export async function updateTsConfigTarget() { const updatedContent = JSON.stringify(tsConfig, null, 2); // 2 spaces indentation // Write the updated content back to the file - replaceFile(tsConfigPath, updatedContent); + await replaceFile(tsConfigPath, updatedContent); // consola.success( // "Updated tsconfig.json target to esnext to support Drizzle-Kit." // ); }); } -export function createQueriesAndMutationsFolders( +export async function createQueriesAndMutationsFolders( libPath: string, driver: DBType ) { @@ -837,14 +864,14 @@ export const deleteComputer = async (id: ComputerId) => { throw { error: message }; } };`; - createFile( + await createFile( formatFilePath("lib/api/computers/queries.ts", { removeExtension: false, prefix: "rootPath", }), query ); - createFile( + await createFile( formatFilePath("lib/api/computers/mutations.ts", { prefix: "rootPath", removeExtension: false, @@ -870,20 +897,19 @@ export const env = createEnv({ NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), - ${blank ? "// " : ""}DATABASE_URL: z.string().min(1), - + ${blank ? "// " : ""}DATABASE_URL: z.string().min(1) }, client: { - // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1), + // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1) }, // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually // runtimeEnv: { // DATABASE_URL: process.env.DATABASE_URL, - // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY // }, // For Next.js >= 13.4.4, you only need to destructure client variables: experimental__runtimeEnv: { - // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY }, }); `; diff --git a/src/commands/add/orm/drizzle/index.ts b/src/commands/add/orm/drizzle/index.ts index fccfc7ad..8baa64ce 100644 --- a/src/commands/add/orm/drizzle/index.ts +++ b/src/commands/add/orm/drizzle/index.ts @@ -48,21 +48,21 @@ export const addDrizzle = async ( // create all the files here if (includeExampleModel) { - createInitSchema(libPath, dbType); - createQueriesAndMutationsFolders(libPath, dbType); + await createInitSchema(libPath, dbType); + await createQueriesAndMutationsFolders(libPath, dbType); } else { createFolder(`${hasSrc ? "src/" : ""}lib/db/schema`); createFolder(`${hasSrc ? "src/" : ""}lib/api`); } // dependent on dbtype and driver, create - createIndexTs(dbProvider); - createMigrateTs(libPath, dbType, dbProvider); - createDrizzleConfig(libPath, dbProvider); + await createIndexTs(dbProvider); + await createMigrateTs(libPath, dbType, dbProvider); + await createDrizzleConfig(libPath, dbProvider); // perhaps using push rather than migrate for sqlite? addScriptsToPackageJson(libPath, dbType, preferredPackageManager); - createDotEnv( + await createDotEnv( "drizzle", preferredPackageManager, databaseUrl, @@ -70,7 +70,7 @@ export const addDrizzle = async ( hasSrc ? "src/" : "" ); if (dbProvider === "vercel-pg") - addToDotEnv( + await addToDotEnv( [ { key: "POSTGRES_URL", value: "" }, { key: "POSTGRES_URL_NON_POOLING", value: "" }, @@ -82,13 +82,17 @@ export const addDrizzle = async ( rootPath ); if (dbProvider === "turso") - addToDotEnv([{ key: "DATABASE_AUTH_TOKEN", value: "" }], rootPath); + await addToDotEnv([{ key: "DATABASE_AUTH_TOKEN", value: "" }], rootPath); await updateTsConfigTarget(); - addNanoidToUtils(); + await addNanoidToUtils(); - updateConfigFile({ driver: dbType, provider: dbProvider, orm: "drizzle" }); + await updateConfigFile({ + driver: dbType, + provider: dbProvider, + orm: "drizzle", + }); await installDependencies(dbProvider, preferredPackageManager); - addPackageToConfig("drizzle"); + await addPackageToConfig("drizzle"); }; diff --git a/src/commands/add/orm/drizzle/utils.ts b/src/commands/add/orm/drizzle/utils.ts index ed9e3611..3efd70d2 100644 --- a/src/commands/add/orm/drizzle/utils.ts +++ b/src/commands/add/orm/drizzle/utils.ts @@ -3,7 +3,7 @@ import { createFile, readConfigFile, replaceFile } from "../../../../utils.js"; import { consola } from "consola"; import { formatFilePath, getFilePaths } from "../../../filePaths/index.js"; -export function addToDrizzleModel( +export async function addToDrizzleModel( modelName: string, attributesToAdd: string, additionalImports?: string[] @@ -31,7 +31,7 @@ export function addToDrizzleModel( '} from "drizzle-orm', `${additionalImports.map((i) => `, ${i}`)} } from "drizzle-orm` ); - replaceFile(pathToModel, newSchemaWithUpdatedImports); + await replaceFile(pathToModel, newSchemaWithUpdatedImports); consola.info("Updated Drizzle schema"); } } @@ -49,7 +49,7 @@ const getDrizzleModelStartAndEnd = (schema: string, modelName: string) => { return { modelStart, modelEnd, modelExists }; }; -export const addNanoidToUtils = () => { +export const addNanoidToUtils = async () => { const nanoidContent = `import { customAlphabet } from "nanoid"; export const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789");`; const { shared } = getFilePaths(); @@ -59,18 +59,18 @@ export const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789");`; }); const utilsExists = existsSync(utilsPath); if (!utilsExists) { - createFile(utilsPath, nanoidContent); + await createFile(utilsPath, nanoidContent); } else { const utilsContent = readFileSync(utilsPath, "utf-8"); const newContent = `${nanoidContent.split("\n")[0].trim()} ${utilsContent} ${nanoidContent.split("\n")[1].trim()} `; - replaceFile(utilsPath, newContent); + await replaceFile(utilsPath, newContent); } }; -export const checkTimestampsInUtils = () => { +export const checkTimestampsInUtils = async () => { const timestampsContent = `export const timestamps: { createdAt: true; updatedAt: true } = { createdAt: true, updatedAt: true, @@ -83,14 +83,14 @@ export const checkTimestampsInUtils = () => { }); const utilsExists = existsSync(utilsPath); if (!utilsExists) { - createFile(utilsPath, timestampsContent); + await createFile(utilsPath, timestampsContent); } else { const utilsContent = readFileSync(utilsPath, "utf-8"); if (utilsContent.indexOf(timestampsContent) === -1) { const newContent = `${utilsContent} -${timestampsContent} -`; - replaceFile(utilsPath, newContent); + ${timestampsContent} + `; + await replaceFile(utilsPath, newContent); } } }; diff --git a/src/commands/add/orm/prisma/index.ts b/src/commands/add/orm/prisma/index.ts index e42fe77d..f150a1ed 100644 --- a/src/commands/add/orm/prisma/index.ts +++ b/src/commands/add/orm/prisma/index.ts @@ -37,12 +37,15 @@ export const addPrisma = async ( // if mysql, ask if planetscale if (dbType === "mysql") { // scaffold planetscale specific schema - createFile( + await createFile( `prisma/schema.prisma`, - generatePrismaSchema(dbType, initOptions.dbProvider === "planetscale") + await generatePrismaSchema( + dbType, + initOptions.dbProvider === "planetscale" + ) ); - updateConfigFile({ provider: "planetscale" }); - createDotEnv( + await updateConfigFile({ provider: "planetscale" }); + await createDotEnv( "prisma", preferredPackageManager, generateDbUrl(dbType), @@ -51,8 +54,11 @@ export const addPrisma = async ( ); } else { // create prisma/schema.prisma (with db type) - createFile(`prisma/schema.prisma`, generatePrismaSchema(dbType, false)); - createDotEnv( + await createFile( + `prisma/schema.prisma`, + await generatePrismaSchema(dbType, false) + ); + await createDotEnv( "prisma", preferredPackageManager, generateDbUrl(dbType), @@ -64,7 +70,7 @@ export const addPrisma = async ( // create .env with database_url // generate prisma global instance - createFile( + await createFile( formatFilePath(dbIndex, { prefix: "rootPath", removeExtension: false, @@ -78,7 +84,7 @@ export const addPrisma = async ( // create all the files here if (includeExampleModel) { - addToPrismaSchema( + await addToPrismaSchema( `model Computer { id String @id @default(cuid()) brand String @@ -87,17 +93,17 @@ export const addPrisma = async ( "Computer" ); // generate /lib/db/schema/computers.ts - createFile( + await createFile( `${rootPath}lib/db/schema/computers.ts`, generatePrismaComputerModel() ); // generate /lib/api/computers/queries.ts && /lib/api/computers/mutations.ts - createFile( + await createFile( `${rootPath}lib/api/computers/queries.ts`, generatePrismaComputerQueries() ); - createFile( + await createFile( `${rootPath}lib/api/computers/mutations.ts`, generatePrismaComputerMutations() ); @@ -106,7 +112,7 @@ export const addPrisma = async ( createFolder(`${hasSrc ? "src/" : ""}lib/api`); } - addScriptsToPackageJsonForPrisma(dbType); + await addScriptsToPackageJsonForPrisma(dbType); // install packages: regular: [] dev: [prisma, zod-prisma] // await installPackages( @@ -121,8 +127,8 @@ export const addPrisma = async ( // run prisma generate if (includeExampleModel) await prismaGenerate(preferredPackageManager); - addPackageToConfig("prisma"); - updateConfigFile({ orm: "prisma", driver: dbType }); + await addPackageToConfig("prisma"); + await updateConfigFile({ orm: "prisma", driver: dbType }); // consola.success("Prisma has been added to your project!"); }; diff --git a/src/commands/add/orm/prisma/utils.ts b/src/commands/add/orm/prisma/utils.ts index 9c55a6e3..cb9cc261 100644 --- a/src/commands/add/orm/prisma/utils.ts +++ b/src/commands/add/orm/prisma/utils.ts @@ -1,6 +1,7 @@ import path from "path"; import fs from "fs"; import { DBType } from "../../../../types.js"; +import { formatFileContentWithPrettier } from "../../../init/utils.js"; export const prismaDbTypeMappings: { [key in DBType]: string } = { pg: "postgresql", @@ -8,7 +9,7 @@ export const prismaDbTypeMappings: { [key in DBType]: string } = { sqlite: "sqlite", }; -export const addScriptsToPackageJsonForPrisma = (driver: DBType) => { +export const addScriptsToPackageJsonForPrisma = async (driver: DBType) => { // Define the path to package.json const packageJsonPath = path.resolve("package.json"); @@ -35,7 +36,10 @@ export const addScriptsToPackageJsonForPrisma = (driver: DBType) => { const updatedPackageJsonData = JSON.stringify(packageJson, null, 2); // Write the updated content back to package.json - fs.writeFileSync(packageJsonPath, updatedPackageJsonData); + fs.writeFileSync( + packageJsonPath, + await formatFileContentWithPrettier(updatedPackageJsonData, packageJsonPath) + ); // consola.success("Scripts added to package.json"); }; diff --git a/src/commands/add/orm/utils.ts b/src/commands/add/orm/utils.ts index d092612c..572fc2c1 100644 --- a/src/commands/add/orm/utils.ts +++ b/src/commands/add/orm/utils.ts @@ -72,6 +72,6 @@ export async function updateTsConfigPrismaTypeAlias() { const updatedContent = JSON.stringify(tsConfig, null, 2); // 2 spaces indentation // Write the updated content back to the file - replaceFile(tsConfigPath, updatedContent); + await replaceFile(tsConfigPath, updatedContent); // consola.success("Updated tsconfig.json to support zod-prisma type alias."); } diff --git a/src/commands/add/utils.ts b/src/commands/add/utils.ts index b60d360a..d128e6f1 100644 --- a/src/commands/add/utils.ts +++ b/src/commands/add/utils.ts @@ -35,6 +35,7 @@ export const Packages: { { name: "Clerk", value: "clerk" }, { name: "Lucia", value: "lucia" }, { name: "Kinde", value: "kinde" }, + { name: "Supabase", value: "supabase" }, ], misc: [ { name: "TRPC", value: "trpc" }, @@ -44,7 +45,9 @@ export const Packages: { componentLib: [{ name: "Shadcn UI (with next-themes)", value: "shadcn-ui" }], }; -export const addContextProviderToRootLayout = (provider: "ThemeProvider") => { +export const addContextProviderToRootLayout = async ( + provider: "ThemeProvider" +) => { const { hasSrc, alias } = readConfigFile(); const path = `${hasSrc ? "src/" : ""}app/layout.tsx`; @@ -87,10 +90,10 @@ export const addContextProviderToRootLayout = (provider: "ThemeProvider") => { searchValue, replacementText ); - replaceFile(path, newLayoutContent); + await replaceFile(path, newLayoutContent); }; -export const addAuthCheckToAppLayout = () => { +export const addAuthCheckToAppLayout = async () => { const { hasSrc } = readConfigFile(); const path = `${hasSrc ? "src/" : ""}app/(app)/layout.tsx`; const { shared } = getFilePaths(); @@ -109,9 +112,9 @@ export const addAuthCheckToAppLayout = () => { `; const newText = importStatement + fileContent.replace(searchText, replacementText); - replaceFile(path, newText); + await replaceFile(path, newText); }; -export const addContextProviderToAppLayout = ( +export const addContextProviderToAppLayout = async ( provider: | "NextAuthProvider" | "TrpcProvider" @@ -205,10 +208,10 @@ export const addContextProviderToAppLayout = ( searchValue, replacementText ); - replaceFile(path, newLayoutContent); + await replaceFile(path, newLayoutContent); }; -export const addContextProviderToAuthLayout = ( +export const addContextProviderToAuthLayout = async ( provider: | "NextAuthProvider" | "TrpcProvider" @@ -279,7 +282,7 @@ export const addContextProviderToAuthLayout = ( searchValue, replacementText ); - replaceFile(path, newLayoutContent); + await replaceFile(path, newLayoutContent); }; export const AuthSubTypeMapping: Record = { @@ -287,6 +290,7 @@ export const AuthSubTypeMapping: Record = { kinde: "managed", "next-auth": "self-hosted", lucia: "managed", + supabase: "managed", }; const installList: { regular: string[]; dev: string[] } = { @@ -368,6 +372,9 @@ export const printNextSteps = ( ...(promptResponses.auth === "clerk" ? [`${chalk.underline("Authentication")}: Clerk`] : []), + ...(promptResponses.auth === "supabase" + ? [`${chalk.underline("Authentication")}: Supabase`] + : []), ...(promptResponses.auth === "lucia" ? [`${chalk.underline("Authentication")}: Lucia`] : []), diff --git a/src/commands/filePaths/index.ts b/src/commands/filePaths/index.ts index d9d20952..3eb39b5b 100644 --- a/src/commands/filePaths/index.ts +++ b/src/commands/filePaths/index.ts @@ -15,6 +15,7 @@ export const paths: { t3: Paths; normal: Paths } = { schemaDir: "lib/db/schema", }, auth: { + authActions: "lib/auth/actions.ts", authUtils: "lib/auth/utils.ts", accountPage: "app/(app)/account/page.tsx", authSchema: "lib/db/schema/auth.ts", @@ -62,6 +63,15 @@ export const paths: { t3: Paths; normal: Paths } = { formErrorComponent: "components/auth/AuthFormError.tsx", signOutButtonComponent: "components/auth/SignOutBtn.tsx", }, + supabase: { + authFormComponent: "components/auth/Form.tsx", + signInPage: "app/(auth)/sign-in/page.tsx", + signUpPage: "app/(auth)/sign-up/page.tsx", + signOutButtonComponent: "components/auth/SignOutBtn.tsx", + callbackApiRoute: "app/api/auth/callback/route.ts", + libSupabaseAuthHelpers: "lib/supabase/auth/helper.ts", + middleware: "middleware.ts", + }, kinde: { routeHandler: "app/api/auth/[kindeAuth]/route.ts", signInPage: "app/(auth)/sign-in/page.tsx", @@ -109,6 +119,7 @@ export const paths: { t3: Paths; normal: Paths } = { schemaDir: "server/db/schema", }, auth: { + authActions: "lib/auth/actions.ts", authUtils: "lib/auth/utils.ts", accountPage: "app/(app)/account/page.tsx", authSchema: "server/db/schema/auth.ts", @@ -156,6 +167,15 @@ export const paths: { t3: Paths; normal: Paths } = { formErrorComponent: "components/auth/AuthFormError.tsx", signOutButtonComponent: "components/auth/SignOutBtn.tsx", }, + supabase: { + authFormComponent: "components/auth/Form.tsx", + signInPage: "app/(auth)/sign-in/page.tsx", + signUpPage: "app/(auth)/sign-up/page.tsx", + signOutButtonComponent: "components/auth/SignOutBtn.tsx", + callbackApiRoute: "app/api/auth/callback/route.ts", + libSupabaseAuthHelpers: "lib/supabase/auth/helper.ts", + middleware: "middleware.ts", + }, kinde: { routeHandler: "app/api/auth/[kindeAuth]/route.ts", signInPage: "app/(auth)/sign-in/page.tsx", diff --git a/src/commands/filePaths/types.d.ts b/src/commands/filePaths/types.d.ts index a3136cb9..ef8147c0 100644 --- a/src/commands/filePaths/types.d.ts +++ b/src/commands/filePaths/types.d.ts @@ -23,6 +23,7 @@ export type Paths = { schemaDir?: string; }; auth: { + authActions: string; authUtils: string; signInComponent: string; accountApiRoute: string; @@ -54,6 +55,15 @@ export type Paths = { signOutButtonComponent: string; formErrorComponent: string; }; + supabase: { + signInPage: string; + signUpPage: string; + authFormComponent: string; + callbackApiRoute: string; + libSupabaseAuthHelpers: string; + signOutButtonComponent: string; + middleware: string; + }; kinde: { routeHandler: string; signInPage: string; diff --git a/src/commands/generate/generators/apiRoute.ts b/src/commands/generate/generators/apiRoute.ts index 4c6cd21d..0db519ac 100644 --- a/src/commands/generate/generators/apiRoute.ts +++ b/src/commands/generate/generators/apiRoute.ts @@ -4,13 +4,13 @@ import { formatFilePath, getFilePaths } from "../../filePaths/index.js"; import { Schema } from "../types.js"; import { formatTableName, toCamelCase } from "../utils.js"; -export const scaffoldAPIRoute = (schema: Schema) => { +export const scaffoldAPIRoute = async (schema: Schema) => { const { hasSrc, driver } = readConfigFile(); const { tableName } = schema; const path = `${hasSrc ? "src/" : ""}app/api/${toCamelCase( tableName )}/route.ts`; - createFile(path, generateRouteContent(schema, driver)); + await createFile(path, generateRouteContent(schema, driver)); }; const generateRouteContent = (schema: Schema, driver: DBType) => { diff --git a/src/commands/generate/generators/model/index.ts b/src/commands/generate/generators/model/index.ts index 2d170296..b0eb9db1 100644 --- a/src/commands/generate/generators/model/index.ts +++ b/src/commands/generate/generators/model/index.ts @@ -1,8 +1,15 @@ -import { consola } from "consola"; +// import { consola } from "consola"; import { DBType } from "../../../../types.js"; -import { createFile, readConfigFile, replaceFile } from "../../../../utils.js"; +import { + createFile, + readConfigFile, + // replaceFile +} from "../../../../utils.js"; import { prismaFormat, prismaGenerate } from "../../../add/orm/utils.js"; -import { ExtendedSchema, Schema } from "../../types.js"; +import { + ExtendedSchema, + // Schema +} from "../../types.js"; import { toCamelCase } from "../../utils.js"; import { generateMutationContent } from "./mutations/index.js"; import { generateQueryContent } from "./queries/index.js"; @@ -12,7 +19,7 @@ import { generateServiceFileNames, getFilePaths, } from "../../../filePaths/index.js"; -import { existsSync, readFileSync } from "fs"; +// import { existsSync, readFileSync } from "fs"; import { updateRootSchema } from "./utils.js"; export async function scaffoldModel( @@ -22,17 +29,20 @@ export async function scaffoldModel( ) { const { tableName } = schema; const { orm, preferredPackageManager, driver, t3 } = readConfigFile(); - const { shared, drizzle } = getFilePaths(); + const { + shared, + // drizzle + } = getFilePaths(); const serviceFileNames = generateServiceFileNames(toCamelCase(tableName)); const modelPath = `${formatFilePath(shared.orm.schemaDir, { prefix: "rootPath", removeExtension: false, })}/${toCamelCase(tableName)}.ts`; - createFile(modelPath, generateModelContent(schema, dbType)); + await createFile(modelPath, await generateModelContent(schema, dbType)); if (t3 && orm === "drizzle") { - updateRootSchema(tableName); + await updateRootSchema(tableName); } if (orm === "prisma") { @@ -41,10 +51,13 @@ export async function scaffoldModel( } // create queryFile - createFile(serviceFileNames.queriesPath, generateQueryContent(schema, orm)); + await createFile( + serviceFileNames.queriesPath, + generateQueryContent(schema, orm) + ); // create mutationFile - createFile( + await createFile( serviceFileNames.mutationsPath, generateMutationContent(schema, driver, orm) ); diff --git a/src/commands/generate/generators/model/schema/index.ts b/src/commands/generate/generators/model/schema/index.ts index 53dbce2a..22d670c6 100644 --- a/src/commands/generate/generators/model/schema/index.ts +++ b/src/commands/generate/generators/model/schema/index.ts @@ -301,7 +301,7 @@ const generateIndexFields = ( .join("\n ")}`; }; -const generatePrismaSchema = ( +const generatePrismaSchema = async ( schema: Schema, mappings: TypeMap, zodSchemas: string, @@ -333,22 +333,23 @@ const generatePrismaSchema = ( schema.includeTimestamps ? generateTimestampFieldsPrisma() : "" } }`; - addToPrismaSchema(prismaSchemaContent, tableNameSingularCapitalised); + await addToPrismaSchema(prismaSchemaContent, tableNameSingularCapitalised); if (schema.belongsToUser && authSubtype === "self-hosted") - addToPrismaModel( + await addToPrismaModel( "User", `${tableNameCamelCase} ${tableNameSingularCapitalised}[]` ); - relations.forEach((relation) => { + relations.forEach(async (relation) => { const { references } = relation; const { tableNameSingularCapitalised: singularCapitalised } = formatTableName(references); - addToPrismaModel( + await addToPrismaModel( singularCapitalised, `${tableNameCamelCase} ${tableNameSingularCapitalised}[]` ); }); + const importStatement = generateImportStatement( "prisma", schema, @@ -359,11 +360,11 @@ const generatePrismaSchema = ( return `${importStatement}\n\n${zodSchemas}`; }; -export function generateModelContent(schema: Schema, dbType: DBType) { +export async function generateModelContent(schema: Schema, dbType: DBType) { const { provider, orm, auth } = readConfigFile(); const mappings = createOrmMappings()[orm][dbType]; const zodSchemas = createZodSchemas(schema, orm); - if (schema.includeTimestamps) checkTimestampsInUtils(); + if (schema.includeTimestamps) await checkTimestampsInUtils(); if (orm === "drizzle") { return generateDrizzleSchema( @@ -376,7 +377,7 @@ export function generateModelContent(schema: Schema, dbType: DBType) { ); } if (orm === "prisma") { - return generatePrismaSchema( + return await generatePrismaSchema( schema, mappings, zodSchemas, diff --git a/src/commands/generate/generators/model/utils.ts b/src/commands/generate/generators/model/utils.ts index beaa5803..309cf5fa 100644 --- a/src/commands/generate/generators/model/utils.ts +++ b/src/commands/generate/generators/model/utils.ts @@ -144,7 +144,7 @@ export const authForWhereClausePrisma = (belongsToUser: boolean) => { return belongsToUser ? ", userId: session?.user.id!" : ""; }; -export const updateRootSchema = ( +export const updateRootSchema = async ( tableName: string, usingAuth?: boolean, auth?: AuthType @@ -165,6 +165,8 @@ export const updateRootSchema = ( break; case "kinde": break; + case "supabase": + break; case "lucia": tableNames = "keys, users, sessions"; break; @@ -194,10 +196,10 @@ export const updateRootSchema = ( const afterImport = rootSchemaWithNewExport.slice(nextLineAfterLastImport); const withNewImport = `${beforeImport}${newImportStatement}${afterImport}`; - replaceFile(rootSchemaPath, withNewImport); + await replaceFile(rootSchemaPath, withNewImport); } else { // if not create schema/_root.ts -> then do same import as above - createFile( + await createFile( rootSchemaPath, `${newImportStatement} @@ -218,7 +220,7 @@ import * as extended from "~/server/db/schema/_root";` `{ schema }`, `{ schema: { ...schema, ...extended } }` ); - replaceFile(indexDbPath, updatedContentsFinal); + await replaceFile(indexDbPath, updatedContentsFinal); // update drizzle config file to add all in server/db/* const drizzleConfigPath = "drizzle.config.ts"; @@ -227,6 +229,6 @@ import * as extended from "~/server/db/schema/_root";` `schema: "./src/server/db/schema.ts",`, `schema: "./src/server/db/*",` ); - replaceFile(drizzleConfigPath, updatedContents); + await replaceFile(drizzleConfigPath, updatedContents); } }; diff --git a/src/commands/generate/generators/model/views-shared.ts b/src/commands/generate/generators/model/views-shared.ts index df3d1038..f4d3313d 100644 --- a/src/commands/generate/generators/model/views-shared.ts +++ b/src/commands/generate/generators/model/views-shared.ts @@ -3,7 +3,7 @@ import { formatFilePath } from "../../../filePaths/index.js"; import { formatTableName } from "../../utils.js"; import { replaceFile } from "../../../../utils.js"; -export const addLinkToSidebar = (tableName: string) => { +export const addLinkToSidebar = async (tableName: string) => { const { tableNameKebabCase, tableNameNormalEnglishCapitalised } = formatTableName(tableName); const sidebarConfigPath = formatFilePath("config/nav.ts", { @@ -48,5 +48,5 @@ export const addLinkToSidebar = (tableName: string) => { `; newContent = configContents.replace(searchQuery, replacement); } - replaceFile(sidebarConfigPath, newContent); + await replaceFile(sidebarConfigPath, newContent); }; diff --git a/src/commands/generate/generators/serverActions.ts b/src/commands/generate/generators/serverActions.ts index 96d18702..b324010e 100644 --- a/src/commands/generate/generators/serverActions.ts +++ b/src/commands/generate/generators/serverActions.ts @@ -3,14 +3,14 @@ import { formatFilePath } from "../../filePaths/index.js"; import { Schema } from "../types.js"; import { formatTableName } from "../utils.js"; -export const scaffoldServerActions = (schema: Schema) => { +export const scaffoldServerActions = async (schema: Schema) => { const { tableName } = schema; const { tableNameCamelCase } = formatTableName(tableName); const path = formatFilePath(`lib/actions/${tableNameCamelCase}.ts`, { prefix: "rootPath", removeExtension: false, }); - createFile(path, generateRouteContent(schema)); + await createFile(path, generateRouteContent(schema)); }; const generateRouteContent = (schema: Schema) => { diff --git a/src/commands/generate/generators/trpcRoute.ts b/src/commands/generate/generators/trpcRoute.ts index 0e8a12cf..3a2fe0ac 100644 --- a/src/commands/generate/generators/trpcRoute.ts +++ b/src/commands/generate/generators/trpcRoute.ts @@ -20,9 +20,9 @@ export const scaffoldTRPCRoute = async (schema: Schema) => { prefix: "rootPath", removeExtension: false, })}/${tableNameCamelCase}.ts`; - createFile(path, generateRouteContent(schema)); + await createFile(path, generateRouteContent(schema)); - updateTRPCRouter(tableNameCamelCase); + await updateTRPCRouter(tableNameCamelCase); }; // function updateTRPCRouterOld(routerName: string): void { @@ -46,13 +46,13 @@ export const scaffoldTRPCRoute = async (schema: Schema) => { // const beforeRouter = modifiedImportContent.slice(0, beforeRouterBlockEnd); // const afterRouter = modifiedImportContent.slice(beforeRouterBlockEnd); // const modifiedRouterContent = `${beforeRouter}\n ${routerName}: ${routerName}Router,${afterRouter}`; -// replaceFile(filePath, modifiedRouterContent); +// await replaceFile(filePath, modifiedRouterContent); // // consola.success(`Added ${routerName} router to root router successfully.`); // } -export function updateTRPCRouter(routerName: string): void { - const { hasSrc, t3, rootPath } = readConfigFile(); +export async function updateTRPCRouter(routerName: string) { + const { t3, rootPath } = readConfigFile(); const { trpcRootDir, rootRouterRelativePath } = getFileLocations(); const filePath = rootPath.concat(`${trpcRootDir}${rootRouterRelativePath}`); @@ -98,18 +98,19 @@ export function updateTRPCRouter(routerName: string): void { modifiedRouterContent = withNewImport.replace(oldRouter, newRouter); } else { // Regular multi-line router - const routerBlockEnd = withNewImport.indexOf("});"); + const routerBlockEnd = withNewImport.indexOf("})"); const beforeRouterBlockEnd = withNewImport.lastIndexOf( "\n", routerBlockEnd ); const beforeRouter = withNewImport.slice(0, beforeRouterBlockEnd); + const hasCommaBefore = beforeRouter.endsWith(","); const afterRouter = withNewImport.slice(beforeRouterBlockEnd); const newRouterStatement = `\n ${routerName}: ${routerName}Router,`; - modifiedRouterContent = `${beforeRouter}${newRouterStatement}${afterRouter}`; + modifiedRouterContent = `${beforeRouter}${hasCommaBefore ? "" : ","}${newRouterStatement}${afterRouter}`; } - replaceFile(filePath, modifiedRouterContent); + await replaceFile(filePath, modifiedRouterContent); // consola.success( // `Added '${routerName}' router to the root tRPC router successfully.` diff --git a/src/commands/generate/generators/views-with-server-actions.ts b/src/commands/generate/generators/views-with-server-actions.ts index bacf0c15..8edde7b4 100644 --- a/src/commands/generate/generators/views-with-server-actions.ts +++ b/src/commands/generate/generators/views-with-server-actions.ts @@ -51,19 +51,19 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( // require trpc for these views if (packages.includes("shadcn-ui")) { // check if utils correct - checkUtils(); + await checkUtils(); // check if modal exists components/shared/Modal.tsx - checkModalExists(); + await checkModalExists(); // check if modal exists components/shared/BackButton.tsx - checkBackButtonExists(); + await checkBackButtonExists(); // check if vfh exists - checkValidatedForm(); + await checkValidatedForm(); // create view - tableName/page.tsx - createFile( + await createFile( formatFilePath(`app/(app)/${tableNameKebabCase}/page.tsx`, { prefix: "rootPath", removeExtension: false, @@ -72,7 +72,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( ); // create components/tableName/TableNameList.tsx - createFile( + await createFile( formatFilePath( `components/${tableNameCamelCase}/${tableNameSingularCapitalised}List.tsx`, { removeExtension: false, prefix: "rootPath" } @@ -81,7 +81,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( ); // create components/tableName/TableNameForm.tsx - createFile( + await createFile( formatFilePath( `components/${tableNameCamelCase}/${tableNameSingularCapitalised}Form.tsx`, { prefix: "rootPath", removeExtension: false } @@ -90,7 +90,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( ); // create optimisticEntity - createFile( + await createFile( formatFilePath( `app/(app)/${tableNameKebabCase}/useOptimistic${tableNameCapitalised}.tsx`, { @@ -102,7 +102,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( ); // create tableName/[id]/page.tsx - createFile( + await createFile( formatFilePath( `app/(app)/${tableNameKebabCase}/[${tableNameSingular}Id]/page.tsx`, { removeExtension: false, prefix: "rootPath" } @@ -118,7 +118,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( return `${parent.tableNameKebabCase}/[${parent.tableNameSingular}Id]/`; }) .join(""); - createFile( + await createFile( formatFilePath( `app/(app)/${baseUrl}${tableNameKebabCase}/[${tableNameSingular}Id]/page.tsx`, { removeExtension: false, prefix: "rootPath" } @@ -127,7 +127,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( ); } // create tableName/[id]/OptimisticEntity.tsx - createFile( + await createFile( formatFilePath( `app/(app)/${tableNameKebabCase}/[${tableNameSingular}Id]/Optimistic${tableNameSingularCapitalised}.tsx`, { removeExtension: false, prefix: "rootPath" } @@ -157,7 +157,7 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( // await installShadcnUIComponents(baseComponents); addToShadcnComponentList(baseComponents); } else { - addPackage(); + await addPackage(); } }; @@ -1068,7 +1068,7 @@ const SaveButton = ({ `; }; -const checkUtils = () => { +const checkUtils = async () => { const utilTsPath = formatFilePath("lib/utils.ts", { removeExtension: false, prefix: "rootPath", @@ -1085,15 +1085,13 @@ export type OptimisticAction = { data: T; }; `; - if (utilTsContent.includes(contentToQuery)) { - return; - } else { - const newUtilTs = utilTsContent.concat("\n\n".concat(contentToQuery)); - replaceFile(utilTsPath, newUtilTs); - } + if (utilTsContent.includes(contentToQuery)) return; + + const newUtilTs = utilTsContent.concat("\n\n".concat(contentToQuery)); + await replaceFile(utilTsPath, newUtilTs); }; -const checkModalExists = () => { +const checkModalExists = async () => { const modalPath = formatFilePath("components/shared/Modal.tsx", { removeExtension: false, prefix: "rootPath", @@ -1134,11 +1132,11 @@ export default function Modal({ ); } `; - createFile(modalPath, modalContents); + await createFile(modalPath, modalContents); } }; -const checkBackButtonExists = () => { +const checkBackButtonExists = async () => { const bbPath = formatFilePath("components/shared/BackButton.tsx", { removeExtension: false, prefix: "rootPath", @@ -1184,7 +1182,7 @@ export function BackButton({ ); } `; - createFile(bbPath, bbContents); + await createFile(bbPath, bbContents); } }; @@ -1309,7 +1307,7 @@ export const useOptimistic${tableNamePluralCapitalised} = ( `; }; -const checkValidatedForm = () => { +const checkValidatedForm = async () => { const vfhPath = formatFilePath("lib/hooks/useValidatedForm.tsx", { prefix: "rootPath", removeExtension: false, @@ -1356,8 +1354,9 @@ export function useValidatedForm(insertEntityZodSchema: ZodSchema) { `; const vfhExists = existsSync(vfhPath); + if (!vfhExists) { - createFile(vfhPath, vfhContent); + await createFile(vfhPath, vfhContent); } }; diff --git a/src/commands/generate/generators/views.ts b/src/commands/generate/generators/views.ts index 926cbd51..4e3623c7 100644 --- a/src/commands/generate/generators/views.ts +++ b/src/commands/generate/generators/views.ts @@ -1,11 +1,6 @@ import { DBField } from "../../../types.js"; import pluralize from "pluralize"; -import { - createFile, - getFileContents, - installShadcnUIComponents, - readConfigFile, -} from "../../../utils.js"; +import { createFile, getFileContents, readConfigFile } from "../../../utils.js"; import { addPackage } from "../../add/index.js"; import { formatFilePath, getFilePaths } from "../../filePaths/index.js"; import { Schema } from "../types.js"; @@ -28,26 +23,26 @@ export const scaffoldViewsAndComponents = async (schema: Schema) => { if (packages.includes("trpc") && packages.includes("shadcn-ui")) { // create view - tableName/page.tsx const rootPath = hasSrc ? "src/" : ""; - createFile( + await createFile( rootPath.concat(`app/(app)/${tableNameKebabCase}/page.tsx`), generateView(schema) ); // create components/tableName/TableNameList.tsx - createFile( + await createFile( rootPath.concat( `components/${tableNameCamelCase}/${tableNameSingularCapitalised}List.tsx` ), createListComponent(schema) ); // create components/tableName/TableNameForm.tsx - createFile( + await createFile( rootPath.concat( `components/${tableNameCamelCase}/${tableNameSingularCapitalised}Form.tsx` ), createFormComponent(schema) ); // create components/tableName/TableNameModal.tsx - createFile( + await createFile( rootPath.concat( `components/${tableNameCamelCase}/${tableNameSingularCapitalised}Modal.tsx` ), diff --git a/src/commands/generate/index.ts b/src/commands/generate/index.ts index 86134345..6e2a13ba 100644 --- a/src/commands/generate/index.ts +++ b/src/commands/generate/index.ts @@ -336,7 +336,7 @@ async function askForChildModel(parentModel: string) { }); } -export function preBuild() { +export async function preBuild() { const config = readConfigFile(); if (!config) { @@ -345,7 +345,7 @@ export function preBuild() { return false; } - if (config.orm === undefined) updateConfigFileAfterUpdate(); + if (config.orm === undefined) await updateConfigFileAfterUpdate(); return true; } @@ -487,23 +487,24 @@ async function generateResources( message: `Would you like to add a link to '${tnEnglish}' in your sidebar?`, default: true, }); - if (addToSidebar) addLinkToSidebar(schema.tableName); + if (addToSidebar) await addLinkToSidebar(schema.tableName); } if (resourceType.includes("model")) scaffoldModel(schema, config.driver, config.hasSrc); - if (resourceType.includes("api_route")) scaffoldAPIRoute(schema); - if (resourceType.includes("trpc_route")) scaffoldTRPCRoute(schema); + if (resourceType.includes("api_route")) await scaffoldAPIRoute(schema); + if (resourceType.includes("trpc_route")) await scaffoldTRPCRoute(schema); if (resourceType.includes("views_and_components_trpc")) - scaffoldViewsAndComponents(schema); - if (resourceType.includes("server_actions")) scaffoldServerActions(schema); + await scaffoldViewsAndComponents(schema); + if (resourceType.includes("server_actions")) + await scaffoldServerActions(schema); if (resourceType.includes("views_and_components_server_actions")) - scaffoldViewsAndComponentsWithServerActions(schema); + await scaffoldViewsAndComponentsWithServerActions(schema); await installShadcnComponentList(); } export async function buildSchema() { - const ready = preBuild(); + const ready = await preBuild(); if (!ready) return; const config = readConfigFile(); diff --git a/src/commands/generate/utils.ts b/src/commands/generate/utils.ts index 499f871d..2750bdac 100644 --- a/src/commands/generate/utils.ts +++ b/src/commands/generate/utils.ts @@ -330,7 +330,7 @@ export function getCurrentSchemas() { } } -export const addToPrismaSchema = (schema: string, modelName: string) => { +export const addToPrismaSchema = async (schema: string, modelName: string) => { const schemaPath = "prisma/schema.prisma"; const schemaExists = existsSync(schemaPath); if (schemaExists) { @@ -346,11 +346,11 @@ export const addToPrismaSchema = (schema: string, modelName: string) => { schemaContents.slice(0, modelStart) + schema + schemaContents.slice(modelEnd + 1); - replaceFile(schemaPath, newContent); + await replaceFile(schemaPath, newContent); // consola.success(`Replaced ${modelName} in Prisma schema`); } else { const newContent = schemaContents.concat("\n", schema); - replaceFile(schemaPath, newContent); + await replaceFile(schemaPath, newContent); // consola.success(`Added ${modelName} to Prisma schema`); } } else { @@ -385,7 +385,10 @@ const getPrismaModelStartAndEnd = (schema: string, modelName: string) => { return { modelStart, modelEnd, modelExists }; }; -export function addToPrismaModel(modelName: string, attributesToAdd: string) { +export async function addToPrismaModel( + modelName: string, + attributesToAdd: string +) { const hasSchema = existsSync("prisma/schema.prisma"); if (!hasSchema) { console.error("Prisma schema not found!"); @@ -404,12 +407,12 @@ export function addToPrismaModel(modelName: string, attributesToAdd: string) { const newSchema = beforeModelEnd + " " + attributesToAdd + "\n" + afterModelEnd; - replaceFile("prisma/schema.prisma", newSchema); + await replaceFile("prisma/schema.prisma", newSchema); consola.info("Updated Prisma schema"); } } -export function addToPrismaModelBulk( +export async function addToPrismaModelBulk( modelName: string, attributesToAdd: string ) { @@ -428,7 +431,7 @@ export function addToPrismaModelBulk( const newSchema = beforeModelEnd + " " + attributesToAdd + "\n" + afterModelEnd; - replaceFile("prisma/schema.prisma", newSchema); + await replaceFile("prisma/schema.prisma", newSchema); consola.info("Updated Prisma schema"); } } diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 5cb7ce1f..c4fa4467 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -5,7 +5,11 @@ import { consola } from "consola"; import { addPackage } from "../add/index.js"; import { existsSync, readFileSync } from "fs"; import path from "path"; -import { checkForPackageManager } from "./utils.js"; +import { + // checkAndCreatIfNotExistPrettierConfig, + checkForPackageManager, + createPrettierConfigFileIfNotExist, +} from "./utils.js"; import figlet from "figlet"; import chalk from "chalk"; @@ -64,7 +68,9 @@ export async function initProject(options?: InitOptions) { if (tsConfigString.includes("@/*")) alias = "@"; if (tsConfigString.includes("~/*")) alias = "~"; - createConfigFile({ + await createPrettierConfigFileIfNotExist(); + + await createConfigFile({ driver: undefined, hasSrc: srcExists, provider: undefined, diff --git a/src/commands/init/utils.ts b/src/commands/init/utils.ts index 14706e4b..78e90382 100644 --- a/src/commands/init/utils.ts +++ b/src/commands/init/utils.ts @@ -1,4 +1,8 @@ -import { existsSync, readFileSync } from "fs"; +import { + existsSync, + readFileSync, + // writeFileSync +} from "fs"; import { AvailablePackage, Config, @@ -8,17 +12,20 @@ import { PMType, } from "../../types.js"; import { - installPackages, + createFile, readConfigFile, replaceFile, updateConfigFile, - wrapInParenthesis, } from "../../utils.js"; import { consola } from "consola"; import { updateTsConfigPrismaTypeAlias } from "../add/orm/utils.js"; import { addToInstallList } from "../add/utils.js"; import { addNanoidToUtils } from "../add/orm/drizzle/utils.js"; -// test +import { + format as prettierFormat, + resolveConfigFile, + resolveConfig as resolvePrettierConfig, +} from "prettier"; export const DBProviders: DBProviderOptions = { pg: [ @@ -47,7 +54,7 @@ export const DBProviders: DBProviderOptions = { export const checkForExistingPackages = async (rootPath: string) => { consola.start("Checking project for existing packages..."); // get package json - const { preferredPackageManager } = readConfigFile(); + // const { preferredPackageManager } = readConfigFile(); const packageJsonInitText = readFileSync("package.json", "utf-8"); let configObj: Partial = { @@ -58,6 +65,7 @@ export const checkForExistingPackages = async (rootPath: string) => { trpc: ["@trpc/client", "@trpc/react-query", "@trpc/server", "@trpc/next"], clerk: ["@clerk/nextjs"], lucia: ["lucia"], + supabase: ["@supabase/supabase-js", "@supabase/ssr"], prisma: ["prisma"], resend: ["resend"], stripe: ["stripe", "@stripe/stripe-js"], @@ -74,6 +82,7 @@ export const checkForExistingPackages = async (rootPath: string) => { clerk: "auth", "next-auth": "auth", lucia: "auth", + supabase: "auth", drizzle: "orm", }; @@ -198,7 +207,7 @@ export const checkForExistingPackages = async (rootPath: string) => { // { regular: "", dev: "zod-prisma" }, // preferredPackageManager, // ); - addZodGeneratorToPrismaSchema(); + await addZodGeneratorToPrismaSchema(); // consola.success("Successfully installed!"); await updateTsConfigPrismaTypeAlias(); @@ -211,17 +220,17 @@ export const checkForExistingPackages = async (rootPath: string) => { // preferredPackageManager // ); addToInstallList({ regular: ["drizzle-zod", "nanoid"], dev: [] }); - addNanoidToUtils(); + await addNanoidToUtils(); // consola.success("Successfully installed!"); } } // if (drizzle), check if using one schema file or schema directory - perhaps just force users? // update config file - updateConfigFile(configObj); + await updateConfigFile(configObj); }; -const addZodGeneratorToPrismaSchema = () => { +const addZodGeneratorToPrismaSchema = async () => { const hasSchema = existsSync("prisma/schema.prisma"); if (!hasSchema) { console.error("Prisma schema not found!"); @@ -240,7 +249,7 @@ generator zod { } `); - replaceFile("prisma/schema.prisma", newSchema); + await replaceFile("prisma/schema.prisma", newSchema); consola.info("Updated Prisma schema"); }; @@ -256,6 +265,64 @@ export const checkForPackageManager = (): PMType | null => { return null; }; +export const createPrettierConfigFileIfNotExist = async () => { + const userPrettierConfigPath = await resolveConfigFile(); + + // Throws an error if there is an error during parsing the users prettier config file + let prettierConfig = await resolvePrettierConfig(userPrettierConfigPath); + + if (prettierConfig != null) + consola.success( + "Prettier config found! Using prettier config for formatting..." + ); + + if (prettierConfig === null) { + consola.info( + "Prettier config not found! Create default prettier config for further usage..." + ); + + await createDefaultPrettierConfig(); + } +}; + +export const formatFileContentWithPrettier = async ( + content: string, + filePath: string, + skipPrettier?: boolean +) => { + if (skipPrettier) return content; + + const prettierConfigPath = await resolveConfigFile(); + + // Throws an error if there is an error during parsing the users prettier config file + let prettierConfig = await resolvePrettierConfig(prettierConfigPath); + + if ( + prettierConfig && + typeof prettierConfig === "object" && + !prettierConfig.filepath + ) + prettierConfig.filepath = filePath; + + return await prettierFormat(content, prettierConfig); +}; + +const createDefaultPrettierConfig = async () => { + /** @link https://prettier.io/docs/en/configuration#basic-configuration */ + const defaultPrettierConfig = { + trailingComma: "es5" as const, + tabWidth: 4, + semi: false, + singleQuote: true, + }; + + await createFile( + ".prettierrc", + JSON.stringify(defaultPrettierConfig, null, 2), + true + ); +}; + export const toggleAnalytics = (input: { toggle?: boolean }) => { const { analytics } = readConfigFile(); diff --git a/src/index.ts b/src/index.ts index 65535cbd..971061c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ program .action(toggleAnalytics); addCommonOptions(program.command("init")) - .description("initialise and configure kirimase within directory") + .description("initialise and configure kirimase within a directory") .action(initProject); program @@ -43,7 +43,10 @@ function addCommonOptions(command: Command) { .option("-o, --orm ", "preferred orm (prisma, drizzle)") .option("-db, --db ", "preferred database (pg, mysql, sqlite)") .option("-dbp, --db-provider ", "database provider") - .option("-a, --auth ", "preferred auth (next-auth, clerk, lucia)") + .option( + "-a, --auth ", + "preferred auth (next-auth, clerk, lucia, supabase)" + ) .option( "-ap, --auth-providers ", "auth providers (if using next-auth - discord, google, github, apple)" diff --git a/src/types.d.ts b/src/types.d.ts index 654181b8..d32b75fa 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -74,12 +74,13 @@ export type AvailablePackage = | "resend" | "lucia" | "kinde" - | "stripe"; + | "stripe" + | "supabase"; export type PackageType = "orm" | "auth" | "componentLib" | "misc"; export type ComponentLibType = "shadcn-ui"; export type ORMType = "drizzle" | "prisma"; -export type AuthType = "next-auth" | "clerk" | "lucia" | "kinde"; +export type AuthType = "next-auth" | "clerk" | "lucia" | "kinde" | "supabase"; export type MiscType = "trpc" | "stripe" | "resend"; export type AuthSubType = "self-hosted" | "managed"; diff --git a/src/utils.ts b/src/utils.ts index 69acef04..350a4f0a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,11 +4,16 @@ import { consola } from "consola"; import { AvailablePackage, Config, PMType, UpdateConfig } from "./types.js"; import { execa } from "execa"; import { spinner } from "./commands/add/index.js"; +import { formatFileContentWithPrettier } from "./commands/init/utils.js"; export const delay = (ms = 2000) => new Promise((resolve) => setTimeout(resolve, ms)); -export function createFile(filePath: string, content: string) { +export async function createFile( + filePath: string, + content: string, + skipPrettier = false +) { const resolvedPath = path.resolve(filePath); const dirName = path.dirname(resolvedPath); @@ -19,12 +24,20 @@ export function createFile(filePath: string, content: string) { // consola.success(`Directory ${dirName} created.`); } - fs.writeFileSync(resolvedPath, content); + fs.writeFileSync( + resolvedPath, + await formatFileContentWithPrettier(content, filePath, skipPrettier) + ); // TODO - add flag for verbose // consola.success(`File created at ${filePath}`); } -export function replaceFile(filePath: string, content: string, log = true) { +export async function replaceFile( + filePath: string, + content: string, + log = true, + skipPrettier = false +) { const resolvedPath = path.resolve(filePath); const dirName = path.dirname(resolvedPath); @@ -35,7 +48,10 @@ export function replaceFile(filePath: string, content: string, log = true) { // consola.success(`Directory ${dirName} created.`); } - fs.writeFileSync(resolvedPath, content); + fs.writeFileSync( + resolvedPath, + await formatFileContentWithPrettier(content, filePath, skipPrettier) + ); if (log === true) { // TODO as above // consola.success(`File replaced at ${filePath}`); @@ -103,14 +119,14 @@ export async function installPackages( } } -export const createConfigFile = (options: Config) => { - createFile("./kirimase.config.json", JSON.stringify(options, null, 2)); +export const createConfigFile = async (options: Config) => { + await createFile("./kirimase.config.json", JSON.stringify(options, null, 2)); }; -export const updateConfigFile = (options: UpdateConfig) => { +export const updateConfigFile = async (options: UpdateConfig) => { const config = readConfigFile(); const newConfig = { ...config, ...options }; - replaceFile( + await replaceFile( "./kirimase.config.json", JSON.stringify(newConfig, null, 2), false @@ -134,9 +150,9 @@ export const readConfigFile = (): (Config & { rootPath: string }) | null => { return { ...config, rootPath }; }; -export const addPackageToConfig = (packageName: AvailablePackage) => { +export const addPackageToConfig = async (packageName: AvailablePackage) => { const config = readConfigFile(); - updateConfigFile({ packages: [...config?.packages, packageName] }); + await updateConfigFile({ packages: [...config?.packages, packageName] }); }; export const wrapInParenthesis = (string: string) => { @@ -202,12 +218,12 @@ export const getFileContents = (filePath: string) => { return fileContents; }; -export const updateConfigFileAfterUpdate = () => { +export const updateConfigFileAfterUpdate = async () => { const { packages, orm, auth } = readConfigFile(); if (orm === undefined || auth === undefined) { const updatedOrm = packages.includes("drizzle") ? "drizzle" : null; const updatedAuth = packages.includes("next-auth") ? "next-auth" : null; - updateConfigFile({ orm: updatedOrm, auth: updatedAuth }); + await updateConfigFile({ orm: updatedOrm, auth: updatedAuth }); consola.info("Config file updated."); } else { consola.info("Config file already up to date.");