mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add orpc
This commit is contained in:
@@ -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);
|
||||
|
||||
37
apps/cli/src/helpers/api-setup.ts
Normal file
37
apps/cli/src/helpers/api-setup.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}`,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user