refractor: organize files

This commit is contained in:
Aman Varshney
2025-06-22 22:29:05 +05:30
parent 293dee5b46
commit 215bd2f326
14 changed files with 317 additions and 299 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
refractor files

View File

@@ -36,6 +36,8 @@ jobs:
with:
publish: bun run publish-packages
env:
MODE: "prod"
TELEMETRY: "true"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}

View File

@@ -5,7 +5,7 @@ import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { PackageManager, ProjectConfig } from "../../types";
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import {
addEnvVariablesToFile,
type EnvVariable,

View File

@@ -6,7 +6,7 @@ import fs from "fs-extra";
import pc from "picocolors";
import type { PackageManager } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import {
addEnvVariablesToFile,
type EnvVariable,

View File

@@ -5,7 +5,7 @@ import { type ExecaError, execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { PackageManager, ProjectConfig } from "../../types";
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import {
addEnvVariablesToFile,
type EnvVariable,

View File

@@ -0,0 +1,181 @@
import path from "node:path";
import { cancel, intro, log, outro } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../../constants";
import { getAddonsToAdd } from "../../prompts/addons";
import { gatherConfig } from "../../prompts/config-prompts";
import { getProjectName } from "../../prompts/project-name";
import type { AddInput, CreateInput, ProjectConfig } from "../../types";
import { trackProjectCreation } from "../../utils/analytics";
import { displayConfig } from "../../utils/display-config";
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
import {
handleDirectoryConflict,
setupProjectDirectory,
} from "../../utils/project-directory";
import { renderTitle } from "../../utils/render-title";
import { getProvidedFlags, processAndValidateFlags } from "../../validation";
import { addAddonsToProject } from "./add-addons";
import { createProject } from "./create-project";
import { detectProjectConfig } from "./detect-project-config";
export async function createProjectHandler(
input: CreateInput & { projectName?: string },
) {
const startTime = Date.now();
try {
renderTitle();
intro(pc.magenta("Creating a new Better-T Stack project"));
let currentPathInput: string;
if (input.yes && input.projectName) {
currentPathInput = input.projectName;
} else if (input.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(input.projectName);
}
const { finalPathInput, shouldClearDirectory } =
await handleDirectoryConflict(currentPathInput);
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
finalPathInput,
shouldClearDirectory,
);
const cliInput = {
...input,
projectDirectory: input.projectName,
};
const providedFlags = getProvidedFlags(cliInput);
const flagConfig = processAndValidateFlags(
cliInput,
providedFlags,
finalBaseName,
);
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
if (!input.yes && Object.keys(otherFlags).length > 0) {
log.info(pc.yellow("Using these pre-selected options:"));
log.message(displayConfig(otherFlags));
log.message("");
}
let config: ProjectConfig;
if (input.yes) {
config = {
...DEFAULT_CONFIG,
...flagConfig,
projectName: finalBaseName,
projectDir: finalResolvedPath,
relativePath: finalPathInput,
};
if (config.backend === "convex") {
log.info(
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
);
} else if (config.backend === "none") {
log.info(
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
);
}
log.info(
pc.yellow("Using default/flag options (config prompts skipped):"),
);
log.message(displayConfig(config));
log.message("");
} else {
config = await gatherConfig(
flagConfig,
finalBaseName,
finalResolvedPath,
finalPathInput,
);
}
await createProject(config);
const reproducibleCommand = generateReproducibleCommand(config);
log.success(
pc.blue(
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
),
);
await trackProjectCreation(config);
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
outro(
pc.magenta(
`Project created successfully in ${pc.bold(
elapsedTimeInSeconds,
)} seconds!`,
),
);
} catch (error) {
console.error(error);
process.exit(1);
}
}
export async function addAddonsHandler(input: AddInput): Promise<void> {
try {
if (!input.addons || input.addons.length === 0) {
const projectDir = input.projectDir || process.cwd();
const detectedConfig = await detectProjectConfig(projectDir);
if (!detectedConfig) {
cancel(
pc.red(
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
),
);
process.exit(1);
}
const addonsPrompt = await getAddonsToAdd(
detectedConfig.frontend || [],
detectedConfig.addons || [],
);
if (addonsPrompt.length === 0) {
outro(
pc.yellow(
"No addons to add or all compatible addons are already present.",
),
);
return;
}
input.addons = addonsPrompt;
}
if (!input.addons || input.addons.length === 0) {
outro(pc.yellow("No addons specified to add."));
return;
}
await addAddonsToProject({
...input,
addons: input.addons,
});
} catch (error) {
console.error(error);
process.exit(1);
}
}

View File

@@ -7,7 +7,7 @@ import type {
ProjectConfig,
Runtime,
} from "../../types";
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
import { getPackageExecutionCommand } from "../../utils/package-runner";
export function displayPostInstallInstructions(
config: ProjectConfig & { depsInstalled: boolean },

View File

@@ -4,7 +4,7 @@ import consola from "consola";
import { execa } from "execa";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
import { getPackageExecutionCommand } from "../../utils/package-runner";
export async function setupStarlight(config: ProjectConfig): Promise<void> {
const { packageManager, projectDir } = config;

View File

@@ -6,7 +6,7 @@ import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
import { getPackageExecutionCommand } from "../../utils/package-runner";
export async function setupTauri(config: ProjectConfig): Promise<void> {
const { packageManager, frontend, projectDir } = config;

View File

@@ -1,25 +1,11 @@
import path from "node:path";
import {
cancel,
intro,
isCancel,
log,
outro,
select,
spinner,
} from "@clack/prompts";
import { intro, log } from "@clack/prompts";
import { consola } from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import { createCli, trpcServer, zod as z } from "trpc-cli";
import { DEFAULT_CONFIG } from "./constants";
import { addAddonsToProject } from "./helpers/project-generation/add-addons";
import { createProject } from "./helpers/project-generation/create-project";
import { detectProjectConfig } from "./helpers/project-generation/detect-project-config";
import { getAddonsToAdd } from "./prompts/addons";
import { gatherConfig } from "./prompts/config-prompts";
import { getProjectName } from "./prompts/project-name";
import type { AddInput, CreateInput, ProjectConfig } from "./types";
import {
addAddonsHandler,
createProjectHandler,
} from "./helpers/project-generation/command-handlers";
import {
AddonsSchema,
APISchema,
@@ -33,282 +19,13 @@ import {
ProjectNameSchema,
RuntimeSchema,
} from "./types";
import { trackProjectCreation } from "./utils/analytics";
import { displayConfig } from "./utils/display-config";
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
import { openUrl } from "./utils/open-url";
import { renderTitle } from "./utils/render-title";
import { displaySponsors, fetchSponsors } from "./utils/sponsors";
import { getProvidedFlags, processAndValidateFlags } from "./validation";
const t = trpcServer.initTRPC.create();
async function handleDirectoryConflict(currentPathInput: string): Promise<{
finalPathInput: string;
shouldClearDirectory: boolean;
}> {
while (true) {
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
const dirExists = fs.pathExistsSync(resolvedPath);
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
if (!dirIsNotEmpty) {
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
}
log.warn(
`Directory "${pc.yellow(
currentPathInput,
)}" already exists and is not empty.`,
);
const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
message: "What would you like to do?",
options: [
{
value: "overwrite",
label: "Overwrite",
hint: "Empty the directory and create the project",
},
{
value: "merge",
label: "Merge",
hint: "Create project files inside, potentially overwriting conflicts",
},
{
value: "rename",
label: "Choose a different name/path",
hint: "Keep the existing directory and create a new one",
},
{ value: "cancel", label: "Cancel", hint: "Abort the process" },
],
initialValue: "rename",
});
if (isCancel(action)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
switch (action) {
case "overwrite":
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
case "merge":
log.info(
`Proceeding into existing directory "${pc.yellow(
currentPathInput,
)}". Files may be overwritten.`,
);
return {
finalPathInput: currentPathInput,
shouldClearDirectory: false,
};
case "rename": {
log.info("Please choose a different project name or path.");
const newPathInput = await getProjectName(undefined);
return await handleDirectoryConflict(newPathInput);
}
case "cancel":
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
}
}
async function setupProjectDirectory(
finalPathInput: string,
shouldClearDirectory: boolean,
): Promise<{ finalResolvedPath: string; finalBaseName: string }> {
let finalResolvedPath: string;
let finalBaseName: string;
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);
}
} else {
await fs.ensureDir(finalResolvedPath);
}
return { finalResolvedPath, finalBaseName };
}
async function createProjectHandler(
input: CreateInput & { projectName?: string },
) {
const startTime = Date.now();
try {
renderTitle();
intro(pc.magenta("Creating a new Better-T Stack project"));
let currentPathInput: string;
if (input.yes && input.projectName) {
currentPathInput = input.projectName;
} else if (input.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(input.projectName);
}
const { finalPathInput, shouldClearDirectory } =
await handleDirectoryConflict(currentPathInput);
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
finalPathInput,
shouldClearDirectory,
);
const cliInput = {
...input,
projectDirectory: input.projectName,
};
const providedFlags = getProvidedFlags(cliInput);
const flagConfig = processAndValidateFlags(
cliInput,
providedFlags,
finalBaseName,
);
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
if (!input.yes && Object.keys(otherFlags).length > 0) {
log.info(pc.yellow("Using these pre-selected options:"));
log.message(displayConfig(otherFlags));
log.message("");
}
let config: ProjectConfig;
if (input.yes) {
config = {
...DEFAULT_CONFIG,
...flagConfig,
projectName: finalBaseName,
projectDir: finalResolvedPath,
relativePath: finalPathInput,
};
if (config.backend === "convex") {
log.info(
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
);
} else if (config.backend === "none") {
log.info(
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
);
}
log.info(
pc.yellow("Using default/flag options (config prompts skipped):"),
);
log.message(displayConfig(config));
log.message("");
} else {
config = await gatherConfig(
flagConfig,
finalBaseName,
finalResolvedPath,
finalPathInput,
);
}
await createProject(config);
const reproducibleCommand = generateReproducibleCommand(config);
log.success(
pc.blue(
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
),
);
await trackProjectCreation(config);
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
outro(
pc.magenta(
`Project created successfully in ${pc.bold(
elapsedTimeInSeconds,
)} seconds!`,
),
);
} catch (error) {
console.error(error);
process.exit(1);
}
}
async function addAddonsHandler(input: AddInput): Promise<void> {
try {
if (!input.addons || input.addons.length === 0) {
const projectDir = input.projectDir || process.cwd();
const detectedConfig = await detectProjectConfig(projectDir);
if (!detectedConfig) {
cancel(
pc.red(
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
),
);
process.exit(1);
}
const addonsPrompt = await getAddonsToAdd(
detectedConfig.frontend || [],
detectedConfig.addons || [],
);
if (addonsPrompt.length === 0) {
outro(
pc.yellow(
"No addons to add or all compatible addons are already present.",
),
);
return;
}
input.addons = addonsPrompt;
}
if (!input.addons || input.addons.length === 0) {
outro(pc.yellow("No addons specified to add."));
return;
}
await addAddonsToProject({
...input,
addons: input.addons,
});
} catch (error) {
console.error(error);
process.exit(1);
}
}
const router = t.router({
init: t.procedure
.meta({

View File

@@ -14,7 +14,7 @@ export async function trackProjectCreation(
flushInterval: 0,
privacyMode: true,
disableGeoip: true,
disabled: process.env.MODE !== "prod",
disabled: process.env.TELEMETRY !== "true",
});
try {

View File

@@ -0,0 +1,113 @@
import path from "node:path";
import { cancel, isCancel, log, select, spinner } from "@clack/prompts";
import { consola } from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import { getProjectName } from "../prompts/project-name";
export async function handleDirectoryConflict(
currentPathInput: string,
): Promise<{
finalPathInput: string;
shouldClearDirectory: boolean;
}> {
while (true) {
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
const dirExists = fs.pathExistsSync(resolvedPath);
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
if (!dirIsNotEmpty) {
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
}
log.warn(
`Directory "${pc.yellow(
currentPathInput,
)}" already exists and is not empty.`,
);
const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
message: "What would you like to do?",
options: [
{
value: "overwrite",
label: "Overwrite",
hint: "Empty the directory and create the project",
},
{
value: "merge",
label: "Merge",
hint: "Create project files inside, potentially overwriting conflicts",
},
{
value: "rename",
label: "Choose a different name/path",
hint: "Keep the existing directory and create a new one",
},
{ value: "cancel", label: "Cancel", hint: "Abort the process" },
],
initialValue: "rename",
});
if (isCancel(action)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
switch (action) {
case "overwrite":
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
case "merge":
log.info(
`Proceeding into existing directory "${pc.yellow(
currentPathInput,
)}". Files may be overwritten.`,
);
return {
finalPathInput: currentPathInput,
shouldClearDirectory: false,
};
case "rename": {
log.info("Please choose a different project name or path.");
const newPathInput = await getProjectName(undefined);
return await handleDirectoryConflict(newPathInput);
}
case "cancel":
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
}
}
export async function setupProjectDirectory(
finalPathInput: string,
shouldClearDirectory: boolean,
): Promise<{ finalResolvedPath: string; finalBaseName: string }> {
let finalResolvedPath: string;
let finalBaseName: string;
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);
}
} else {
await fs.ensureDir(finalResolvedPath);
}
return { finalResolvedPath, finalBaseName };
}

View File

@@ -10,8 +10,8 @@ export default defineConfig({
banner: "#!/usr/bin/env node",
},
env: {
POSTHOG_API_KEY: "phc_8ZUxEwwfKMajJLvxz1daGd931dYbQrwKNficBmsdIrs",
POSTHOG_HOST: "https://us.i.posthog.com",
MODE: process.env.MODE || "dev", // wierd trick i know
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY || "lol",
POSTHOG_HOST: process.env.POSTHOG_HOST || "lool",
TELEMETRY: process.env.TELEMETRY || "false", // wierd trick i know
},
});