This commit is contained in:
Aman Varshney
2025-04-14 21:45:28 +05:30
parent 8b03441909
commit 7f441ef670
268 changed files with 3513 additions and 3039 deletions

View File

@@ -1,29 +1,30 @@
import path from "node:path";
import fs from "fs-extra";
import { PKG_ROOT } from "../constants";
import type {
ProjectAddons,
ProjectFrontend,
ProjectPackageManager,
} from "../types";
import type { ProjectFrontend } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
import { setupStarlight } from "./starlight-setup";
import { setupTauri } from "./tauri-setup";
export async function setupAddons(
projectDir: string,
addons: ProjectAddons[],
packageManager: ProjectPackageManager,
frontends: ProjectFrontend[],
) {
import type { ProjectConfig } from "../types";
export async function setupAddons(config: ProjectConfig) {
const { projectName, addons, packageManager, frontend } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const hasWebFrontend =
frontends.includes("react-router") || frontends.includes("tanstack-router");
frontend.includes("react-router") || frontend.includes("tanstack-router");
if (addons.includes("turborepo")) {
await addPackageDependency({
devDependencies: ["turbo"],
projectDir,
});
}
if (addons.includes("pwa") && hasWebFrontend) {
await setupPwa(projectDir, frontends);
await setupPwa(projectDir, frontend);
}
if (addons.includes("tauri") && hasWebFrontend) {
await setupTauri(projectDir, packageManager, frontends);
await setupTauri(config);
}
if (addons.includes("biome")) {
await setupBiome(projectDir);
@@ -32,7 +33,7 @@ export async function setupAddons(
await setupHusky(projectDir);
}
if (addons.includes("starlight")) {
await setupStarlight(projectDir, packageManager);
await setupStarlight(config);
}
}
@@ -44,12 +45,7 @@ export function getWebAppDir(
}
async function setupBiome(projectDir: string) {
const biomeTemplateDir = path.join(PKG_ROOT, "template/with-biome");
if (await fs.pathExists(biomeTemplateDir)) {
await fs.copy(biomeTemplateDir, projectDir, { overwrite: true });
}
addPackageDependency({
await addPackageDependency({
devDependencies: ["@biomejs/biome"],
projectDir,
});
@@ -68,12 +64,7 @@ async function setupBiome(projectDir: string) {
}
async function setupHusky(projectDir: string) {
const huskyTemplateDir = path.join(PKG_ROOT, "template/with-husky");
if (await fs.pathExists(huskyTemplateDir)) {
await fs.copy(huskyTemplateDir, projectDir, { overwrite: true });
}
addPackageDependency({
await addPackageDependency({
devDependencies: ["husky", "lint-staged"],
projectDir,
});
@@ -98,81 +89,18 @@ async function setupHusky(projectDir: string) {
}
async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
const pwaTemplateDir = path.join(PKG_ROOT, "template/with-pwa");
if (await fs.pathExists(pwaTemplateDir)) {
await fs.copy(pwaTemplateDir, projectDir, { overwrite: true });
}
const clientPackageDir = getWebAppDir(projectDir, frontends);
if (!(await fs.pathExists(clientPackageDir))) {
return;
}
addPackageDependency({
await addPackageDependency({
dependencies: ["vite-plugin-pwa"],
devDependencies: ["@vite-pwa/assets-generator"],
projectDir: clientPackageDir,
});
const viteConfigPath = path.join(clientPackageDir, "vite.config.ts");
if (await fs.pathExists(viteConfigPath)) {
let viteConfig = await fs.readFile(viteConfigPath, "utf8");
if (!viteConfig.includes("vite-plugin-pwa")) {
const firstImportMatch = viteConfig.match(
/^import .* from ['"](.*)['"]/m,
);
if (firstImportMatch) {
viteConfig = viteConfig.replace(
firstImportMatch[0],
`import { VitePWA } from "vite-plugin-pwa";\n${firstImportMatch[0]}`,
);
} else {
viteConfig = `import { VitePWA } from "vite-plugin-pwa";\n${viteConfig}`;
}
}
const pwaPluginCode = `VitePWA({
registerType: "autoUpdate",
manifest: {
name: "My App",
short_name: "My App",
description: "My App",
theme_color: "#0c0c0c",
},
pwaAssets: {
disabled: false,
config: true,
},
devOptions: {
enabled: true,
},
})`;
if (!viteConfig.includes("VitePWA(")) {
if (frontends.includes("react-router")) {
viteConfig = viteConfig.replace(
/plugins: \[\s*tailwindcss\(\)/,
`plugins: [\n tailwindcss(),\n ${pwaPluginCode}`,
);
} else if (frontends.includes("tanstack-router")) {
viteConfig = viteConfig.replace(
/plugins: \[\s*tailwindcss\(\)/,
`plugins: [\n tailwindcss(),\n ${pwaPluginCode}`,
);
} else {
viteConfig = viteConfig.replace(
/plugins: \[/,
`plugins: [\n ${pwaPluginCode},`,
);
}
}
await fs.writeFile(viteConfigPath, viteConfig);
}
const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
if (await fs.pathExists(clientPackageJsonPath)) {
const packageJson = await fs.readJson(clientPackageJsonPath);

View File

@@ -0,0 +1,37 @@
import * as path from "node:path";
import type { ProjectApi, ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupApi(config: ProjectConfig): Promise<void> {
if (config.api === "none") return;
const { api, projectName } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const webDir = path.join(projectDir, "apps/web");
const serverDir = path.join(projectDir, "apps/server");
if (api === "orpc") {
await addPackageDependency({
dependencies: ["@orpc/react-query", "@orpc/server", "@orpc/client"],
projectDir: webDir,
});
await addPackageDependency({
dependencies: ["@orpc/server", "@orpc/client"],
projectDir: serverDir,
});
}
if (api === "trpc") {
await addPackageDependency({
dependencies: [
"@trpc/tanstack-react-query",
"@trpc/server",
"@trpc/client",
],
projectDir: webDir,
});
await addPackageDependency({
dependencies: ["@trpc/server", "@trpc/client"],
projectDir: serverDir,
});
}
}

View File

@@ -15,40 +15,40 @@ export function generateAuthSecret(length = 32): string {
return result;
}
export async function setupAuth(
projectDir: string,
enableAuth: boolean,
frontends: ProjectFrontend[] = [],
): Promise<void> {
if (!enableAuth) {
import type { ProjectConfig } from "../types";
export async function setupAuth(config: ProjectConfig): Promise<void> {
const { projectName, auth, frontend } = config;
if (!auth) {
return;
}
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server");
const clientDir = path.join(projectDir, "apps/web");
const nativeDir = path.join(projectDir, "apps/native");
try {
addPackageDependency({
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: serverDir,
});
if (
frontends.includes("react-router") ||
frontends.includes("tanstack-router") ||
frontends.includes("tanstack-start")
frontend.includes("react-router") ||
frontend.includes("tanstack-router") ||
frontend.includes("tanstack-start")
) {
addPackageDependency({
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: clientDir,
});
}
if (frontends.includes("native")) {
addPackageDependency({
if (frontend.includes("native")) {
await addPackageDependency({
dependencies: ["better-auth", "@better-auth/expo"],
projectDir: nativeDir,
});
addPackageDependency({
await addPackageDependency({
dependencies: ["@better-auth/expo"],
projectDir: serverDir,
});

View File

@@ -3,11 +3,14 @@ import type { AvailableDependencies } from "../constants";
import type { ProjectBackend, ProjectRuntime } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
import type { ProjectConfig } from "../types";
export async function setupBackendDependencies(
projectDir: string,
framework: ProjectBackend,
runtime: ProjectRuntime,
config: ProjectConfig,
): Promise<void> {
const { projectName, backend, runtime } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const framework = backend;
const serverDir = path.join(projectDir, "apps/server");
const dependencies: AvailableDependencies[] = [];
@@ -40,7 +43,7 @@ export async function setupBackendDependencies(
devDependencies.push("@types/bun");
}
addPackageDependency({
await addPackageDependency({
dependencies,
devDependencies,
projectDir: serverDir,

View File

@@ -1,9 +1,10 @@
import path from "node:path";
import { cancel, spinner } from "@clack/prompts";
import { cancel, log, spinner } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../types";
import { setupAddons } from "./addons-setup";
import { setupApi } from "./api-setup";
import { setupAuth } from "./auth-setup";
import { setupBackendDependencies } from "./backend-framework-setup";
import { createReadme } from "./create-readme";
@@ -17,10 +18,13 @@ import { setupRuntime } from "./runtime-setup";
import {
copyBaseTemplate,
fixGitignoreFiles,
handleExtras,
setupAddonsTemplate,
setupAuthTemplate,
setupBackendFramework,
setupDbOrmTemplates,
setupExamplesTemplate,
setupFrontendTemplates,
setupOrmTemplate,
} from "./template-manager";
export async function createProject(options: ProjectConfig): Promise<string> {
@@ -30,74 +34,47 @@ export async function createProject(options: ProjectConfig): Promise<string> {
try {
await fs.ensureDir(projectDir);
await copyBaseTemplate(projectDir);
await setupFrontendTemplates(projectDir, options.frontend);
await copyBaseTemplate(projectDir, options);
await setupFrontendTemplates(projectDir, options);
await fixGitignoreFiles(projectDir);
await setupBackendFramework(projectDir, options);
await setupBackendDependencies(options);
await setupBackendFramework(projectDir, options.backend);
await setupBackendDependencies(
projectDir,
options.backend,
options.runtime,
);
await setupDbOrmTemplates(projectDir, options);
await setupOrmTemplate(
projectDir,
options.orm,
options.database,
options.auth,
);
await setupDatabase(options);
await setupDatabase(
projectDir,
options.database,
options.orm,
options.packageManager,
options.dbSetup === "turso",
options.dbSetup === "prisma-postgres",
options.dbSetup === "mongodb-atlas",
options.dbSetup === "neon",
);
await setupAuthTemplate(projectDir, options);
await setupAuth(options);
await setupAuthTemplate(
projectDir,
options.auth,
options.backend,
options.orm,
options.database,
options.frontend,
);
await setupAuth(projectDir, options.auth, options.frontend);
await setupRuntime(projectDir, options.runtime, options.backend);
await setupExamples(
projectDir,
options.examples,
options.orm,
options.auth,
options.backend,
options.frontend,
);
await setupEnvironmentVariables(projectDir, options);
await initializeGit(projectDir, options.git);
if (options.addons.length > 0) {
await setupAddons(
projectDir,
options.addons,
options.packageManager,
options.frontend,
);
await setupAddonsTemplate(projectDir, options);
if (options.addons.length > 0 && options.addons[0] !== "none") {
await setupAddons(options);
}
await setupExamplesTemplate(projectDir, options);
await handleExtras(projectDir, options);
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamples(options);
}
await setupApi(options);
await setupRuntime(options);
await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
if (!options.noInstall) {
await initializeGit(projectDir, options.git);
await fixGitignoreFiles(projectDir, options);
log.success("Project template successfully scaffolded!");
if (options.install) {
await installDependencies({
projectDir,
packageManager: options.packageManager,
@@ -105,22 +82,17 @@ export async function createProject(options: ProjectConfig): Promise<string> {
});
}
displayPostInstallInstructions(
options.database,
options.projectName,
options.packageManager,
!options.noInstall,
options.orm,
options.addons,
options.runtime,
options.frontend,
);
displayPostInstallInstructions({
...options,
depsInstalled: options.install,
});
return projectDir;
} catch (error) {
s.message(pc.red("Failed"));
s.stop(pc.red("Failed"));
if (error instanceof Error) {
cancel(pc.red(`Error during project creation: ${error.message}`));
console.error(error.stack);
process.exit(1);
}
throw error;

View File

@@ -10,50 +10,46 @@ import type {
} from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
import { setupNeonPostgres } from "./neon-setup";
import { setupPrismaPostgres } from "./prisma-postgres-setup";
import { setupTurso } from "./turso-setup";
export async function setupDatabase(
projectDir: string,
databaseType: ProjectDatabase,
orm: ProjectOrm,
packageManager: ProjectPackageManager,
setupTursoDb: boolean,
setupPrismaPostgresDb: boolean,
setupMongoDBAtlasDb: boolean,
setupNeonPostgresDb: boolean,
): Promise<void> {
import { setupNeonPostgres } from "./neon-setup";
import type { ProjectConfig } from "../types";
export async function setupDatabase(config: ProjectConfig): Promise<void> {
const { projectName, database, orm, packageManager, dbSetup } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const s = spinner();
const serverDir = path.join(projectDir, "apps/server");
if (databaseType === "none") {
if (database === "none") {
await fs.remove(path.join(serverDir, "src/db"));
return;
}
try {
if (orm === "prisma") {
addPackageDependency({
await addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
} else if (orm === "drizzle") {
if (databaseType === "sqlite") {
addPackageDependency({
if (database === "sqlite") {
await addPackageDependency({
dependencies: ["drizzle-orm", "@libsql/client"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
} else if (databaseType === "postgres") {
addPackageDependency({
} else if (database === "postgres") {
await addPackageDependency({
dependencies: ["drizzle-orm", "pg"],
devDependencies: ["drizzle-kit", "@types/pg"],
projectDir: serverDir,
});
} else if (databaseType === "mysql") {
addPackageDependency({
} else if (database === "mysql") {
await addPackageDependency({
dependencies: ["drizzle-orm", "mysql2"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
@@ -61,16 +57,16 @@ export async function setupDatabase(
}
}
if (databaseType === "sqlite" && setupTursoDb) {
await setupTurso(projectDir, orm === "drizzle");
} else if (databaseType === "postgres") {
if (orm === "prisma" && setupPrismaPostgresDb) {
await setupPrismaPostgres(projectDir, packageManager);
} else if (setupNeonPostgresDb) {
await setupNeonPostgres(projectDir, packageManager);
if (database === "sqlite" && dbSetup === "turso") {
await setupTurso(config);
} else if (database === "postgres") {
if (orm === "prisma" && dbSetup === "prisma-postgres") {
await setupPrismaPostgres(config);
} else if (dbSetup === "neon") {
await setupNeonPostgres(config);
}
} else if (databaseType === "mongodb" && setupMongoDBAtlasDb) {
await setupMongoDBAtlas(projectDir);
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
await setupMongoDBAtlas(config);
}
} catch (error) {
s.stop(pc.red("Failed to set up database"));

View File

@@ -42,9 +42,11 @@ async function addEnvVariablesToFile(
}
export async function setupEnvironmentVariables(
projectDir: string,
options: ProjectConfig,
config: ProjectConfig,
): Promise<void> {
const { projectName } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const options = config;
const serverDir = path.join(projectDir, "apps/server");
const envPath = path.join(serverDir, ".env");

View File

@@ -1,387 +1,35 @@
import path from "node:path";
import fs from "fs-extra";
import { PKG_ROOT } from "../constants";
import type { ProjectBackend, ProjectFrontend, ProjectOrm } from "../types";
import type {
ProjectBackend,
ProjectConfig,
ProjectFrontend,
ProjectOrm,
} from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupExamples(
projectDir: string,
examples: string[],
orm: ProjectOrm,
auth: boolean,
backend: ProjectBackend,
frontend: ProjectFrontend[] = ["tanstack-router"],
): Promise<void> {
const hasTanstackRouter = frontend.includes("tanstack-router");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasReactRouter = frontend.includes("react-router");
const hasWebFrontend =
hasTanstackRouter || hasReactRouter || hasTanstackStart;
let routerType: string;
if (hasTanstackRouter) {
routerType = "web-tanstack-router";
} else if (hasTanstackStart) {
routerType = "web-tanstack-start";
} else {
routerType = "web-react-router";
}
const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web"));
if (examples.includes("todo") && hasWebFrontend && webAppExists) {
await setupTodoExample(projectDir, orm, auth, routerType);
} else {
await cleanupTodoFiles(projectDir, orm);
}
if (
examples.includes("ai") &&
(backend === "hono" || backend === "express") &&
hasWebFrontend &&
webAppExists
) {
await setupAIExample(projectDir, routerType);
}
}
async function setupAIExample(
projectDir: string,
routerType: string,
): Promise<void> {
const aiExampleDir = path.join(PKG_ROOT, "template/examples/ai");
if (await fs.pathExists(aiExampleDir)) {
const aiRouteSourcePath = path.join(
aiExampleDir,
`apps/${routerType}/src/routes/ai.tsx`,
);
const aiRouteTargetPath = path.join(
projectDir,
"apps/web/src/routes/ai.tsx",
);
if (await fs.pathExists(aiRouteSourcePath)) {
await fs.copy(aiRouteSourcePath, aiRouteTargetPath, { overwrite: true });
}
await updateHeaderWithAILink(projectDir, routerType);
export async function setupExamples(config: ProjectConfig): Promise<void> {
const {
projectName,
examples,
orm,
auth,
backend,
frontend = ["tanstack-router"],
} = config;
const projectDir = path.resolve(process.cwd(), projectName);
if (examples.includes("ai")) {
const clientDir = path.join(projectDir, "apps/web");
addPackageDependency({
await addPackageDependency({
dependencies: ["ai"],
projectDir: clientDir,
});
const serverDir = path.join(projectDir, "apps/server");
addPackageDependency({
await addPackageDependency({
dependencies: ["ai", "@ai-sdk/google"],
projectDir: serverDir,
});
await updateServerIndexWithAIRoute(projectDir);
}
}
async function updateServerIndexWithAIRoute(projectDir: string): Promise<void> {
const serverIndexPath = path.join(projectDir, "apps/server/src/index.ts");
if (await fs.pathExists(serverIndexPath)) {
let indexContent = await fs.readFile(serverIndexPath, "utf8");
const isHono = indexContent.includes("hono");
const isExpress = indexContent.includes("express");
if (isHono) {
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`;
const aiRouteHandler = `
// AI chat endpoint
app.post("/ai", async (c) => {
const body = await c.req.json();
const messages = body.messages || [];
const result = streamText({
model: google("gemini-1.5-flash"),
messages,
});
c.header("X-Vercel-AI-Data-Stream", "v1");
c.header("Content-Type", "text/plain; charset=utf-8");
return stream(c, (stream) => stream.pipe(result.toDataStream()));
});`;
if (indexContent.includes("import {")) {
const lastImportIndex = indexContent.lastIndexOf("import");
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
indexContent = `${indexContent.substring(0, endOfLastImport + 1)}
${importSection}
${indexContent.substring(endOfLastImport + 1)}`;
} else {
indexContent = `${importSection}
${indexContent}`;
}
const trpcHandlerIndex =
indexContent.indexOf('app.use("/trpc"') ||
indexContent.indexOf("app.use(trpc(");
if (trpcHandlerIndex !== -1) {
indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler}
${indexContent.substring(trpcHandlerIndex)}`;
} else {
const exportIndex = indexContent.indexOf("export default");
if (exportIndex !== -1) {
indexContent = `${indexContent.substring(0, exportIndex)}${aiRouteHandler}
${indexContent.substring(exportIndex)}`;
} else {
indexContent = `${indexContent}
${aiRouteHandler}`;
}
}
} else if (isExpress) {
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";`;
const aiRouteHandler = `
// AI chat endpoint
app.post("/ai", async (req, res) => {
const { messages = [] } = req.body;
const result = streamText({
model: google("gemini-1.5-flash"),
messages,
});
result.pipeDataStreamToResponse(res);
});`;
if (
indexContent.includes("import {") ||
indexContent.includes("import ")
) {
const lastImportIndex = indexContent.lastIndexOf("import");
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
indexContent = `${indexContent.substring(0, endOfLastImport + 1)}
${importSection}
${indexContent.substring(endOfLastImport + 1)}`;
} else {
indexContent = `${importSection}
${indexContent}`;
}
const trpcHandlerIndex = indexContent.indexOf('app.use("/trpc"');
if (trpcHandlerIndex !== -1) {
indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler}
${indexContent.substring(trpcHandlerIndex)}`;
} else {
const appListenIndex = indexContent.indexOf("app.listen(");
if (appListenIndex !== -1) {
const prevNewlineIndex = indexContent.lastIndexOf(
"\n",
appListenIndex,
);
indexContent = `${indexContent.substring(0, prevNewlineIndex)}${aiRouteHandler}
${indexContent.substring(prevNewlineIndex)}`;
} else {
indexContent = `${indexContent}
${aiRouteHandler}`;
}
}
}
await fs.writeFile(serverIndexPath, indexContent);
}
}
async function updateHeaderWithAILink(
projectDir: string,
routerType: string,
): Promise<void> {
const headerPath = path.join(
projectDir,
"apps/web/src/components/header.tsx",
);
if (await fs.pathExists(headerPath)) {
let headerContent = await fs.readFile(headerPath, "utf8");
const linksPattern = /const links = \[\s*([^;]*?)\s*\];/s;
const linksMatch = headerContent.match(linksPattern);
if (linksMatch) {
const linksContent = linksMatch[1];
if (!linksContent.includes('"/ai"')) {
const updatedLinks = `const links = [\n ${linksContent}${
linksContent.trim().endsWith(",") ? "" : ","
}\n { to: "/ai", label: "AI Chat" },\n ];`;
headerContent = headerContent.replace(linksPattern, updatedLinks);
await fs.writeFile(headerPath, headerContent);
}
}
}
}
async function setupTodoExample(
projectDir: string,
orm: ProjectOrm,
auth: boolean,
routerType: string,
): Promise<void> {
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
if (await fs.pathExists(todoExampleDir)) {
const todoRouteSourcePath = path.join(
todoExampleDir,
`apps/${routerType}/src/routes/todos.tsx`,
);
const todoRouteTargetPath = path.join(
projectDir,
"apps/web/src/routes/todos.tsx",
);
if (await fs.pathExists(todoRouteSourcePath)) {
await fs.copy(todoRouteSourcePath, todoRouteTargetPath, {
overwrite: true,
});
}
if (orm !== "none") {
const todoRouterSourceFile = path.join(
todoExampleDir,
`apps/server/src/routers/with-${orm}-todo.ts`,
);
const todoRouterTargetFile = path.join(
projectDir,
"apps/server/src/routers/todo.ts",
);
if (await fs.pathExists(todoRouterSourceFile)) {
await fs.copy(todoRouterSourceFile, todoRouterTargetFile, {
overwrite: true,
});
}
await updateRouterIndexToIncludeTodo(projectDir);
}
await updateHeaderWithTodoLink(projectDir, routerType);
}
}
async function updateRouterIndexToIncludeTodo(
projectDir: string,
): Promise<void> {
const routerFile = path.join(projectDir, "apps/server/src/routers/index.ts");
if (await fs.pathExists(routerFile)) {
let routerContent = await fs.readFile(routerFile, "utf8");
if (!routerContent.includes("import { todoRouter }")) {
const lastImportIndex = routerContent.lastIndexOf("import");
const endOfImports = routerContent.indexOf("\n\n", lastImportIndex);
if (endOfImports !== -1) {
routerContent = `${routerContent.slice(0, endOfImports)}
import { todoRouter } from "./todo";${routerContent.slice(endOfImports)}`;
} else {
routerContent = `import { todoRouter } from "./todo";\n${routerContent}`;
}
const routerDefIndex = routerContent.indexOf(
"export const appRouter = router({",
);
if (routerDefIndex !== -1) {
const routerContentStart =
routerContent.indexOf("{", routerDefIndex) + 1;
routerContent = `${routerContent.slice(0, routerContentStart)}
todo: todoRouter,${routerContent.slice(routerContentStart)}`;
}
await fs.writeFile(routerFile, routerContent);
}
}
}
async function updateHeaderWithTodoLink(
projectDir: string,
routerType: string,
): Promise<void> {
const headerPath = path.join(
projectDir,
"apps/web/src/components/header.tsx",
);
if (await fs.pathExists(headerPath)) {
let headerContent = await fs.readFile(headerPath, "utf8");
const linksPattern = /const links = \[\s*([^;]*?)\s*\];/s;
const linksMatch = headerContent.match(linksPattern);
if (linksMatch) {
const linksContent = linksMatch[1];
if (!linksContent.includes('"/todos"')) {
const updatedLinks = `const links = [\n ${linksContent}${
linksContent.trim().endsWith(",") ? "" : ","
}\n { to: "/todos", label: "Todos" },\n ];`;
headerContent = headerContent.replace(linksPattern, updatedLinks);
await fs.writeFile(headerPath, headerContent);
}
}
}
}
async function cleanupTodoFiles(
projectDir: string,
orm: ProjectOrm,
): Promise<void> {
if (orm === "drizzle") {
const todoSchemaFile = path.join(
projectDir,
"apps/server/src/db/schema/todo.ts",
);
if (await fs.pathExists(todoSchemaFile)) {
await fs.remove(todoSchemaFile);
}
} else if (orm === "prisma") {
const todoPrismaFile = path.join(
projectDir,
"apps/server/prisma/schema/todo.prisma",
);
if (await fs.pathExists(todoPrismaFile)) {
await fs.remove(todoPrismaFile);
}
}
const todoRouterFile = path.join(
projectDir,
"apps/server/src/routers/todo.ts",
);
if (await fs.pathExists(todoRouterFile)) {
await fs.remove(todoRouterFile);
}
await updateRouterIndex(projectDir);
}
async function updateRouterIndex(projectDir: string): Promise<void> {
const routerFile = path.join(projectDir, "apps/server/src/routers/index.ts");
if (await fs.pathExists(routerFile)) {
let routerContent = await fs.readFile(routerFile, "utf8");
routerContent = routerContent.replace(
/import { todoRouter } from ".\/todo";/,
"",
);
routerContent = routerContent.replace(/todo: todoRouter,/, "");
await fs.writeFile(routerFile, routerContent);
}
}

View File

@@ -4,6 +4,7 @@ import consola from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../types";
import { commandExists } from "../utils/command-exists";
type MongoDBConfig = {
@@ -129,7 +130,9 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
`);
}
export async function setupMongoDBAtlas(projectDir: string) {
export async function setupMongoDBAtlas(config: ProjectConfig) {
const { projectName } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const mainSpinner = spinner();
mainSpinner.start("Setting up MongoDB Atlas");

View File

@@ -5,6 +5,7 @@ import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectPackageManager } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
type NeonConfig = {
connectionString: string;
@@ -13,41 +14,20 @@ type NeonConfig = {
roleName: string;
};
function buildNeonCommand(
packageManager: string,
args: string[],
): { cmd: string; cmdArgs: string[] } {
let cmd: string;
let cmdArgs: string[];
switch (packageManager) {
case "pnpm":
cmd = "pnpm";
cmdArgs = ["dlx", "neonctl", ...args];
break;
case "bun":
cmd = "bunx";
cmdArgs = ["neonctl", ...args];
break;
default:
cmd = "npx";
cmdArgs = ["neonctl", ...args];
}
return { cmd, cmdArgs };
}
async function executeNeonCommand(
packageManager: string,
args: string[],
packageManager: ProjectPackageManager,
commandArgsString: string,
spinnerText?: string,
) {
const s = spinnerText ? spinner() : null;
const s = spinner();
try {
const { cmd, cmdArgs } = buildNeonCommand(packageManager, args);
const fullCommand = getPackageExecutionCommand(
packageManager,
commandArgsString,
);
if (s) s.start(spinnerText);
const result = await execa(cmd, cmdArgs);
const result = await execa(fullCommand, { shell: true });
if (s) s.stop(spinnerText);
return result;
@@ -57,13 +37,10 @@ async function executeNeonCommand(
}
}
async function isNeonAuthenticated(packageManager: string) {
async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
try {
const { cmd, cmdArgs } = buildNeonCommand(packageManager, [
"projects",
"list",
]);
const result = await execa(cmd, cmdArgs);
const commandArgsString = "neonctl projects list";
const result = await executeNeonCommand(packageManager, commandArgsString);
return (
!result.stdout.includes("not authenticated") &&
!result.stdout.includes("error")
@@ -73,11 +50,11 @@ async function isNeonAuthenticated(packageManager: string) {
}
}
async function authenticateWithNeon(packageManager: string) {
async function authenticateWithNeon(packageManager: ProjectPackageManager) {
try {
await executeNeonCommand(
packageManager,
["auth"],
"neonctl auth",
"Authenticating with Neon...",
);
log.success("Authenticated with Neon successfully!");
@@ -90,12 +67,13 @@ async function authenticateWithNeon(packageManager: string) {
async function createNeonProject(
projectName: string,
packageManager: string,
packageManager: ProjectPackageManager,
): Promise<NeonConfig | null> {
try {
const commandArgsString = `neonctl projects create --name "${projectName}" --output json`;
const { stdout } = await executeNeonCommand(
packageManager,
["projects", "create", "--name", projectName, "--output", "json"],
commandArgsString,
`Creating Neon project "${projectName}"...`,
);
@@ -150,10 +128,11 @@ function displayManualSetupInstructions() {
DATABASE_URL="your_connection_string"`);
}
export async function setupNeonPostgres(
projectDir: string,
packageManager: ProjectPackageManager,
) {
import type { ProjectConfig } from "../types";
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
const { projectName, packageManager } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const setupSpinner = spinner();
setupSpinner.start("Setting up Neon PostgreSQL");

View File

@@ -9,18 +9,24 @@ import type {
ProjectPackageManager,
ProjectRuntime,
} from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
import type { ProjectConfig } from "../types";
export function displayPostInstallInstructions(
database: ProjectDatabase,
projectName: string,
packageManager: ProjectPackageManager,
depsInstalled: boolean,
orm: ProjectOrm,
addons: ProjectAddons[],
runtime: ProjectRuntime,
frontends: ProjectFrontend[],
dbSetup?: ProjectDBSetup,
config: ProjectConfig & { depsInstalled: boolean },
) {
const {
database,
projectName,
packageManager,
depsInstalled,
orm,
addons,
runtime,
frontend,
dbSetup,
} = config;
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`;
const hasHuskyOrBiome =
@@ -36,38 +42,59 @@ export function displayPostInstallInstructions(
const lintingInstructions = hasHuskyOrBiome
? getLintingInstructions(runCmd)
: "";
const nativeInstructions = frontends?.includes("native")
const nativeInstructions = frontend?.includes("native")
? getNativeInstructions()
: "";
const pwaInstructions =
addons?.includes("pwa") && frontends?.includes("react-router")
addons?.includes("pwa") && frontend?.includes("react-router")
? getPwaInstructions()
: "";
const starlightInstructions = addons?.includes("starlight")
? getStarlightInstructions(runCmd)
: "";
const hasTanstackRouter = frontends?.includes("tanstack-router");
const hasTanstackStart = frontends?.includes("tanstack-start");
const hasReactRouter = frontends?.includes("react-router");
const hasTanstackRouter = frontend?.includes("tanstack-router");
const hasTanstackStart = frontend?.includes("tanstack-start");
const hasReactRouter = frontend?.includes("react-router");
const hasWebFrontend =
hasTanstackRouter || hasReactRouter || hasTanstackStart;
const hasNativeFrontend = frontends?.includes("native");
const hasNativeFrontend = frontend?.includes("native");
const hasFrontend = hasWebFrontend || hasNativeFrontend;
const webPort = hasReactRouter ? "5173" : "3001";
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
consola.box(
`${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}
${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
${
!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""
}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
${pc.bold("Your project will be available at:")}
${
hasFrontend
? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n` : ""}`
: `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`
}${pc.cyan("•")} API: http://localhost:3000
${addons?.includes("starlight") ? `${pc.cyan("•")} Docs: http://localhost:4321\n` : ""}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${starlightInstructions ? `\n${starlightInstructions.trim()}` : ""}
? `${
hasWebFrontend
? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`
: ""
}`
: `${pc.yellow(
"NOTE:",
)} You are creating a backend-only app (no frontend selected)\n`
}${pc.cyan("•")} Backend: http://localhost:3000
${
addons?.includes("starlight")
? `${pc.cyan("•")} Docs: http://localhost:4321\n`
: ""
}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${
databaseInstructions ? `\n${databaseInstructions.trim()}` : ""
}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${
lintingInstructions ? `\n${lintingInstructions.trim()}` : ""
}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${
starlightInstructions ? `\n${starlightInstructions.trim()}` : ""
}
${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}
${pc.bold("Like Better-T Stack?")} Please consider giving us a star on GitHub:
${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`,
@@ -75,11 +102,15 @@ ${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`,
}
function getNativeInstructions(): string {
return `${pc.yellow("NOTE:")} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`;
return `${pc.yellow(
"NOTE:",
)} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`;
}
function getLintingInstructions(runCmd?: string): string {
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n\n`;
return `${pc.bold("Linting and formatting:")}\n${pc.cyan(
"•",
)} Format and lint fix: ${`${runCmd} check`}\n\n`;
}
function getDatabaseInstructions(
@@ -93,14 +124,18 @@ function getDatabaseInstructions(
if (orm === "prisma") {
if (database === "sqlite") {
instructions.push(
`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires additional setup.`,
`${pc.yellow(
"NOTE:",
)} Turso support with Prisma is in Early Access and requires additional setup.`,
`${"Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso"}`,
);
}
if (runtime === "bun") {
instructions.push(
`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`,
`${pc.yellow(
"NOTE:",
)} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`,
);
}
@@ -117,13 +152,25 @@ function getDatabaseInstructions(
}
function getTauriInstructions(runCmd?: string): string {
return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`;
return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan(
"•",
)} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan(
"•",
)} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow(
"NOTE:",
)} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`;
}
function getPwaInstructions(): string {
return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`;
return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
"NOTE:",
)} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`;
}
function getStarlightInstructions(runCmd?: string): string {
return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan("•")} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan("•")} Build docs site: ${`cd apps/docs && ${runCmd} build`}\n`;
return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan(
"•",
)} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan(
"•",
)} Build docs site: ${`cd apps/docs && ${runCmd} build`}\n`;
}

View File

@@ -6,6 +6,7 @@ import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectPackageManager } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
type PrismaConfig = {
databaseUrl: string;
@@ -22,18 +23,17 @@ async function initPrismaDatabase(
const prismaDir = path.join(serverDir, "prisma");
await fs.ensureDir(prismaDir);
const initCmd =
packageManager === "npm"
? "npx"
: packageManager === "pnpm"
? "pnpm dlx"
: "bunx";
s.stop("Initializing Prisma. Follow the prompts below:");
await execa(initCmd, ["prisma", "init", "--db"], {
const prismaInitCommand = getPackageExecutionCommand(
packageManager,
"prisma init --db",
);
await execa(prismaInitCommand, {
cwd: serverDir,
stdio: "inherit",
shell: true,
});
log.info(
@@ -112,7 +112,7 @@ DATABASE_URL="your_database_url"`);
async function addPrismaAccelerateExtension(serverDir: string) {
try {
addPackageDependency({
await addPackageDependency({
dependencies: ["@prisma/extension-accelerate"],
projectDir: serverDir,
});
@@ -152,10 +152,11 @@ export default prisma;
}
}
export async function setupPrismaPostgres(
projectDir: string,
packageManager: ProjectPackageManager = "npm",
) {
import type { ProjectConfig } from "../types";
export async function setupPrismaPostgres(config: ProjectConfig) {
const { projectName, packageManager } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server");
const s = spinner();
s.start("Setting up Prisma PostgreSQL");
@@ -184,7 +185,9 @@ export async function setupPrismaPostgres(
s.stop(pc.red("Prisma PostgreSQL setup failed"));
consola.error(
pc.red(
`Error during Prisma PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`,
`Error during Prisma PostgreSQL setup: ${
error instanceof Error ? error.message : String(error)
}`,
),
);

View File

@@ -3,7 +3,6 @@ import { log } from "@clack/prompts";
import { $, execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import { PKG_ROOT } from "../constants";
import type { ProjectConfig } from "../types";
export async function updatePackageConfigurations(
@@ -23,24 +22,71 @@ async function updateRootPackageJson(
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
// Define script sets
const turboScripts = {
dev: "turbo dev",
build: "turbo build",
"check-types": "turbo check-types",
"dev:native": "turbo -F native dev",
"dev:web": "turbo -F web dev",
"dev:server": "turbo -F server dev",
"db:push": "turbo -F server db:push",
"db:studio": "turbo -F server db:studio",
};
const pnpmScripts = {
dev: "pnpm -r --parallel dev",
build: "pnpm -r build",
"check-types": "pnpm -r check-types",
"dev:native": "pnpm --filter native dev",
"dev:web": "pnpm --filter web dev",
"dev:server": "pnpm --filter server dev",
"db:push": "pnpm --filter server db:push",
"db:studio": "pnpm --filter server db:studio",
};
const npmScripts = {
dev: "npm run dev --workspaces",
build: "npm run build --workspaces",
"check-types": "npm run check-types --workspaces",
"dev:native": "npm run dev --workspace native",
"dev:web": "npm run dev --workspace web",
"dev:server": "npm run dev --workspace server",
"db:push": "npm run db:push --workspace server",
"db:studio": "npm run db:studio --workspace server",
};
const bunScripts = {
dev: "bun run --filter '*' dev",
build: "bun run --filter '*' build",
"check-types": "bun run --filter '*' check-types",
"dev:native": "bun run --filter native dev",
"dev:web": "bun run --filter web dev",
"dev:server": "bun run --filter server dev",
"db:push": "bun run --filter server db:push",
"db:studio": "bun run --filter server db:studio",
};
if (options.addons.includes("turborepo")) {
packageJson.scripts = turboScripts;
} else {
if (options.packageManager === "pnpm") {
packageJson.scripts = pnpmScripts;
} else if (options.packageManager === "npm") {
packageJson.scripts = npmScripts;
} else if (options.packageManager === "bun") {
packageJson.scripts = bunScripts;
} else {
packageJson.scripts = {};
}
}
const { stdout } = await execa(options.packageManager, ["-v"], {
cwd: projectDir,
});
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
if (options.packageManager === "pnpm") {
const pnpmWorkspaceTemplatePath = path.join(
PKG_ROOT,
"template/with-pnpm/pnpm-workspace.yaml",
);
const targetWorkspacePath = path.join(projectDir, "pnpm-workspace.yaml");
if (await fs.pathExists(pnpmWorkspaceTemplatePath)) {
await fs.copy(pnpmWorkspaceTemplatePath, targetWorkspacePath);
}
}
}
}
@@ -57,7 +103,7 @@ async function updateServerPackageJson(
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (options.database !== "none") {
if (options.database === "sqlite") {
if (options.database === "sqlite" && options.orm === "drizzle") {
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
}

View File

@@ -1,44 +1,27 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectBackend, ProjectRuntime } from "../types";
import type { ProjectBackend, ProjectConfig, ProjectRuntime } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupRuntime(
projectDir: string,
runtime: ProjectRuntime,
backendFramework: ProjectBackend,
): Promise<void> {
if (backendFramework === "next") {
export async function setupRuntime(config: ProjectConfig): Promise<void> {
const { projectName, runtime, backend } = config;
const projectDir = path.resolve(process.cwd(), projectName);
if (backend === "next") {
return;
}
const serverDir = path.join(projectDir, "apps/server");
const serverIndexPath = path.join(serverDir, "src/index.ts");
const indexContent = await fs.readFile(serverIndexPath, "utf-8");
if (runtime === "bun") {
await setupBunRuntime(
serverDir,
serverIndexPath,
indexContent,
backendFramework,
);
await setupBunRuntime(serverDir, backend);
} else if (runtime === "node") {
await setupNodeRuntime(
serverDir,
serverIndexPath,
indexContent,
backendFramework,
);
await setupNodeRuntime(serverDir, backend);
}
}
async function setupBunRuntime(
serverDir: string,
serverIndexPath: string,
indexContent: string,
backendFramework: ProjectBackend,
backend: ProjectBackend,
): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath);
@@ -51,22 +34,15 @@ async function setupBunRuntime(
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
addPackageDependency({
await addPackageDependency({
devDependencies: ["@types/bun"],
projectDir: serverDir,
});
if (backendFramework === "hono") {
const updatedContent = `${indexContent}\n\nexport default app;\n`;
await fs.writeFile(serverIndexPath, updatedContent);
}
}
async function setupNodeRuntime(
serverDir: string,
serverIndexPath: string,
indexContent: string,
backendFramework: ProjectBackend,
backend: ProjectBackend,
): Promise<void> {
const packageJsonPath = path.join(serverDir, "package.json");
const packageJson = await fs.readJson(packageJsonPath);
@@ -79,62 +55,20 @@ async function setupNodeRuntime(
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
addPackageDependency({
await addPackageDependency({
devDependencies: ["tsx", "@types/node"],
projectDir: serverDir,
});
if (backendFramework === "hono") {
addPackageDependency({
if (backend === "hono") {
await addPackageDependency({
dependencies: ["@hono/node-server"],
projectDir: serverDir,
});
const importLine = 'import { serve } from "@hono/node-server";\n';
const serverCode = `
serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(\`Server is running on http://localhost:\${info.port}\`);
},
);\n`;
if (!indexContent.includes("@hono/node-server")) {
const importEndIndex = indexContent.lastIndexOf("import");
const importSection = indexContent.substring(0, importEndIndex);
const restOfFile = indexContent.substring(importEndIndex);
const updatedContent =
importSection + importLine + restOfFile + serverCode;
await fs.writeFile(serverIndexPath, updatedContent);
}
} else if (backendFramework === "elysia") {
addPackageDependency({
} else if (backend === "elysia") {
await addPackageDependency({
dependencies: ["@elysiajs/node"],
projectDir: serverDir,
});
if (!indexContent.includes("@elysiajs/node")) {
const nodeImport = 'import { node } from "@elysiajs/node";\n';
const firstImportEnd = indexContent.indexOf(
"\n",
indexContent.indexOf("import"),
);
const before = indexContent.substring(0, firstImportEnd + 1);
const after = indexContent.substring(firstImportEnd + 1);
let updatedContent = before + nodeImport + after;
updatedContent = updatedContent.replace(
/const app = new Elysia\([^)]*\)/,
"const app = new Elysia({ adapter: node() })",
);
await fs.writeFile(serverIndexPath, updatedContent);
}
}
}

View File

@@ -1,42 +1,20 @@
import path from "node:path";
import { log, spinner } from "@clack/prompts";
import { spinner } from "@clack/prompts";
import consola from "consola";
import { execa } from "execa";
import pc from "picocolors";
import type { ProjectPackageManager } from "../types";
import type { ProjectConfig } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
export async function setupStarlight(
projectDir: string,
packageManager: ProjectPackageManager,
): Promise<void> {
export async function setupStarlight(config: ProjectConfig): Promise<void> {
const { projectName, packageManager } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const s = spinner();
try {
s.start("Setting up Starlight documentation site...");
s.start("Setting up Starlight docs...");
let cmd: string;
let args: string[];
switch (packageManager) {
case "npm":
cmd = "npx";
args = ["create-astro@latest"];
break;
case "pnpm":
cmd = "pnpm";
args = ["dlx", "create-astro@latest"];
break;
case "bun":
cmd = "bunx";
args = ["create-astro@latest"];
break;
default:
cmd = "npx";
args = ["create-astro@latest"];
}
args = [
...args,
const starlightArgs = [
"docs",
"--template",
"starlight",
@@ -46,17 +24,26 @@ export async function setupStarlight(
"--no-git",
"--skip-houston",
];
const starlightArgsString = starlightArgs.join(" ");
await execa(cmd, args, {
const commandWithArgs = `create-astro@latest ${starlightArgsString}`;
const starlightInitCommand = getPackageExecutionCommand(
packageManager,
commandWithArgs,
);
await execa(starlightInitCommand, {
cwd: path.join(projectDir, "apps"),
env: {
CI: "true",
},
shell: true,
});
s.stop("Starlight documentation site setup successfully!");
s.stop("Starlight docs setup successfully!");
} catch (error) {
s.stop(pc.red("Failed to set up Starlight documentation site"));
s.stop(pc.red("Failed to set up Starlight docs"));
if (error instanceof Error) {
consola.error(pc.red(error.message));
}

View File

@@ -1,17 +1,17 @@
import path from "node:path";
import { log, spinner } from "@clack/prompts";
import { spinner } from "@clack/prompts";
import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectFrontend, ProjectPackageManager } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
export async function setupTauri(
projectDir: string,
packageManager: ProjectPackageManager,
frontends: ProjectFrontend[],
): Promise<void> {
import type { ProjectConfig } from "../types";
export async function setupTauri(config: ProjectConfig): Promise<void> {
const { projectName, packageManager, frontend } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const s = spinner();
const clientPackageDir = path.join(projectDir, "apps/web");
@@ -22,7 +22,7 @@ export async function setupTauri(
try {
s.start("Setting up Tauri desktop app support...");
addPackageDependency({
await addPackageDependency({
devDependencies: ["@tauri-apps/cli"],
projectDir: clientPackageDir,
});
@@ -41,48 +41,35 @@ export async function setupTauri(
await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
}
let cmd: string;
let args: string[];
switch (packageManager) {
case "npm":
cmd = "npx";
args = ["@tauri-apps/cli@latest"];
break;
case "pnpm":
cmd = "pnpm";
args = ["dlx", "@tauri-apps/cli@latest"];
break;
case "bun":
cmd = "bunx";
args = ["@tauri-apps/cli@latest"];
break;
default:
cmd = "npx";
args = ["@tauri-apps/cli@latest"];
}
const hasReactRouter = frontends.includes("react-router");
const hasReactRouter = frontend.includes("react-router");
const devUrl = hasReactRouter
? "http://localhost:5173"
: "http://localhost:3001";
args = [
...args,
const tauriArgs = [
"init",
`--app-name=${path.basename(projectDir)}`,
`--window-title=${path.basename(projectDir)}`,
"--frontend-dist=../dist",
`--dev-url=${devUrl}`,
`--before-dev-command=${packageManager} run dev`,
`--before-build-command=${packageManager} run build`,
`--before-dev-command=\"${packageManager} run dev\"`,
`--before-build-command=\"${packageManager} run build\"`,
];
const tauriArgsString = tauriArgs.join(" ");
await execa(cmd, args, {
const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
const tauriInitCommand = getPackageExecutionCommand(
packageManager,
commandWithArgs,
);
await execa(tauriInitCommand, {
cwd: clientPackageDir,
env: {
CI: "true",
},
shell: true,
});
s.stop("Tauri desktop app support configured successfully!");

View File

@@ -1,493 +1,538 @@
import path from "node:path";
import consola from "consola";
import fs from "fs-extra";
import { globby } from "globby";
import pc from "picocolors";
import { PKG_ROOT } from "../constants";
import type {
ProjectBackend,
ProjectDatabase,
ProjectFrontend,
ProjectOrm,
} from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
import type { ProjectConfig } from "../types";
import { processTemplate } from "../utils/template-processor";
/**
* Copy base template structure but exclude app-specific folders that will be added based on options
*/
export async function copyBaseTemplate(projectDir: string): Promise<void> {
const templateDir = path.join(PKG_ROOT, "template/base");
async function processAndCopyFiles(
sourcePattern: string | string[],
baseSourceDir: string,
destDir: string,
context: ProjectConfig,
overwrite = true,
): Promise<void> {
const sourceFiles = await globby(sourcePattern, {
cwd: baseSourceDir,
dot: true,
onlyFiles: true,
absolute: false,
});
if (!(await fs.pathExists(templateDir))) {
throw new Error(`Template directory not found: ${templateDir}`);
}
for (const relativeSrcPath of sourceFiles) {
const srcPath = path.join(baseSourceDir, relativeSrcPath);
let relativeDestPath = relativeSrcPath;
await fs.ensureDir(projectDir);
if (relativeSrcPath.endsWith(".hbs")) {
relativeDestPath = relativeSrcPath.slice(0, -4);
}
const rootFiles = await fs.readdir(templateDir);
for (const file of rootFiles) {
const srcPath = path.join(templateDir, file);
const destPath = path.join(projectDir, file);
const destPath = path.join(destDir, relativeDestPath);
if (file === "apps") continue;
await fs.ensureDir(path.dirname(destPath));
if (await fs.stat(srcPath).then((stat) => stat.isDirectory())) {
await fs.copy(srcPath, destPath);
if (srcPath.endsWith(".hbs")) {
await processTemplate(srcPath, destPath, context);
} else {
await fs.copy(srcPath, destPath);
if (!overwrite && (await fs.pathExists(destPath))) {
continue;
}
await fs.copy(srcPath, destPath, { overwrite: true });
}
}
}
await fs.ensureDir(path.join(projectDir, "apps"));
const serverSrcDir = path.join(templateDir, "apps/server");
const serverDestDir = path.join(projectDir, "apps/server");
if (await fs.pathExists(serverSrcDir)) {
await fs.copy(serverSrcDir, serverDestDir);
}
export async function copyBaseTemplate(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
const templateDir = path.join(PKG_ROOT, "templates/base");
await processAndCopyFiles(
["package.json", "_gitignore"],
templateDir,
projectDir,
context,
);
}
export async function setupFrontendTemplates(
projectDir: string,
frontends: ProjectFrontend[],
context: ProjectConfig,
): Promise<void> {
const hasTanstackWeb = frontends.includes("tanstack-router");
const hasTanstackStart = frontends.includes("tanstack-start");
const hasReactRouterWeb = frontends.includes("react-router");
const hasNextWeb = frontends.includes("next");
const hasNative = frontends.includes("native");
const webFrontends = context.frontend.filter(
(f) =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start" ||
f === "next",
);
const hasNative = context.frontend.includes("native");
if (hasTanstackWeb || hasReactRouterWeb || hasTanstackStart || hasNextWeb) {
const webDir = path.join(projectDir, "apps/web");
await fs.ensureDir(webDir);
if (webFrontends.length > 0) {
const webAppDir = path.join(projectDir, "apps/web");
await fs.ensureDir(webAppDir);
const webBaseDir = path.join(PKG_ROOT, "template/base/apps/web-base");
const webBaseDir = path.join(PKG_ROOT, "templates/frontend/web-base");
if (await fs.pathExists(webBaseDir)) {
await fs.copy(webBaseDir, webDir);
await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
}
if (hasTanstackWeb) {
const frameworkDir = path.join(
for (const framework of webFrontends) {
const frameworkSrcDir = path.join(
PKG_ROOT,
"template/base/apps/web-tanstack-router",
`templates/frontend/${framework}`,
);
if (await fs.pathExists(frameworkDir)) {
await fs.copy(frameworkDir, webDir, { overwrite: true });
}
} else if (hasTanstackStart) {
const frameworkDir = path.join(
PKG_ROOT,
"template/base/apps/web-tanstack-start",
);
if (await fs.pathExists(frameworkDir)) {
await fs.copy(frameworkDir, webDir, { overwrite: true });
}
} else if (hasReactRouterWeb) {
const frameworkDir = path.join(
PKG_ROOT,
"template/base/apps/web-react-router",
);
if (await fs.pathExists(frameworkDir)) {
await fs.copy(frameworkDir, webDir, { overwrite: true });
}
} else if (hasNextWeb) {
const frameworkDir = path.join(PKG_ROOT, "template/base/apps/web-next");
if (await fs.pathExists(frameworkDir)) {
await fs.copy(frameworkDir, webDir, { overwrite: true });
if (await fs.pathExists(frameworkSrcDir)) {
await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
}
}
const packageJsonPath = path.join(webDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = "web";
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
if (context.api !== "none") {
const webFramework = webFrontends[0];
const apiWebBaseDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/base`,
);
if (await fs.pathExists(apiWebBaseDir)) {
await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
}
const apiWebFrameworkDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/web/${webFramework}`,
);
if (await fs.pathExists(apiWebFrameworkDir)) {
await processAndCopyFiles(
"**/*",
apiWebFrameworkDir,
webAppDir,
context,
);
}
}
}
if (hasNative) {
const nativeSrcDir = path.join(PKG_ROOT, "template/base/apps/native");
const nativeDestDir = path.join(projectDir, "apps/native");
const nativeAppDir = path.join(projectDir, "apps/native");
await fs.ensureDir(nativeAppDir);
if (await fs.pathExists(nativeSrcDir)) {
await fs.copy(nativeSrcDir, nativeDestDir);
const nativeBaseDir = path.join(PKG_ROOT, "templates/frontend/native");
if (await fs.pathExists(nativeBaseDir)) {
await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context);
}
await fs.writeFile(
path.join(projectDir, ".npmrc"),
"node-linker=hoisted\n",
);
if (context.api !== "none") {
const apiNativeSrcDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/native`,
);
if (await fs.pathExists(apiNativeSrcDir)) {
await processAndCopyFiles(
"**/*",
apiNativeSrcDir,
nativeAppDir,
context,
);
} else {
}
}
}
}
export async function setupBackendFramework(
projectDir: string,
framework: ProjectBackend,
context: ProjectConfig,
): Promise<void> {
if (framework === "next") {
const serverDir = path.join(projectDir, "apps/server");
const nextTemplateDir = path.join(
PKG_ROOT,
"template/with-next/apps/server",
if ((context.backend as string) === "none") return;
const serverAppDir = path.join(projectDir, "apps/server");
await fs.ensureDir(serverAppDir);
const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server-base");
if (await fs.pathExists(serverBaseDir)) {
await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
} else {
consola.warn(
pc.yellow(`Warning: server-base template not found at ${serverBaseDir}`),
);
await fs.ensureDir(serverDir);
if (await fs.pathExists(nextTemplateDir)) {
await fs.copy(nextTemplateDir, serverDir, { overwrite: true });
const packageJsonPath = path.join(serverDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = "server";
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
}
return;
}
const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`);
if (await fs.pathExists(frameworkDir)) {
await fs.copy(frameworkDir, projectDir, { overwrite: true });
const frameworkSrcDir = path.join(
PKG_ROOT,
`templates/backend/${context.backend}`,
);
if (await fs.pathExists(frameworkSrcDir)) {
await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Backend template directory not found, skipping: ${frameworkSrcDir}`,
),
);
}
if (context.api !== "none") {
const apiServerBaseDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/server/base`,
);
if (await fs.pathExists(apiServerBaseDir)) {
await processAndCopyFiles(
"**/*",
apiServerBaseDir,
serverAppDir,
context,
);
}
const apiServerFrameworkDir = path.join(
PKG_ROOT,
`templates/api/${context.api}/server/${context.backend}`,
);
if (await fs.pathExists(apiServerFrameworkDir)) {
await processAndCopyFiles(
"**/*",
apiServerFrameworkDir,
serverAppDir,
context,
);
}
}
}
export async function setupOrmTemplate(
export async function setupDbOrmTemplates(
projectDir: string,
orm: ProjectOrm,
database: ProjectDatabase,
auth: boolean,
context: ProjectConfig,
): Promise<void> {
if (orm === "none" || database === "none") return;
if (context.orm === "none" || context.database === "none") return;
const ormTemplateDir = path.join(PKG_ROOT, getOrmTemplateDir(orm, database));
const serverAppDir = path.join(projectDir, "apps/server");
await fs.ensureDir(serverAppDir);
if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
const dbOrmSrcDir = path.join(
PKG_ROOT,
`templates/db/${context.orm}/${context.database}`,
);
if (!auth) {
if (orm === "prisma") {
const authSchemaPath = path.join(
projectDir,
"apps/server/prisma/schema/auth.prisma",
);
if (await fs.pathExists(authSchemaPath)) {
await fs.remove(authSchemaPath);
}
} else if (orm === "drizzle") {
const authSchemaPath = path.join(
projectDir,
"apps/server/src/db/schema/auth.ts",
);
if (await fs.pathExists(authSchemaPath)) {
await fs.remove(authSchemaPath);
}
}
}
if (await fs.pathExists(dbOrmSrcDir)) {
await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Database/ORM template directory not found, skipping: ${dbOrmSrcDir}`,
),
);
}
}
export async function setupAuthTemplate(
projectDir: string,
auth: boolean,
framework: ProjectBackend,
orm: ProjectOrm,
database: ProjectDatabase,
frontends: ProjectFrontend[],
context: ProjectConfig,
): Promise<void> {
if (!auth) return;
if (!context.auth) return;
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
if (await fs.pathExists(authTemplateDir)) {
const hasReactRouter = frontends.includes("react-router");
const hasTanStackRouter = frontends.includes("tanstack-router");
const hasTanStackStart = frontends.includes("tanstack-start");
const hasNextRouter = frontends.includes("next");
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
const nativeAppDir = path.join(projectDir, "apps/native");
const webFrontends = context.frontend.filter(
(f) =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start" ||
f === "next",
);
const hasNative = context.frontend.includes("native");
if (
hasReactRouter ||
hasTanStackRouter ||
hasTanStackStart ||
hasNextRouter
) {
const webDir = path.join(projectDir, "apps/web");
const webBaseAuthDir = path.join(authTemplateDir, "apps/web-base");
if (await fs.pathExists(webBaseAuthDir)) {
await fs.copy(webBaseAuthDir, webDir, { overwrite: true });
}
if (hasReactRouter) {
const reactRouterAuthDir = path.join(
authTemplateDir,
"apps/web-react-router",
);
if (await fs.pathExists(reactRouterAuthDir)) {
await fs.copy(reactRouterAuthDir, webDir, { overwrite: true });
}
}
if (hasTanStackRouter) {
const tanstackAuthDir = path.join(
authTemplateDir,
"apps/web-tanstack-router",
);
if (await fs.pathExists(tanstackAuthDir)) {
await fs.copy(tanstackAuthDir, webDir, { overwrite: true });
}
}
if (hasTanStackStart) {
const tanstackStartAuthDir = path.join(
authTemplateDir,
"apps/web-tanstack-start",
);
if (await fs.pathExists(tanstackStartAuthDir)) {
await fs.copy(tanstackStartAuthDir, webDir, { overwrite: true });
}
}
if (hasNextRouter) {
const nextAuthDir = path.join(authTemplateDir, "apps/web-next");
if (await fs.pathExists(nextAuthDir)) {
await fs.copy(nextAuthDir, webDir, { overwrite: true });
}
}
if (await fs.pathExists(serverAppDir)) {
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
if (await fs.pathExists(authServerBaseSrc)) {
await processAndCopyFiles(
"**/*",
authServerBaseSrc,
serverAppDir,
context,
);
} else {
consola.warn(
pc.yellow(
`Warning: Base auth server template not found at ${authServerBaseSrc}`,
),
);
}
const serverAuthDir = path.join(authTemplateDir, "apps/server/src");
const projectServerDir = path.join(projectDir, "apps/server/src");
await fs.copy(
path.join(serverAuthDir, "lib/trpc.ts"),
path.join(projectServerDir, "lib/trpc.ts"),
{ overwrite: true },
);
await fs.copy(
path.join(serverAuthDir, "routers/index.ts"),
path.join(projectServerDir, "routers/index.ts"),
{ overwrite: true },
);
if (framework === "next") {
if (
await fs.pathExists(
path.join(authTemplateDir, "apps/server/src/with-next-app"),
)
) {
const nextAppAuthDir = path.join(
authTemplateDir,
"apps/server/src/with-next-app",
);
const nextAppDestDir = path.join(projectDir, "apps/server/src/app");
await fs.ensureDir(nextAppDestDir);
const files = await fs.readdir(nextAppAuthDir);
for (const file of files) {
const srcPath = path.join(nextAppAuthDir, file);
const destPath = path.join(nextAppDestDir, file);
await fs.copy(srcPath, destPath, { overwrite: true });
}
}
const contextFileName = "with-next-context.ts";
await fs.copy(
path.join(serverAuthDir, "lib", contextFileName),
path.join(projectServerDir, "lib/context.ts"),
{ overwrite: true },
const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
if (await fs.pathExists(authServerNextSrc)) {
await processAndCopyFiles(
"**/*",
authServerNextSrc,
serverAppDir,
context,
);
} else {
consola.warn(
pc.yellow(
`Warning: Next auth server template not found at ${authServerNextSrc}`,
),
);
}
const authLibFileName = getAuthLibDir(orm, database);
const authLibSourceDir = path.join(serverAuthDir, authLibFileName);
if (await fs.pathExists(authLibSourceDir)) {
const files = await fs.readdir(authLibSourceDir);
for (const file of files) {
await fs.copy(
path.join(authLibSourceDir, file),
path.join(projectServerDir, "lib", file),
{ overwrite: true },
);
}
if (context.orm !== "none" && context.database !== "none") {
const orm = context.orm;
const db = context.database;
let authDbSrc = "";
if (orm === "drizzle") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/drizzle/${db}`,
);
} else if (orm === "prisma") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/prisma/${db}`,
);
}
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Auth template for ${orm}/${db} not found at ${authDbSrc}`,
),
);
}
}
} else {
consola.warn(
pc.yellow(
"Warning: apps/server directory does not exist, skipping server-side auth setup.",
),
);
}
if (webFrontends.length > 0 && (await fs.pathExists(webAppDir))) {
const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/base");
if (await fs.pathExists(authWebBaseSrc)) {
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Base auth web template not found at ${authWebBaseSrc}`,
),
);
}
for (const framework of webFrontends) {
const authWebFrameworkSrc = path.join(
PKG_ROOT,
`templates/auth/web/${framework}`,
);
if (await fs.pathExists(authWebFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
authWebFrameworkSrc,
webAppDir,
context,
);
} else {
consola.warn(
pc.yellow(
`Warning: Auth web template for ${framework} not found at ${authWebFrameworkSrc}`,
),
);
}
}
}
if (hasNative && (await fs.pathExists(nativeAppDir))) {
const authNativeSrc = path.join(PKG_ROOT, "templates/auth/native");
if (await fs.pathExists(authNativeSrc)) {
await processAndCopyFiles("**/*", authNativeSrc, nativeAppDir, context);
} else {
consola.warn(
pc.yellow(
`Warning: Auth native template not found at ${authNativeSrc}`,
),
);
}
}
}
export async function setupAddonsTemplate(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if (context.addons.includes("turborepo")) {
const turboSrcDir = path.join(PKG_ROOT, "templates/addons/turborepo");
if (await fs.pathExists(turboSrcDir)) {
await processAndCopyFiles("**/*", turboSrcDir, projectDir, context);
} else {
consola.warn(pc.yellow("Warning: Turborepo addon template not found."));
}
}
if (context.addons.includes("husky")) {
const huskySrcDir = path.join(PKG_ROOT, "templates/addons/husky");
if (await fs.pathExists(huskySrcDir)) {
await processAndCopyFiles("**/*", huskySrcDir, projectDir, context);
} else {
consola.warn(pc.yellow("Warning: Husky addon template not found."));
}
}
if (context.addons.includes("biome")) {
const biomeSrcDir = path.join(PKG_ROOT, "templates/addons/biome");
if (await fs.pathExists(biomeSrcDir)) {
await processAndCopyFiles("**/*", biomeSrcDir, projectDir, context);
} else {
consola.warn(pc.yellow("Warning: Biome addon template not found."));
}
}
if (context.addons.includes("pwa")) {
const pwaSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web");
const webAppDir = path.join(projectDir, "apps/web");
if (await fs.pathExists(pwaSrcDir)) {
if (await fs.pathExists(webAppDir)) {
await processAndCopyFiles("**/*", pwaSrcDir, webAppDir, context);
} else {
consola.warn(
pc.yellow(
"Warning: apps/web directory not found, cannot setup PWA addon.",
),
);
}
} else {
const contextFileName = `with-${framework}-context.ts`;
await fs.copy(
path.join(serverAuthDir, "lib", contextFileName),
path.join(projectServerDir, "lib/context.ts"),
{ overwrite: true },
);
consola.warn(pc.yellow("Warning: PWA addon template not found."));
}
}
}
const indexFileName = `with-${framework}-index.ts`;
await fs.copy(
path.join(serverAuthDir, indexFileName),
path.join(projectServerDir, "index.ts"),
{ overwrite: true },
);
export async function setupExamplesTemplate(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if (!context.examples || context.examples.length === 0) return;
const authLibFileName = getAuthLibDir(orm, database);
const authLibSourceDir = path.join(serverAuthDir, authLibFileName);
if (await fs.pathExists(authLibSourceDir)) {
const files = await fs.readdir(authLibSourceDir);
for (const file of files) {
await fs.copy(
path.join(authLibSourceDir, file),
path.join(projectServerDir, "lib", file),
{ overwrite: true },
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
for (const example of context.examples) {
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
if (await fs.pathExists(serverAppDir)) {
const exampleServerSrc = path.join(exampleBaseDir, "server");
if (await fs.pathExists(exampleServerSrc)) {
if (context.orm !== "none") {
const exampleOrmBaseSrc = path.join(
exampleServerSrc,
context.orm,
"base",
);
if (await fs.pathExists(exampleOrmBaseSrc)) {
await processAndCopyFiles(
"**/*",
exampleOrmBaseSrc,
serverAppDir,
context,
false,
);
}
if (context.database !== "none") {
const exampleDbSchemaSrc = path.join(
exampleServerSrc,
context.orm,
context.database,
);
if (await fs.pathExists(exampleDbSchemaSrc)) {
await processAndCopyFiles(
"**/*",
exampleDbSchemaSrc,
serverAppDir,
context,
false,
);
}
}
}
}
}
if (frontends.includes("native")) {
const nativeAuthDir = path.join(authTemplateDir, "apps/native");
const projectNativeDir = path.join(projectDir, "apps/native");
if (await fs.pathExists(nativeAuthDir)) {
await fs.copy(nativeAuthDir, projectNativeDir, { overwrite: true });
if (await fs.pathExists(webAppDir)) {
const exampleWebSrc = path.join(exampleBaseDir, "web");
if (await fs.pathExists(exampleWebSrc)) {
const webFrameworks = context.frontend.filter((f) =>
[
"next",
"react-router",
"tanstack-router",
"tanstack-start",
].includes(f),
);
for (const framework of webFrameworks) {
const exampleWebFrameworkSrc = path.join(exampleWebSrc, framework);
if (await fs.pathExists(exampleWebFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
exampleWebFrameworkSrc,
webAppDir,
context,
false,
);
}
}
}
addPackageDependency({
dependencies: ["@better-auth/expo"],
projectDir: path.join(projectDir, "apps/server"),
});
await updateAuthConfigWithExpoPlugin(projectDir, orm, database);
}
}
}
// Need to find a better way to handle this
async function updateAuthConfigWithExpoPlugin(
export async function fixGitignoreFiles(
projectDir: string,
orm: ProjectOrm,
database: ProjectDatabase,
context: ProjectConfig,
): Promise<void> {
const serverDir = path.join(projectDir, "apps/server");
const gitignoreFiles = await globby(["**/.gitignore.hbs", "**/_gitignore"], {
cwd: projectDir,
dot: true,
onlyFiles: true,
absolute: true,
ignore: ["**/node_modules/**", "**/.git/**"],
});
let authFilePath: string | undefined;
if (orm === "drizzle") {
if (database === "sqlite") {
authFilePath = path.join(serverDir, "src/lib/auth.ts");
} else if (database === "postgres") {
authFilePath = path.join(serverDir, "src/lib/auth.ts");
}
} else if (orm === "prisma") {
if (database === "sqlite") {
authFilePath = path.join(serverDir, "src/lib/auth.ts");
} else if (database === "postgres") {
authFilePath = path.join(serverDir, "src/lib/auth.ts");
}
}
for (const currentPath of gitignoreFiles) {
const dir = path.dirname(currentPath);
const filename = path.basename(currentPath);
const destPath = path.join(dir, ".gitignore");
if (authFilePath && (await fs.pathExists(authFilePath))) {
let authFileContent = await fs.readFile(authFilePath, "utf8");
if (!authFileContent.includes("@better-auth/expo")) {
const importLine = 'import { expo } from "@better-auth/expo";\n';
const lastImportIndex = authFileContent.lastIndexOf("import");
const afterLastImport =
authFileContent.indexOf("\n", lastImportIndex) + 1;
authFileContent =
authFileContent.substring(0, afterLastImport) +
importLine +
authFileContent.substring(afterLastImport);
}
if (!authFileContent.includes("plugins:")) {
authFileContent = authFileContent.replace(
/}\);/,
" plugins: [expo()],\n});",
);
} else if (!authFileContent.includes("expo()")) {
authFileContent = authFileContent.replace(
/plugins: \[(.*?)\]/s,
(match, plugins) => {
return `plugins: [${plugins}${plugins.trim() ? ", " : ""}expo()]`;
},
);
}
if (!authFileContent.includes("my-better-t-app://")) {
authFileContent = authFileContent.replace(
/trustedOrigins: \[(.*?)\]/s,
(match, origins) => {
return `trustedOrigins: [${origins}${origins.trim() ? ", " : ""}"my-better-t-app://"]`;
},
);
}
await fs.writeFile(authFilePath, authFileContent);
}
}
export async function fixGitignoreFiles(projectDir: string): Promise<void> {
const gitignorePaths = await findGitignoreFiles(projectDir);
for (const gitignorePath of gitignorePaths) {
if (await fs.pathExists(gitignorePath)) {
const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
await fs.move(gitignorePath, targetPath, { overwrite: true });
}
}
}
/**
* Find all _gitignore files in the project recursively
*/
async function findGitignoreFiles(dir: string): Promise<string[]> {
const gitignoreFiles: string[] = [];
const gitignorePath = path.join(dir, "_gitignore");
if (await fs.pathExists(gitignorePath)) {
gitignoreFiles.push(gitignorePath);
}
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== "node_modules") {
const subDirPath = path.join(dir, entry.name);
const subDirFiles = await findGitignoreFiles(subDirPath);
gitignoreFiles.push(...subDirFiles);
try {
if (filename === ".gitignore.hbs") {
await processTemplate(currentPath, destPath, context);
await fs.remove(currentPath);
} else if (filename === "_gitignore") {
await fs.move(currentPath, destPath, { overwrite: true });
}
} catch (error) {
consola.error(`Error processing gitignore file ${currentPath}:`, error);
}
} catch (error) {}
return gitignoreFiles;
}
}
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
if (database === "sqlite") return "template/with-drizzle-sqlite";
if (database === "postgres") return "template/with-drizzle-postgres";
if (database === "mysql") return "template/with-drizzle-mysql";
export async function handleExtras(
projectDir: string,
context: ProjectConfig,
): Promise<void> {
if (context.packageManager === "pnpm") {
const src = path.join(PKG_ROOT, "templates/extras/pnpm-workspace.yaml");
const dest = path.join(projectDir, "pnpm-workspace.yaml");
if (await fs.pathExists(src)) {
await fs.copy(src, dest);
} else {
consola.warn(
pc.yellow("Warning: pnpm-workspace.yaml template not found."),
);
}
}
if (orm === "prisma") {
if (database === "sqlite") return "template/with-prisma-sqlite";
if (database === "postgres") return "template/with-prisma-postgres";
if (database === "mysql") return "template/with-prisma-mysql";
if (database === "mongodb") return "template/with-prisma-mongodb";
}
return "template/base";
}
function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
if (database === "sqlite") return "with-drizzle-sqlite-lib";
if (database === "postgres") return "with-drizzle-postgres-lib";
if (database === "mysql") return "with-drizzle-mysql-lib";
}
if (orm === "prisma") {
if (database === "sqlite") return "with-prisma-sqlite-lib";
if (database === "postgres") return "with-prisma-postgres-lib";
if (database === "mysql") return "with-prisma-mysql-lib";
if (database === "mongodb") return "with-prisma-mongodb-lib";
}
throw new Error("Invalid ORM or database configuration for auth setup");
}

View File

@@ -202,29 +202,22 @@ DATABASE_URL=your_database_url
DATABASE_AUTH_TOKEN=your_auth_token`);
}
export async function setupTurso(
projectDir: string,
shouldSetupTurso: boolean,
) {
import type { ProjectConfig } from "../types";
export async function setupTurso(config: ProjectConfig): Promise<void> {
const { projectName, orm } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const isDrizzle = orm === "drizzle";
const setupSpinner = spinner();
setupSpinner.start("Setting up Turso database");
try {
if (!shouldSetupTurso) {
setupSpinner.stop("Skipping Turso setup");
await writeEnvFile(projectDir);
log.info(
pc.blue("Skipping Turso setup. Setting up empty configuration."),
);
displayManualSetupInstructions();
return;
}
const platform = os.platform();
const isMac = platform === "darwin";
const canInstallCLI = platform !== "win32";
const isLinux = platform === "linux";
const isWindows = platform === "win32";
if (!canInstallCLI) {
if (isWindows) {
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
await writeEnvFile(projectDir);