mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
Prompt to overwrite non-empty dirs before config
This commit is contained in:
5
.changeset/fluffy-bears-pick.md
Normal file
5
.changeset/fluffy-bears-pick.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Prompt to overwrite non-empty dirs before config
|
||||||
@@ -9,6 +9,8 @@ export const PKG_ROOT = path.join(distPath, "../");
|
|||||||
|
|
||||||
export const DEFAULT_CONFIG: ProjectConfig = {
|
export const DEFAULT_CONFIG: ProjectConfig = {
|
||||||
projectName: "my-better-t-app",
|
projectName: "my-better-t-app",
|
||||||
|
projectDir: path.resolve(process.cwd(), "my-better-t-app"),
|
||||||
|
relativePath: "my-better-t-app",
|
||||||
frontend: ["tanstack-router"],
|
frontend: ["tanstack-router"],
|
||||||
database: "sqlite",
|
database: "sqlite",
|
||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import { setupTauri } from "./tauri-setup";
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupAddons(config: ProjectConfig) {
|
export async function setupAddons(config: ProjectConfig) {
|
||||||
const { projectName, addons, frontend } = config;
|
const { projectName, addons, frontend, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const hasReactWebFrontend =
|
const hasReactWebFrontend =
|
||||||
frontend.includes("react-router") || frontend.includes("tanstack-router");
|
frontend.includes("react-router") || frontend.includes("tanstack-router");
|
||||||
const hasNuxtFrontend = frontend.includes("nuxt");
|
const hasNuxtFrontend = frontend.includes("nuxt");
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type { ProjectConfig, ProjectFrontend } from "../types";
|
|||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupApi(config: ProjectConfig): Promise<void> {
|
export async function setupApi(config: ProjectConfig): Promise<void> {
|
||||||
const { api, projectName, frontend, backend, packageManager } = config;
|
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
config;
|
||||||
const isConvex = backend === "convex";
|
const isConvex = backend === "convex";
|
||||||
const webDir = path.join(projectDir, "apps/web");
|
const webDir = path.join(projectDir, "apps/web");
|
||||||
const nativeDir = path.join(projectDir, "apps/native");
|
const nativeDir = path.join(projectDir, "apps/native");
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import type { ProjectConfig } from "../types";
|
|||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupAuth(config: ProjectConfig): Promise<void> {
|
export async function setupAuth(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, auth, frontend, backend } = config;
|
const { projectName, auth, frontend, backend, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex" || !auth) {
|
if (backend === "convex" || !auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const clientDir = path.join(projectDir, "apps/web");
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
const nativeDir = path.join(projectDir, "apps/native");
|
const nativeDir = path.join(projectDir, "apps/native");
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import type { ProjectConfig } from "../types";
|
|||||||
export async function setupBackendDependencies(
|
export async function setupBackendDependencies(
|
||||||
config: ProjectConfig,
|
config: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { projectName, backend, runtime, api } = config;
|
const { projectName, backend, runtime, api, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex") {
|
if (backend === "convex") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const framework = backend;
|
const framework = backend;
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import path from "node:path";
|
import { cancel, log } from "@clack/prompts";
|
||||||
import { cancel, log, spinner } from "@clack/prompts";
|
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
@@ -27,7 +26,7 @@ import {
|
|||||||
} from "./template-manager";
|
} from "./template-manager";
|
||||||
|
|
||||||
export async function createProject(options: ProjectConfig) {
|
export async function createProject(options: ProjectConfig) {
|
||||||
const projectDir = path.resolve(process.cwd(), options.projectName);
|
const projectDir = options.projectDir;
|
||||||
const isConvex = options.backend === "convex";
|
const isConvex = options.backend === "convex";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import { setupNeonPostgres } from "./neon-setup";
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, database, orm, dbSetup, backend } = config;
|
const { projectName, database, orm, dbSetup, backend, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex" || database === "none") {
|
if (backend === "convex" || database === "none") {
|
||||||
if (backend !== "convex") {
|
if (backend !== "convex") {
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const serverDbDir = path.join(serverDir, "src/db");
|
const serverDbDir = path.join(serverDir, "src/db");
|
||||||
if (await fs.pathExists(serverDbDir)) {
|
if (await fs.pathExists(serverDbDir)) {
|
||||||
@@ -27,7 +26,6 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ export async function setupEnvironmentVariables(
|
|||||||
auth,
|
auth,
|
||||||
examples,
|
examples,
|
||||||
dbSetup,
|
dbSetup,
|
||||||
|
projectDir,
|
||||||
} = config;
|
} = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
|
|
||||||
const hasReactRouter = frontend.includes("react-router");
|
const hasReactRouter = frontend.includes("react-router");
|
||||||
const hasTanStackRouter = frontend.includes("tanstack-router");
|
const hasTanStackRouter = frontend.includes("tanstack-router");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ProjectConfig } from "../types";
|
|||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, examples, frontend, backend } = config;
|
const { projectName, examples, frontend, backend, projectDir } = config;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
backend === "convex" ||
|
backend === "convex" ||
|
||||||
@@ -16,8 +16,6 @@ export async function setupExamples(config: ProjectConfig): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
|
|
||||||
if (examples.includes("ai")) {
|
if (examples.includes("ai")) {
|
||||||
const clientDir = path.join(projectDir, "apps/web");
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|||||||
@@ -130,8 +130,7 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setupMongoDBAtlas(config: ProjectConfig) {
|
export async function setupMongoDBAtlas(config: ProjectConfig) {
|
||||||
const { projectName } = config;
|
const { projectName, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const mainSpinner = spinner();
|
const mainSpinner = spinner();
|
||||||
mainSpinner.start("Setting up MongoDB Atlas");
|
mainSpinner.start("Setting up MongoDB Atlas");
|
||||||
|
|
||||||
|
|||||||
@@ -128,8 +128,7 @@ DATABASE_URL="your_connection_string"`);
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
|
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, packageManager } = config;
|
const { projectName, packageManager, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const setupSpinner = spinner();
|
const setupSpinner = spinner();
|
||||||
setupSpinner.start("Setting up Neon PostgreSQL");
|
setupSpinner.start("Setting up Neon PostgreSQL");
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function displayPostInstallInstructions(
|
|||||||
const {
|
const {
|
||||||
database,
|
database,
|
||||||
projectName,
|
projectName,
|
||||||
|
relativePath,
|
||||||
packageManager,
|
packageManager,
|
||||||
depsInstalled,
|
depsInstalled,
|
||||||
orm,
|
orm,
|
||||||
@@ -27,7 +28,7 @@ export function displayPostInstallInstructions(
|
|||||||
|
|
||||||
const isConvex = backend === "convex";
|
const isConvex = backend === "convex";
|
||||||
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
||||||
const cdCmd = `cd ${projectName}`;
|
const cdCmd = `cd ${relativePath}`;
|
||||||
const hasHuskyOrBiome =
|
const hasHuskyOrBiome =
|
||||||
addons?.includes("husky") || addons?.includes("biome");
|
addons?.includes("husky") || addons?.includes("biome");
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export function displayPostInstallInstructions(
|
|||||||
!isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
|
!isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
|
||||||
|
|
||||||
const hasReactRouter = frontend?.includes("react-router");
|
const hasReactRouter = frontend?.includes("react-router");
|
||||||
const hasSvelte = frontend?.includes("svelte"); // Keep separate for port logic
|
const hasSvelte = frontend?.includes("svelte");
|
||||||
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
|
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
|
||||||
|
|
||||||
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
|
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
|
||||||
|
|||||||
@@ -154,8 +154,7 @@ export default prisma;
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupPrismaPostgres(config: ProjectConfig) {
|
export async function setupPrismaPostgres(config: ProjectConfig) {
|
||||||
const { projectName, packageManager } = config;
|
const { projectName, packageManager, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
s.start("Setting up Prisma PostgreSQL");
|
s.start("Setting up Prisma PostgreSQL");
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import type { ProjectBackend, ProjectConfig } from "../types";
|
|||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, runtime, backend } = config;
|
const { projectName, runtime, backend, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex" || backend === "next" || runtime === "none") {
|
if (backend === "convex" || backend === "next" || runtime === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
if (!(await fs.pathExists(serverDir))) {
|
if (!(await fs.pathExists(serverDir))) {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import type { ProjectConfig } from "../types";
|
|||||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||||
|
|
||||||
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, packageManager } = config;
|
const { projectName, packageManager, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import { getPackageExecutionCommand } from "../utils/get-package-execution-comma
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, packageManager, frontend } = config;
|
const { projectName, packageManager, frontend, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
|
|||||||
@@ -198,8 +198,7 @@ DATABASE_AUTH_TOKEN=your_auth_token`);
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupTurso(config: ProjectConfig): Promise<void> {
|
export async function setupTurso(config: ProjectConfig): Promise<void> {
|
||||||
const { projectName, orm } = config;
|
const { projectName, orm, projectDir } = config;
|
||||||
const projectDir = path.resolve(process.cwd(), projectName);
|
|
||||||
const isDrizzle = orm === "drizzle";
|
const isDrizzle = orm === "drizzle";
|
||||||
const setupSpinner = spinner();
|
const setupSpinner = spinner();
|
||||||
setupSpinner.start("Setting up Turso database");
|
setupSpinner.start("Setting up Turso database");
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { cancel, intro, log, outro } from "@clack/prompts";
|
import {
|
||||||
|
cancel,
|
||||||
|
confirm,
|
||||||
|
intro,
|
||||||
|
isCancel,
|
||||||
|
log,
|
||||||
|
outro,
|
||||||
|
spinner,
|
||||||
|
} from "@clack/prompts";
|
||||||
import { consola } from "consola";
|
import { consola } from "consola";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
@@ -147,17 +155,96 @@ async function main() {
|
|||||||
.parse();
|
.parse();
|
||||||
|
|
||||||
const options = argv as YargsArgv;
|
const options = argv as YargsArgv;
|
||||||
const projectDirectory = options.projectDirectory;
|
const cliProjectNameArg = options.projectDirectory;
|
||||||
|
|
||||||
renderTitle();
|
renderTitle();
|
||||||
|
|
||||||
const flagConfig = processAndValidateFlags(options, projectDirectory);
|
|
||||||
|
|
||||||
intro(pc.magenta("Creating a new Better-T-Stack project"));
|
intro(pc.magenta("Creating a new Better-T-Stack project"));
|
||||||
|
|
||||||
if (!options.yes && Object.keys(flagConfig).length > 0) {
|
let currentPathInput: string;
|
||||||
|
let finalPathInput: string;
|
||||||
|
let finalResolvedPath: string;
|
||||||
|
let finalBaseName: string;
|
||||||
|
let shouldClearDirectory = false;
|
||||||
|
|
||||||
|
if (options.yes && cliProjectNameArg) {
|
||||||
|
currentPathInput = cliProjectNameArg;
|
||||||
|
} else if (options.yes) {
|
||||||
|
let defaultName = DEFAULT_CONFIG.relativePath;
|
||||||
|
let counter = 1;
|
||||||
|
while (
|
||||||
|
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||||
|
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||||
|
) {
|
||||||
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
currentPathInput = defaultName;
|
||||||
|
} else {
|
||||||
|
currentPathInput = await getProjectName(cliProjectNameArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
||||||
|
const dirExists = fs.pathExistsSync(resolvedPath);
|
||||||
|
const dirIsNotEmpty =
|
||||||
|
dirExists && fs.readdirSync(resolvedPath).length > 0;
|
||||||
|
|
||||||
|
if (!dirIsNotEmpty) {
|
||||||
|
finalPathInput = currentPathInput;
|
||||||
|
shouldClearDirectory = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldOverwrite = await confirm({
|
||||||
|
message: `Directory "${pc.yellow(
|
||||||
|
currentPathInput,
|
||||||
|
)}" already exists and is not empty. Overwrite and replace all existing files?`,
|
||||||
|
initialValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(shouldOverwrite)) {
|
||||||
|
cancel(pc.red("Operation cancelled."));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldOverwrite) {
|
||||||
|
finalPathInput = currentPathInput;
|
||||||
|
shouldClearDirectory = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Please choose a different project name or path.");
|
||||||
|
currentPathInput = await getProjectName(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalPathInput === ".") {
|
||||||
|
finalResolvedPath = process.cwd();
|
||||||
|
finalBaseName = path.basename(finalResolvedPath);
|
||||||
|
} else {
|
||||||
|
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
||||||
|
finalBaseName = path.basename(finalResolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldClearDirectory) {
|
||||||
|
const s = spinner();
|
||||||
|
s.start(`Clearing directory "${finalResolvedPath}"...`);
|
||||||
|
try {
|
||||||
|
await fs.emptyDir(finalResolvedPath);
|
||||||
|
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
||||||
|
} catch (error) {
|
||||||
|
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
||||||
|
consola.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagConfig = processAndValidateFlags(options, finalBaseName);
|
||||||
|
|
||||||
|
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||||
|
|
||||||
|
if (!options.yes && Object.keys(otherFlags).length > 0) {
|
||||||
log.info(pc.yellow("Using these pre-selected options:"));
|
log.info(pc.yellow("Using these pre-selected options:"));
|
||||||
log.message(displayConfig(flagConfig));
|
log.message(displayConfig(otherFlags));
|
||||||
log.message("");
|
log.message("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +252,10 @@ async function main() {
|
|||||||
if (options.yes) {
|
if (options.yes) {
|
||||||
config = {
|
config = {
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
projectName: projectDirectory ?? DEFAULT_CONFIG.projectName,
|
|
||||||
...flagConfig,
|
...flagConfig,
|
||||||
|
projectName: finalBaseName,
|
||||||
|
projectDir: finalResolvedPath,
|
||||||
|
relativePath: finalPathInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.backend === "convex") {
|
if (config.backend === "convex") {
|
||||||
@@ -176,27 +265,25 @@ async function main() {
|
|||||||
config.api = "none";
|
config.api = "none";
|
||||||
config.runtime = "none";
|
config.runtime = "none";
|
||||||
config.dbSetup = "none";
|
config.dbSetup = "none";
|
||||||
|
config.examples = ["todo"];
|
||||||
} else if (config.database === "none") {
|
} else if (config.database === "none") {
|
||||||
config.orm = "none";
|
config.orm = "none";
|
||||||
config.auth = false;
|
config.auth = false;
|
||||||
config.dbSetup = "none";
|
config.dbSetup = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(pc.yellow("Using these default/flag options:"));
|
log.info(
|
||||||
|
pc.yellow("Using default/flag options (config prompts skipped):"),
|
||||||
|
);
|
||||||
log.message(displayConfig(config));
|
log.message(displayConfig(config));
|
||||||
log.message("");
|
log.message("");
|
||||||
} else {
|
} else {
|
||||||
config = await gatherConfig(flagConfig);
|
config = await gatherConfig(
|
||||||
}
|
flagConfig,
|
||||||
|
finalBaseName,
|
||||||
const projectDir = path.resolve(process.cwd(), config.projectName);
|
finalResolvedPath,
|
||||||
|
finalPathInput,
|
||||||
if (
|
);
|
||||||
fs.pathExistsSync(projectDir) &&
|
|
||||||
fs.readdirSync(projectDir).length > 0
|
|
||||||
) {
|
|
||||||
const newProjectName = await getProjectName();
|
|
||||||
config.projectName = newProjectName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await createProject(config);
|
await createProject(config);
|
||||||
@@ -238,7 +325,7 @@ async function main() {
|
|||||||
|
|
||||||
function processAndValidateFlags(
|
function processAndValidateFlags(
|
||||||
options: YargsArgv,
|
options: YargsArgv,
|
||||||
projectDirectory?: string,
|
projectName?: string,
|
||||||
): Partial<ProjectConfig> {
|
): Partial<ProjectConfig> {
|
||||||
const config: Partial<ProjectConfig> = {};
|
const config: Partial<ProjectConfig> = {};
|
||||||
const providedFlags: Set<string> = new Set(
|
const providedFlags: Set<string> = new Set(
|
||||||
@@ -305,8 +392,13 @@ function processAndValidateFlags(
|
|||||||
if (options.packageManager) {
|
if (options.packageManager) {
|
||||||
config.packageManager = options.packageManager as ProjectPackageManager;
|
config.packageManager = options.packageManager as ProjectPackageManager;
|
||||||
}
|
}
|
||||||
if (projectDirectory) {
|
|
||||||
config.projectName = projectDirectory;
|
if (projectName) {
|
||||||
|
config.projectName = projectName;
|
||||||
|
} else if (options.projectDirectory) {
|
||||||
|
config.projectName = path.basename(
|
||||||
|
path.resolve(process.cwd(), options.projectDirectory),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.frontend && options.frontend.length > 0) {
|
if (options.frontend && options.frontend.length > 0) {
|
||||||
@@ -363,7 +455,7 @@ function processAndValidateFlags(
|
|||||||
config.examples = options.examples.filter(
|
config.examples = options.examples.filter(
|
||||||
(ex): ex is ProjectExamples => ex !== "none",
|
(ex): ex is ProjectExamples => ex !== "none",
|
||||||
);
|
);
|
||||||
if (config.backend !== "convex" && options.examples.includes("none")) {
|
if (options.examples.includes("none") && config.backend !== "convex") {
|
||||||
config.examples = [];
|
config.examples = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,15 +476,12 @@ function processAndValidateFlags(
|
|||||||
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
||||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||||
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
||||||
if (providedFlags.has("examples")) {
|
|
||||||
incompatibleFlags.push("--examples");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (incompatibleFlags.length > 0) {
|
if (incompatibleFlags.length > 0) {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
|
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
|
||||||
", ",
|
", ",
|
||||||
)}. Please remove them. The 'todo' example is included automatically with Convex.`,
|
)}. Please remove them.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -463,6 +552,12 @@ function processAndValidateFlags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.orm === "mongoose" && !providedFlags.has("database")) {
|
if (config.orm === "mongoose" && !providedFlags.has("database")) {
|
||||||
|
if (effectiveDatabase && effectiveDatabase !== "mongodb") {
|
||||||
|
consola.fatal(
|
||||||
|
`Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
config.database = "mongodb";
|
config.database = "mongodb";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,7 +659,7 @@ function processAndValidateFlags(
|
|||||||
if (
|
if (
|
||||||
(includesNuxt || includesSvelte || includesSolid) &&
|
(includesNuxt || includesSvelte || includesSolid) &&
|
||||||
effectiveApi !== "orpc" &&
|
effectiveApi !== "orpc" &&
|
||||||
(!options.api || (options.yes && options.api !== "trpc"))
|
(!options.api || (options.yes && options.api === "trpc"))
|
||||||
) {
|
) {
|
||||||
if (config.api !== "none") {
|
if (config.api !== "none") {
|
||||||
config.api = "orpc";
|
config.api = "orpc";
|
||||||
@@ -576,32 +671,43 @@ function processAndValidateFlags(
|
|||||||
const hasWebSpecificAddons = config.addons.some((addon) =>
|
const hasWebSpecificAddons = config.addons.some((addon) =>
|
||||||
webSpecificAddons.includes(addon),
|
webSpecificAddons.includes(addon),
|
||||||
);
|
);
|
||||||
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => {
|
||||||
(f) =>
|
const isPwaCompatible =
|
||||||
|
f === "tanstack-router" || f === "react-router" || f === "solid";
|
||||||
|
const isTauriCompatible =
|
||||||
f === "tanstack-router" ||
|
f === "tanstack-router" ||
|
||||||
f === "react-router" ||
|
f === "react-router" ||
|
||||||
f === "solid" ||
|
f === "nuxt" ||
|
||||||
(f === "nuxt" &&
|
f === "svelte" ||
|
||||||
config.addons?.includes("tauri") &&
|
f === "solid";
|
||||||
!config.addons?.includes("pwa")) ||
|
|
||||||
(f === "svelte" &&
|
if (
|
||||||
config.addons?.includes("tauri") &&
|
config.addons?.includes("pwa") &&
|
||||||
!config.addons?.includes("pwa")),
|
config.addons?.includes("tauri")
|
||||||
);
|
) {
|
||||||
|
return isPwaCompatible && isTauriCompatible;
|
||||||
|
}
|
||||||
|
if (config.addons?.includes("pwa")) {
|
||||||
|
return isPwaCompatible;
|
||||||
|
}
|
||||||
|
if (config.addons?.includes("tauri")) {
|
||||||
|
return isTauriCompatible;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||||
let incompatibleAddon = "";
|
let incompatibleReason = "Selected frontend is not compatible.";
|
||||||
if (config.addons.includes("pwa") && includesNuxt) {
|
if (config.addons.includes("pwa")) {
|
||||||
incompatibleAddon = "PWA addon is not compatible with Nuxt.";
|
incompatibleReason =
|
||||||
} else if (
|
"PWA requires tanstack-router, react-router, or solid.";
|
||||||
config.addons.includes("pwa") ||
|
}
|
||||||
config.addons.includes("tauri")
|
if (config.addons.includes("tauri")) {
|
||||||
) {
|
incompatibleReason =
|
||||||
incompatibleAddon =
|
"Tauri requires tanstack-router, react-router, nuxt, svelte, or solid.";
|
||||||
"PWA requires tanstack-router/react-router/solid. Tauri requires tanstack-router/react-router/Nuxt/Svelte/Solid.";
|
|
||||||
}
|
}
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
`${incompatibleAddon} Cannot use these addons with your frontend selection.`,
|
`Incompatible addon/frontend combination: ${incompatibleReason}`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -671,7 +777,12 @@ main().catch((err) => {
|
|||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
if (
|
if (
|
||||||
!err.message.includes("is only supported with") &&
|
!err.message.includes("is only supported with") &&
|
||||||
!err.message.includes("incompatible with")
|
!err.message.includes("incompatible with") &&
|
||||||
|
!err.message.includes("requires") &&
|
||||||
|
!err.message.includes("Cannot use") &&
|
||||||
|
!err.message.includes("Cannot select multiple") &&
|
||||||
|
!err.message.includes("Cannot combine") &&
|
||||||
|
!err.message.includes("not supported")
|
||||||
) {
|
) {
|
||||||
consola.error(err.message);
|
consola.error(err.message);
|
||||||
consola.error(err.stack);
|
consola.error(err.stack);
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ import { getGitChoice } from "./git";
|
|||||||
import { getinstallChoice } from "./install";
|
import { getinstallChoice } from "./install";
|
||||||
import { getORMChoice } from "./orm";
|
import { getORMChoice } from "./orm";
|
||||||
import { getPackageManagerChoice } from "./package-manager";
|
import { getPackageManagerChoice } from "./package-manager";
|
||||||
import { getProjectName } from "./project-name";
|
|
||||||
import { getRuntimeChoice } from "./runtime";
|
import { getRuntimeChoice } from "./runtime";
|
||||||
|
|
||||||
type PromptGroupResults = {
|
type PromptGroupResults = {
|
||||||
projectName: string;
|
|
||||||
frontend: ProjectFrontend[];
|
frontend: ProjectFrontend[];
|
||||||
backend: ProjectBackend;
|
backend: ProjectBackend;
|
||||||
runtime: ProjectRuntime;
|
runtime: ProjectRuntime;
|
||||||
@@ -47,12 +45,12 @@ type PromptGroupResults = {
|
|||||||
|
|
||||||
export async function gatherConfig(
|
export async function gatherConfig(
|
||||||
flags: Partial<ProjectConfig>,
|
flags: Partial<ProjectConfig>,
|
||||||
|
projectName: string,
|
||||||
|
projectDir: string,
|
||||||
|
relativePath: string,
|
||||||
): Promise<ProjectConfig> {
|
): Promise<ProjectConfig> {
|
||||||
const result = await group<PromptGroupResults>(
|
const result = await group<PromptGroupResults>(
|
||||||
{
|
{
|
||||||
projectName: async () => {
|
|
||||||
return getProjectName(flags.projectName);
|
|
||||||
},
|
|
||||||
frontend: ({ results }) =>
|
frontend: ({ results }) =>
|
||||||
getFrontendChoice(flags.frontend, flags.backend),
|
getFrontendChoice(flags.frontend, flags.backend),
|
||||||
backend: ({ results }) =>
|
backend: ({ results }) =>
|
||||||
@@ -109,7 +107,9 @@ export async function gatherConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectName: result.projectName,
|
projectName: projectName,
|
||||||
|
projectDir: projectDir,
|
||||||
|
relativePath: relativePath,
|
||||||
frontend: result.frontend,
|
frontend: result.frontend,
|
||||||
backend: result.backend,
|
backend: result.backend,
|
||||||
runtime: result.runtime,
|
runtime: result.runtime,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"];
|
|||||||
const MAX_LENGTH = 255;
|
const MAX_LENGTH = 255;
|
||||||
|
|
||||||
function validateDirectoryName(name: string): string | undefined {
|
function validateDirectoryName(name: string): string | undefined {
|
||||||
// Allow "." as it represents current directory
|
|
||||||
if (name === ".") return undefined;
|
if (name === ".") return undefined;
|
||||||
|
|
||||||
if (!name) return "Project name cannot be empty";
|
if (!name) return "Project name cannot be empty";
|
||||||
@@ -30,22 +29,12 @@ function validateDirectoryName(name: string): string | undefined {
|
|||||||
export async function getProjectName(initialName?: string): Promise<string> {
|
export async function getProjectName(initialName?: string): Promise<string> {
|
||||||
if (initialName) {
|
if (initialName) {
|
||||||
if (initialName === ".") {
|
if (initialName === ".") {
|
||||||
const projectDir = process.cwd();
|
return initialName;
|
||||||
if (fs.readdirSync(projectDir).length === 0) {
|
}
|
||||||
return initialName;
|
const finalDirName = path.basename(initialName);
|
||||||
}
|
const validationError = validateDirectoryName(finalDirName);
|
||||||
} else {
|
if (!validationError) {
|
||||||
const finalDirName = path.basename(initialName);
|
return initialName;
|
||||||
const validationError = validateDirectoryName(finalDirName);
|
|
||||||
if (!validationError) {
|
|
||||||
const projectDir = path.resolve(process.cwd(), initialName);
|
|
||||||
if (
|
|
||||||
!fs.pathExistsSync(projectDir) ||
|
|
||||||
fs.readdirSync(projectDir).length === 0
|
|
||||||
) {
|
|
||||||
return initialName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +43,10 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
|||||||
let defaultName = DEFAULT_CONFIG.projectName;
|
let defaultName = DEFAULT_CONFIG.projectName;
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
|
|
||||||
while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName))) {
|
while (
|
||||||
|
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||||
|
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||||
|
) {
|
||||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
@@ -69,33 +61,17 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
|||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
const nameToUse = value.trim() || defaultName;
|
const nameToUse = value.trim() || defaultName;
|
||||||
|
|
||||||
if (nameToUse === ".") {
|
const finalDirName = path.basename(nameToUse);
|
||||||
const dirContents = fs.readdirSync(process.cwd());
|
|
||||||
if (dirContents.length > 0) {
|
|
||||||
return "Current directory is not empty. Please choose a different directory.";
|
|
||||||
}
|
|
||||||
isValid = true;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectDir = path.resolve(process.cwd(), nameToUse);
|
|
||||||
const finalDirName = path.basename(projectDir);
|
|
||||||
|
|
||||||
const validationError = validateDirectoryName(finalDirName);
|
const validationError = validateDirectoryName(finalDirName);
|
||||||
if (validationError) return validationError;
|
if (validationError) return validationError;
|
||||||
|
|
||||||
if (!projectDir.startsWith(process.cwd())) {
|
if (nameToUse !== ".") {
|
||||||
return "Project path must be within current directory";
|
const projectDir = path.resolve(process.cwd(), nameToUse);
|
||||||
}
|
if (!projectDir.startsWith(process.cwd())) {
|
||||||
|
return "Project path must be within current directory";
|
||||||
if (fs.pathExistsSync(projectDir)) {
|
|
||||||
const dirContents = fs.readdirSync(projectDir);
|
|
||||||
if (dirContents.length > 0) {
|
|
||||||
return `Directory "${nameToUse}" already exists and is not empty. Please choose a different name or path.`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid = true;
|
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -106,6 +82,7 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
projectPath = response || defaultName;
|
projectPath = response || defaultName;
|
||||||
|
isValid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectPath;
|
return projectPath;
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export type ProjectApi = "trpc" | "orpc" | "none";
|
|||||||
|
|
||||||
export interface ProjectConfig {
|
export interface ProjectConfig {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
projectDir: string;
|
||||||
|
relativePath: string;
|
||||||
backend: ProjectBackend;
|
backend: ProjectBackend;
|
||||||
runtime: ProjectRuntime;
|
runtime: ProjectRuntime;
|
||||||
database: ProjectDatabase;
|
database: ProjectDatabase;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
|||||||
baseCommand = "bun create better-t-stack@latest";
|
baseCommand = "bun create better-t-stack@latest";
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectName = config.projectName ? ` ${config.projectName}` : "";
|
const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
|
||||||
|
|
||||||
return `${baseCommand}${projectName} ${flags.join(" ")}`;
|
return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const auth = betterAuth({
|
|||||||
database: "", // Invalid configuration
|
database: "", // Invalid configuration
|
||||||
trustedOrigins: [
|
trustedOrigins: [
|
||||||
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
|
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
|
||||||
"my-better-t-app://", // Use hardcoded scheme{{/if}}
|
"my-better-t-app://",{{/if}}
|
||||||
],
|
],
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ export default function SignInForm({
|
|||||||
type="email"
|
type="email"
|
||||||
value={field().state.value}
|
value={field().state.value}
|
||||||
onBlur={field().handleBlur}
|
onBlur={field().handleBlur}
|
||||||
onInput={(e) => field().handleChange(e.currentTarget.value)} // Use onInput and currentTarget
|
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
class="w-full rounded border p-2" // Example basic styling
|
class="w-full rounded border p-2"
|
||||||
/>
|
/>
|
||||||
<For each={field().state.meta.errors}>
|
<For each={field().state.meta.errors}>
|
||||||
{(error) => (
|
{(error) => (
|
||||||
@@ -122,7 +122,7 @@ export default function SignInForm({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSwitchToSignUp}
|
onClick={onSwitchToSignUp}
|
||||||
class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline" // Example basic styling
|
class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline"
|
||||||
>
|
>
|
||||||
Need an account? Sign Up
|
Need an account? Sign Up
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ app.use(
|
|||||||
app.all("/api/auth{/*path}", toNodeHandler(auth));
|
app.all("/api/auth{/*path}", toNodeHandler(auth));
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
app.use(express.json())
|
|
||||||
|
|
||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
app.use(
|
app.use(
|
||||||
"/trpc",
|
"/trpc",
|
||||||
@@ -67,6 +65,8 @@ app.use('/rpc{*path}', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
{{#if (includes examples "ai")}}
|
{{#if (includes examples "ai")}}
|
||||||
app.post("/ai", async (req, res) => {
|
app.post("/ai", async (req, res) => {
|
||||||
const { messages = [] } = req.body || {};
|
const { messages = [] } = req.body || {};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const MAX_VISIBLE_PAGES = 5;
|
|||||||
|
|
||||||
export default function Testimonials() {
|
export default function Testimonials() {
|
||||||
const [startIndex, setStartIndex] = useState(0);
|
const [startIndex, setStartIndex] = useState(0);
|
||||||
const [tweetsPerPage] = useState(6); // Show 6 tweets per page
|
const [tweetsPerPage] = useState(6);
|
||||||
|
|
||||||
const totalPages = useMemo(
|
const totalPages = useMemo(
|
||||||
() => Math.ceil(TWEET_IDS.length / tweetsPerPage),
|
() => Math.ceil(TWEET_IDS.length / tweetsPerPage),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"; // Make sure you have this utility
|
import { cn } from "@/lib/utils";
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
import { Moon, Sun } from "lucide-react";
|
import { Moon, Sun } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|||||||
Reference in New Issue
Block a user