mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
96
apps/cli/src/helpers/core/add-addons.ts
Normal file
96
apps/cli/src/helpers/core/add-addons.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { AddInput, Addons, ProjectConfig } from "../../types";
|
||||
import { validateAddonCompatibility } from "../../utils/addon-compatibility";
|
||||
import { updateBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupAddons } from "../addons/addons-setup";
|
||||
import {
|
||||
detectProjectConfig,
|
||||
isBetterTStackProject,
|
||||
} from "./detect-project-config";
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
import { setupAddonsTemplate } from "./template-manager";
|
||||
|
||||
export async function addAddonsToProject(
|
||||
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
|
||||
) {
|
||||
try {
|
||||
const projectDir = input.projectDir || process.cwd();
|
||||
|
||||
const isBetterTStack = await isBetterTStackProject(projectDir);
|
||||
if (!isBetterTStack) {
|
||||
exitWithError(
|
||||
"This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.",
|
||||
);
|
||||
}
|
||||
|
||||
const detectedConfig = await detectProjectConfig(projectDir);
|
||||
if (!detectedConfig) {
|
||||
exitWithError(
|
||||
"Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.",
|
||||
);
|
||||
}
|
||||
|
||||
const config: ProjectConfig = {
|
||||
projectName: detectedConfig.projectName || path.basename(projectDir),
|
||||
projectDir,
|
||||
relativePath: ".",
|
||||
database: detectedConfig.database || "none",
|
||||
orm: detectedConfig.orm || "none",
|
||||
backend: detectedConfig.backend || "none",
|
||||
runtime: detectedConfig.runtime || "none",
|
||||
frontend: detectedConfig.frontend || [],
|
||||
addons: input.addons,
|
||||
examples: detectedConfig.examples || [],
|
||||
auth: detectedConfig.auth || false,
|
||||
git: false,
|
||||
packageManager:
|
||||
input.packageManager || detectedConfig.packageManager || "npm",
|
||||
install: input.install || false,
|
||||
dbSetup: detectedConfig.dbSetup || "none",
|
||||
api: detectedConfig.api || "none",
|
||||
webDeploy: detectedConfig.webDeploy || "none",
|
||||
serverDeploy: detectedConfig.serverDeploy || "none",
|
||||
};
|
||||
|
||||
for (const addon of input.addons) {
|
||||
const { isCompatible, reason } = validateAddonCompatibility(
|
||||
addon,
|
||||
config.frontend,
|
||||
);
|
||||
if (!isCompatible) {
|
||||
exitWithError(
|
||||
reason ||
|
||||
`${addon} addon is not compatible with current frontend configuration`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await setupAddonsTemplate(projectDir, config);
|
||||
await setupAddons(config, true);
|
||||
|
||||
const currentAddons = detectedConfig.addons || [];
|
||||
const mergedAddons = [...new Set([...currentAddons, ...input.addons])];
|
||||
await updateBtsConfig(projectDir, { addons: mergedAddons });
|
||||
|
||||
if (config.install) {
|
||||
await installDependencies({
|
||||
projectDir,
|
||||
packageManager: config.packageManager,
|
||||
});
|
||||
} else if (!input.suppressInstallMessage) {
|
||||
log.info(
|
||||
pc.yellow(
|
||||
`Run ${pc.bold(
|
||||
`${config.packageManager} install`,
|
||||
)} to install dependencies`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
exitWithError(`Error adding addons: ${message}`);
|
||||
}
|
||||
}
|
||||
121
apps/cli/src/helpers/core/add-deployment.ts
Normal file
121
apps/cli/src/helpers/core/add-deployment.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
AddInput,
|
||||
ProjectConfig,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../../types";
|
||||
import { updateBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupServerDeploy } from "../deployment/server-deploy-setup";
|
||||
import { setupWebDeploy } from "../deployment/web-deploy-setup";
|
||||
import {
|
||||
detectProjectConfig,
|
||||
isBetterTStackProject,
|
||||
} from "./detect-project-config";
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
import { setupDeploymentTemplates } from "./template-manager";
|
||||
|
||||
export async function addDeploymentToProject(
|
||||
input: AddInput & {
|
||||
webDeploy?: WebDeploy;
|
||||
serverDeploy?: ServerDeploy;
|
||||
suppressInstallMessage?: boolean;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const projectDir = input.projectDir || process.cwd();
|
||||
|
||||
const isBetterTStack = await isBetterTStackProject(projectDir);
|
||||
if (!isBetterTStack) {
|
||||
exitWithError(
|
||||
"This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.",
|
||||
);
|
||||
}
|
||||
|
||||
const detectedConfig = await detectProjectConfig(projectDir);
|
||||
if (!detectedConfig) {
|
||||
exitWithError(
|
||||
"Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.",
|
||||
);
|
||||
}
|
||||
|
||||
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) {
|
||||
exitWithError(
|
||||
`${input.webDeploy} web deployment is already configured for this project.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
input.serverDeploy &&
|
||||
detectedConfig.serverDeploy === input.serverDeploy
|
||||
) {
|
||||
exitWithError(
|
||||
`${input.serverDeploy} server deployment is already configured for this project.`,
|
||||
);
|
||||
}
|
||||
|
||||
const config: ProjectConfig = {
|
||||
projectName: detectedConfig.projectName || path.basename(projectDir),
|
||||
projectDir,
|
||||
relativePath: ".",
|
||||
database: detectedConfig.database || "none",
|
||||
orm: detectedConfig.orm || "none",
|
||||
backend: detectedConfig.backend || "none",
|
||||
runtime: detectedConfig.runtime || "none",
|
||||
frontend: detectedConfig.frontend || [],
|
||||
addons: detectedConfig.addons || [],
|
||||
examples: detectedConfig.examples || [],
|
||||
auth: detectedConfig.auth || false,
|
||||
git: false,
|
||||
packageManager:
|
||||
input.packageManager || detectedConfig.packageManager || "npm",
|
||||
install: input.install || false,
|
||||
dbSetup: detectedConfig.dbSetup || "none",
|
||||
api: detectedConfig.api || "none",
|
||||
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
|
||||
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none",
|
||||
};
|
||||
|
||||
if (input.webDeploy && input.webDeploy !== "none") {
|
||||
log.info(
|
||||
pc.green(
|
||||
`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.serverDeploy && input.serverDeploy !== "none") {
|
||||
log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
|
||||
}
|
||||
|
||||
await setupDeploymentTemplates(projectDir, config);
|
||||
await setupWebDeploy(config);
|
||||
await setupServerDeploy(config);
|
||||
|
||||
await updateBtsConfig(projectDir, {
|
||||
webDeploy: input.webDeploy || config.webDeploy,
|
||||
serverDeploy: input.serverDeploy || config.serverDeploy,
|
||||
});
|
||||
|
||||
if (config.install) {
|
||||
await installDependencies({
|
||||
projectDir,
|
||||
packageManager: config.packageManager,
|
||||
});
|
||||
} else if (!input.suppressInstallMessage) {
|
||||
log.info(
|
||||
pc.yellow(
|
||||
`Run ${pc.bold(
|
||||
`${config.packageManager} install`,
|
||||
)} to install dependencies`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
exitWithError(`Error adding deployment: ${message}`);
|
||||
}
|
||||
}
|
||||
303
apps/cli/src/helpers/core/api-setup.ts
Normal file
303
apps/cli/src/helpers/core/api-setup.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import type { Frontend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
async function addBackendWorkspaceDependency(
|
||||
projectDir: string,
|
||||
backendPackageName: string,
|
||||
workspaceVersion: string,
|
||||
) {
|
||||
const pkgJsonPath = path.join(projectDir, "package.json");
|
||||
try {
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
if (!pkgJson.dependencies) {
|
||||
pkgJson.dependencies = {};
|
||||
}
|
||||
pkgJson.dependencies[backendPackageName] = workspaceVersion;
|
||||
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
function getFrontendType(frontend: Frontend[]): {
|
||||
hasReactWeb: boolean;
|
||||
hasNuxtWeb: boolean;
|
||||
hasSvelteWeb: boolean;
|
||||
hasSolidWeb: boolean;
|
||||
hasNative: boolean;
|
||||
} {
|
||||
const reactBasedFrontends = [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
];
|
||||
const nativeFrontends = ["native-nativewind", "native-unistyles"];
|
||||
|
||||
return {
|
||||
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
|
||||
hasNuxtWeb: frontend.includes("nuxt"),
|
||||
hasSvelteWeb: frontend.includes("svelte"),
|
||||
hasSolidWeb: frontend.includes("solid"),
|
||||
hasNative: frontend.some((f) => nativeFrontends.includes(f)),
|
||||
};
|
||||
}
|
||||
|
||||
function getApiDependencies(
|
||||
api: string,
|
||||
frontendType: ReturnType<typeof getFrontendType>,
|
||||
) {
|
||||
const deps: Record<
|
||||
string,
|
||||
{ dependencies: string[]; devDependencies?: string[] }
|
||||
> = {};
|
||||
|
||||
if (api === "orpc") {
|
||||
deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
|
||||
} else if (api === "trpc") {
|
||||
deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
|
||||
}
|
||||
|
||||
if (frontendType.hasReactWeb) {
|
||||
if (api === "orpc") {
|
||||
deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
||||
} else if (api === "trpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@trpc/tanstack-react-query",
|
||||
"@trpc/client",
|
||||
"@trpc/server",
|
||||
],
|
||||
};
|
||||
}
|
||||
} else if (frontendType.hasNuxtWeb && api === "orpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@tanstack/vue-query",
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
],
|
||||
devDependencies: ["@tanstack/vue-query-devtools"],
|
||||
};
|
||||
} else if (frontendType.hasSvelteWeb && api === "orpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
"@tanstack/svelte-query",
|
||||
],
|
||||
devDependencies: ["@tanstack/svelte-query-devtools"],
|
||||
};
|
||||
} else if (frontendType.hasSolidWeb && api === "orpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
"@tanstack/solid-query",
|
||||
],
|
||||
devDependencies: [
|
||||
"@tanstack/solid-query-devtools",
|
||||
"@tanstack/solid-router-devtools",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (api === "trpc") {
|
||||
deps.native = {
|
||||
dependencies: [
|
||||
"@trpc/tanstack-react-query",
|
||||
"@trpc/client",
|
||||
"@trpc/server",
|
||||
],
|
||||
};
|
||||
} else if (api === "orpc") {
|
||||
deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function getQueryDependencies(frontend: Frontend[]) {
|
||||
const reactBasedFrontends: Frontend[] = [
|
||||
"react-router",
|
||||
"tanstack-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
];
|
||||
|
||||
const deps: Record<
|
||||
string,
|
||||
{ dependencies: string[]; devDependencies?: string[] }
|
||||
> = {};
|
||||
|
||||
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
||||
if (needsReactQuery) {
|
||||
const hasReactWeb = frontend.some(
|
||||
(f) =>
|
||||
f !== "native-nativewind" &&
|
||||
f !== "native-unistyles" &&
|
||||
reactBasedFrontends.includes(f),
|
||||
);
|
||||
const hasNative =
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles");
|
||||
|
||||
if (hasReactWeb) {
|
||||
deps.web = {
|
||||
dependencies: ["@tanstack/react-query"],
|
||||
devDependencies: ["@tanstack/react-query-devtools"],
|
||||
};
|
||||
}
|
||||
if (hasNative) {
|
||||
deps.native = { dependencies: ["@tanstack/react-query"] };
|
||||
}
|
||||
}
|
||||
|
||||
if (frontend.includes("solid")) {
|
||||
deps.web = {
|
||||
dependencies: ["@tanstack/solid-query"],
|
||||
devDependencies: [
|
||||
"@tanstack/solid-query-devtools",
|
||||
"@tanstack/solid-router-devtools",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function getConvexDependencies(frontend: Frontend[]) {
|
||||
const deps: Record<string, { dependencies: string[] }> = {
|
||||
web: { dependencies: ["convex"] },
|
||||
native: { dependencies: ["convex"] },
|
||||
};
|
||||
|
||||
if (frontend.includes("tanstack-start")) {
|
||||
deps.web.dependencies.push("@convex-dev/react-query");
|
||||
}
|
||||
if (frontend.includes("svelte")) {
|
||||
deps.web.dependencies.push("convex-svelte");
|
||||
}
|
||||
if (frontend.includes("nuxt")) {
|
||||
deps.web.dependencies.push("convex-nuxt", "convex-vue");
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
export async function setupApi(config: ProjectConfig) {
|
||||
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
||||
config;
|
||||
const isConvex = backend === "convex";
|
||||
|
||||
const webDir = path.join(projectDir, "apps/web");
|
||||
const nativeDir = path.join(projectDir, "apps/native");
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
const webDirExists = await fs.pathExists(webDir);
|
||||
const nativeDirExists = await fs.pathExists(nativeDir);
|
||||
const serverDirExists = await fs.pathExists(serverDir);
|
||||
|
||||
const frontendType = getFrontendType(frontend);
|
||||
|
||||
if (!isConvex && api !== "none") {
|
||||
const apiDeps = getApiDependencies(api, frontendType);
|
||||
|
||||
if (serverDirExists && apiDeps.server) {
|
||||
await addPackageDependency({
|
||||
dependencies: apiDeps.server.dependencies as AvailableDependencies[],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
if (api === "trpc") {
|
||||
if (backend === "hono") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@hono/trpc-server"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else if (backend === "elysia") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@elysiajs/trpc"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (webDirExists && apiDeps.web) {
|
||||
await addPackageDependency({
|
||||
dependencies: apiDeps.web.dependencies as AvailableDependencies[],
|
||||
devDependencies: apiDeps.web.devDependencies as AvailableDependencies[],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (nativeDirExists && apiDeps.native) {
|
||||
await addPackageDependency({
|
||||
dependencies: apiDeps.native.dependencies as AvailableDependencies[],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isConvex) {
|
||||
const queryDeps = getQueryDependencies(frontend);
|
||||
|
||||
if (webDirExists && queryDeps.web) {
|
||||
await addPackageDependency({
|
||||
dependencies: queryDeps.web.dependencies as AvailableDependencies[],
|
||||
devDependencies: queryDeps.web
|
||||
.devDependencies as AvailableDependencies[],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (nativeDirExists && queryDeps.native) {
|
||||
await addPackageDependency({
|
||||
dependencies: queryDeps.native.dependencies as AvailableDependencies[],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
const convexDeps = getConvexDependencies(frontend);
|
||||
|
||||
if (webDirExists) {
|
||||
await addPackageDependency({
|
||||
dependencies: convexDeps.web.dependencies as AvailableDependencies[],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
await addPackageDependency({
|
||||
dependencies: convexDeps.native.dependencies as AvailableDependencies[],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
|
||||
const backendPackageName = `@${projectName}/backend`;
|
||||
const backendWorkspaceVersion =
|
||||
packageManager === "npm" ? "*" : "workspace:*";
|
||||
|
||||
if (webDirExists) {
|
||||
await addBackendWorkspaceDependency(
|
||||
webDir,
|
||||
backendPackageName,
|
||||
backendWorkspaceVersion,
|
||||
);
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
await addBackendWorkspaceDependency(
|
||||
nativeDir,
|
||||
backendPackageName,
|
||||
backendWorkspaceVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
apps/cli/src/helpers/core/backend-setup.ts
Normal file
64
apps/cli/src/helpers/core/backend-setup.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import path from "node:path";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupBackendDependencies(config: ProjectConfig) {
|
||||
const { backend, runtime, api, projectDir } = config;
|
||||
|
||||
if (backend === "convex") {
|
||||
return;
|
||||
}
|
||||
|
||||
const framework = backend;
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
const dependencies: AvailableDependencies[] = [];
|
||||
const devDependencies: AvailableDependencies[] = [];
|
||||
|
||||
if (framework === "hono") {
|
||||
dependencies.push("hono");
|
||||
if (api === "trpc") {
|
||||
dependencies.push("@hono/trpc-server");
|
||||
}
|
||||
|
||||
if (runtime === "node") {
|
||||
dependencies.push("@hono/node-server");
|
||||
devDependencies.push("tsx", "@types/node");
|
||||
}
|
||||
} else if (framework === "elysia") {
|
||||
dependencies.push("elysia", "@elysiajs/cors");
|
||||
if (api === "trpc") {
|
||||
dependencies.push("@elysiajs/trpc");
|
||||
}
|
||||
if (runtime === "node") {
|
||||
dependencies.push("@elysiajs/node");
|
||||
devDependencies.push("tsx", "@types/node");
|
||||
}
|
||||
} else if (framework === "express") {
|
||||
dependencies.push("express", "cors");
|
||||
devDependencies.push("@types/express", "@types/cors");
|
||||
|
||||
if (runtime === "node") {
|
||||
devDependencies.push("tsx", "@types/node");
|
||||
}
|
||||
} else if (framework === "fastify") {
|
||||
dependencies.push("fastify", "@fastify/cors");
|
||||
|
||||
if (runtime === "node") {
|
||||
devDependencies.push("tsx", "@types/node");
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime === "bun") {
|
||||
devDependencies.push("@types/bun");
|
||||
}
|
||||
|
||||
if (dependencies.length > 0 || devDependencies.length > 0) {
|
||||
await addPackageDependency({
|
||||
dependencies,
|
||||
devDependencies,
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
372
apps/cli/src/helpers/core/command-handlers.ts
Normal file
372
apps/cli/src/helpers/core/command-handlers.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import path from "node:path";
|
||||
import { intro, log, outro } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { getDefaultConfig } from "../../constants";
|
||||
import { getAddonsToAdd } from "../../prompts/addons";
|
||||
import { gatherConfig } from "../../prompts/config-prompts";
|
||||
import { getProjectName } from "../../prompts/project-name";
|
||||
import { getServerDeploymentToAdd } from "../../prompts/server-deploy";
|
||||
import { getDeploymentToAdd } from "../../prompts/web-deploy";
|
||||
import type {
|
||||
AddInput,
|
||||
CreateInput,
|
||||
DirectoryConflict,
|
||||
InitResult,
|
||||
ProjectConfig,
|
||||
} from "../../types";
|
||||
import { trackProjectCreation } from "../../utils/analytics";
|
||||
import { coerceBackendPresets } from "../../utils/compatibility-rules";
|
||||
import { displayConfig } from "../../utils/display-config";
|
||||
import { exitWithError, handleError } from "../../utils/errors";
|
||||
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
|
||||
import {
|
||||
handleDirectoryConflict,
|
||||
setupProjectDirectory,
|
||||
} from "../../utils/project-directory";
|
||||
import { renderTitle } from "../../utils/render-title";
|
||||
import {
|
||||
getProvidedFlags,
|
||||
processAndValidateFlags,
|
||||
processProvidedFlagsWithoutValidation,
|
||||
validateConfigCompatibility,
|
||||
} from "../../validation";
|
||||
import { addAddonsToProject } from "./add-addons";
|
||||
import { addDeploymentToProject } from "./add-deployment";
|
||||
import { createProject } from "./create-project";
|
||||
import { detectProjectConfig } from "./detect-project-config";
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
|
||||
export async function createProjectHandler(
|
||||
input: CreateInput & { projectName?: string },
|
||||
): Promise<InitResult> {
|
||||
const startTime = Date.now();
|
||||
const timeScaffolded = new Date().toISOString();
|
||||
|
||||
if (input.renderTitle !== false) {
|
||||
renderTitle();
|
||||
}
|
||||
intro(pc.magenta("Creating a new Better-T Stack project"));
|
||||
|
||||
if (input.yolo) {
|
||||
consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
|
||||
}
|
||||
|
||||
let currentPathInput: string;
|
||||
if (input.yes && input.projectName) {
|
||||
currentPathInput = input.projectName;
|
||||
} else if (input.yes) {
|
||||
const defaultConfig = getDefaultConfig();
|
||||
let defaultName = defaultConfig.relativePath;
|
||||
let counter = 1;
|
||||
while (
|
||||
(await fs.pathExists(path.resolve(process.cwd(), defaultName))) &&
|
||||
(await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0
|
||||
) {
|
||||
defaultName = `${defaultConfig.projectName}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
currentPathInput = defaultName;
|
||||
} else {
|
||||
currentPathInput = await getProjectName(input.projectName);
|
||||
}
|
||||
|
||||
let finalPathInput: string;
|
||||
let shouldClearDirectory: boolean;
|
||||
|
||||
try {
|
||||
if (input.directoryConflict) {
|
||||
const result = await handleDirectoryConflictProgrammatically(
|
||||
currentPathInput,
|
||||
input.directoryConflict,
|
||||
);
|
||||
finalPathInput = result.finalPathInput;
|
||||
shouldClearDirectory = result.shouldClearDirectory;
|
||||
} else {
|
||||
const result = await handleDirectoryConflict(currentPathInput);
|
||||
finalPathInput = result.finalPathInput;
|
||||
shouldClearDirectory = result.shouldClearDirectory;
|
||||
}
|
||||
} catch (error) {
|
||||
const elapsedTimeMs = Date.now() - startTime;
|
||||
return {
|
||||
success: false,
|
||||
projectConfig: {
|
||||
projectName: "",
|
||||
projectDir: "",
|
||||
relativePath: "",
|
||||
database: "none",
|
||||
orm: "none",
|
||||
backend: "none",
|
||||
runtime: "none",
|
||||
frontend: [],
|
||||
addons: [],
|
||||
examples: [],
|
||||
auth: false,
|
||||
git: false,
|
||||
packageManager: "npm",
|
||||
install: false,
|
||||
dbSetup: "none",
|
||||
api: "none",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
} satisfies ProjectConfig,
|
||||
reproducibleCommand: "",
|
||||
timeScaffolded,
|
||||
elapsedTimeMs,
|
||||
projectDirectory: "",
|
||||
relativePath: "",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
|
||||
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
|
||||
finalPathInput,
|
||||
shouldClearDirectory,
|
||||
);
|
||||
|
||||
const cliInput = {
|
||||
...input,
|
||||
projectDirectory: input.projectName,
|
||||
};
|
||||
|
||||
const providedFlags = getProvidedFlags(cliInput);
|
||||
|
||||
let config: ProjectConfig;
|
||||
if (input.yes) {
|
||||
const flagConfig = processProvidedFlagsWithoutValidation(
|
||||
cliInput,
|
||||
finalBaseName,
|
||||
);
|
||||
|
||||
config = {
|
||||
...getDefaultConfig(),
|
||||
...flagConfig,
|
||||
projectName: finalBaseName,
|
||||
projectDir: finalResolvedPath,
|
||||
relativePath: finalPathInput,
|
||||
};
|
||||
|
||||
coerceBackendPresets(config);
|
||||
|
||||
validateConfigCompatibility(config, providedFlags, cliInput);
|
||||
|
||||
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 {
|
||||
const flagConfig = processAndValidateFlags(
|
||||
cliInput,
|
||||
providedFlags,
|
||||
finalBaseName,
|
||||
);
|
||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||
|
||||
if (Object.keys(otherFlags).length > 0) {
|
||||
log.info(pc.yellow("Using these pre-selected options:"));
|
||||
log.message(displayConfig(otherFlags));
|
||||
log.message("");
|
||||
}
|
||||
|
||||
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, input.disableAnalytics);
|
||||
|
||||
const elapsedTimeMs = Date.now() - startTime;
|
||||
const elapsedTimeInSeconds = (elapsedTimeMs / 1000).toFixed(2);
|
||||
outro(
|
||||
pc.magenta(
|
||||
`Project created successfully in ${pc.bold(
|
||||
elapsedTimeInSeconds,
|
||||
)} seconds!`,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
projectConfig: config,
|
||||
reproducibleCommand,
|
||||
timeScaffolded,
|
||||
elapsedTimeMs,
|
||||
projectDirectory: config.projectDir,
|
||||
relativePath: config.relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleDirectoryConflictProgrammatically(
|
||||
currentPathInput: string,
|
||||
strategy: DirectoryConflict,
|
||||
): Promise<{ finalPathInput: string; shouldClearDirectory: boolean }> {
|
||||
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
||||
|
||||
if (!(await fs.pathExists(currentPath))) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
const dirContents = await fs.readdir(currentPath);
|
||||
const isNotEmpty = dirContents.length > 0;
|
||||
|
||||
if (!isNotEmpty) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case "overwrite":
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
|
||||
|
||||
case "merge":
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
|
||||
case "increment": {
|
||||
let counter = 1;
|
||||
const baseName = currentPathInput;
|
||||
let finalPathInput = `${baseName}-${counter}`;
|
||||
|
||||
while (
|
||||
(await fs.pathExists(path.resolve(process.cwd(), finalPathInput))) &&
|
||||
(await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length >
|
||||
0
|
||||
) {
|
||||
counter++;
|
||||
finalPathInput = `${baseName}-${counter}`;
|
||||
}
|
||||
|
||||
return { finalPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
case "error":
|
||||
throw new Error(
|
||||
`Directory "${currentPathInput}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`,
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown directory conflict strategy: ${strategy}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAddonsHandler(input: AddInput) {
|
||||
try {
|
||||
const projectDir = input.projectDir || process.cwd();
|
||||
const detectedConfig = await detectProjectConfig(projectDir);
|
||||
|
||||
if (!detectedConfig) {
|
||||
exitWithError(
|
||||
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!input.addons || input.addons.length === 0) {
|
||||
const addonsPrompt = await getAddonsToAdd(
|
||||
detectedConfig.frontend || [],
|
||||
detectedConfig.addons || [],
|
||||
);
|
||||
|
||||
if (addonsPrompt.length > 0) {
|
||||
input.addons = addonsPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.webDeploy) {
|
||||
const deploymentPrompt = await getDeploymentToAdd(
|
||||
detectedConfig.frontend || [],
|
||||
detectedConfig.webDeploy,
|
||||
);
|
||||
|
||||
if (deploymentPrompt !== "none") {
|
||||
input.webDeploy = deploymentPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.serverDeploy) {
|
||||
const serverDeploymentPrompt = await getServerDeploymentToAdd(
|
||||
detectedConfig.runtime,
|
||||
detectedConfig.serverDeploy,
|
||||
);
|
||||
|
||||
if (serverDeploymentPrompt !== "none") {
|
||||
input.serverDeploy = serverDeploymentPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
const packageManager =
|
||||
input.packageManager || detectedConfig.packageManager || "npm";
|
||||
|
||||
let somethingAdded = false;
|
||||
|
||||
if (input.addons && input.addons.length > 0) {
|
||||
await addAddonsToProject({
|
||||
...input,
|
||||
install: false,
|
||||
suppressInstallMessage: true,
|
||||
addons: input.addons,
|
||||
});
|
||||
somethingAdded = true;
|
||||
}
|
||||
|
||||
if (input.webDeploy && input.webDeploy !== "none") {
|
||||
await addDeploymentToProject({
|
||||
...input,
|
||||
install: false,
|
||||
suppressInstallMessage: true,
|
||||
webDeploy: input.webDeploy,
|
||||
});
|
||||
somethingAdded = true;
|
||||
}
|
||||
|
||||
if (input.serverDeploy && input.serverDeploy !== "none") {
|
||||
await addDeploymentToProject({
|
||||
...input,
|
||||
install: false,
|
||||
suppressInstallMessage: true,
|
||||
serverDeploy: input.serverDeploy,
|
||||
});
|
||||
somethingAdded = true;
|
||||
}
|
||||
|
||||
if (!somethingAdded) {
|
||||
outro(pc.yellow("No addons or deployment configurations to add."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.install) {
|
||||
await installDependencies({
|
||||
projectDir,
|
||||
packageManager,
|
||||
});
|
||||
} else {
|
||||
log.info(
|
||||
`Run ${pc.bold(`${packageManager} install`)} to install dependencies`,
|
||||
);
|
||||
}
|
||||
|
||||
outro("Add command completed successfully!");
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to add addons or deployment");
|
||||
}
|
||||
}
|
||||
13
apps/cli/src/helpers/core/convex-codegen.ts
Normal file
13
apps/cli/src/helpers/core/convex-codegen.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import path from "node:path";
|
||||
import { execa } from "execa";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
|
||||
export async function runConvexCodegen(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager | null | undefined,
|
||||
) {
|
||||
const backendDir = path.join(projectDir, "packages/backend");
|
||||
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
||||
await execa(cmd, { cwd: backendDir, shell: true });
|
||||
}
|
||||
120
apps/cli/src/helpers/core/create-project.ts
Normal file
120
apps/cli/src/helpers/core/create-project.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { writeBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { formatProjectWithBiome } from "../../utils/format-with-biome";
|
||||
import { setupAddons } from "../addons/addons-setup";
|
||||
import { setupAuth } from "../addons/auth-setup";
|
||||
import { setupExamples } from "../addons/examples-setup";
|
||||
import { setupApi } from "../core/api-setup";
|
||||
import { setupBackendDependencies } from "../core/backend-setup";
|
||||
import { setupDatabase } from "../core/db-setup";
|
||||
import { setupRuntime } from "../core/runtime-setup";
|
||||
import { setupServerDeploy } from "../deployment/server-deploy-setup";
|
||||
import { setupWebDeploy } from "../deployment/web-deploy-setup";
|
||||
import { runConvexCodegen } from "./convex-codegen";
|
||||
import { createReadme } from "./create-readme";
|
||||
import { setupEnvironmentVariables } from "./env-setup";
|
||||
import { initializeGit } from "./git";
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
import { displayPostInstallInstructions } from "./post-installation";
|
||||
import { updatePackageConfigurations } from "./project-config";
|
||||
import {
|
||||
copyBaseTemplate,
|
||||
handleExtras,
|
||||
setupAddonsTemplate,
|
||||
setupAuthTemplate,
|
||||
setupBackendFramework,
|
||||
setupDbOrmTemplates,
|
||||
setupDeploymentTemplates,
|
||||
setupDockerComposeTemplates,
|
||||
setupExamplesTemplate,
|
||||
setupFrontendTemplates,
|
||||
} from "./template-manager";
|
||||
|
||||
export async function createProject(options: ProjectConfig) {
|
||||
const projectDir = options.projectDir;
|
||||
const isConvex = options.backend === "convex";
|
||||
|
||||
try {
|
||||
await fs.ensureDir(projectDir);
|
||||
|
||||
await copyBaseTemplate(projectDir, options);
|
||||
await setupFrontendTemplates(projectDir, options);
|
||||
await setupBackendFramework(projectDir, options);
|
||||
if (!isConvex) {
|
||||
await setupDbOrmTemplates(projectDir, options);
|
||||
await setupDockerComposeTemplates(projectDir, options);
|
||||
await setupAuthTemplate(projectDir, options);
|
||||
}
|
||||
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
||||
await setupExamplesTemplate(projectDir, options);
|
||||
}
|
||||
await setupAddonsTemplate(projectDir, options);
|
||||
|
||||
await setupDeploymentTemplates(projectDir, options);
|
||||
|
||||
await setupApi(options);
|
||||
|
||||
if (!isConvex) {
|
||||
await setupBackendDependencies(options);
|
||||
await setupDatabase(options);
|
||||
await setupRuntime(options);
|
||||
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
||||
await setupExamples(options);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.addons.length > 0 && options.addons[0] !== "none") {
|
||||
await setupAddons(options);
|
||||
}
|
||||
|
||||
if (!isConvex && options.auth) {
|
||||
await setupAuth(options);
|
||||
}
|
||||
|
||||
await handleExtras(projectDir, options);
|
||||
|
||||
await setupWebDeploy(options);
|
||||
await setupServerDeploy(options);
|
||||
|
||||
await setupEnvironmentVariables(options);
|
||||
await updatePackageConfigurations(projectDir, options);
|
||||
await createReadme(projectDir, options);
|
||||
|
||||
await writeBtsConfig(options);
|
||||
|
||||
await formatProjectWithBiome(projectDir);
|
||||
|
||||
if (isConvex) {
|
||||
await runConvexCodegen(projectDir, options.packageManager);
|
||||
}
|
||||
|
||||
log.success("Project template successfully scaffolded!");
|
||||
|
||||
if (options.install) {
|
||||
await installDependencies({
|
||||
projectDir,
|
||||
packageManager: options.packageManager,
|
||||
});
|
||||
}
|
||||
|
||||
await initializeGit(projectDir, options.git);
|
||||
|
||||
await displayPostInstallInstructions({
|
||||
...options,
|
||||
depsInstalled: options.install,
|
||||
});
|
||||
|
||||
return projectDir;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack);
|
||||
exitWithError(`Error during project creation: ${error.message}`);
|
||||
} else {
|
||||
console.error(error);
|
||||
exitWithError(`An unexpected error occurred: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
634
apps/cli/src/helpers/core/create-readme.ts
Normal file
634
apps/cli/src/helpers/core/create-readme.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
import path from "node:path";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
Frontend,
|
||||
ORM,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
} from "../../types";
|
||||
|
||||
export async function createReadme(projectDir: string, options: ProjectConfig) {
|
||||
const readmePath = path.join(projectDir, "README.md");
|
||||
const content = generateReadmeContent(options);
|
||||
|
||||
try {
|
||||
await fs.writeFile(readmePath, content);
|
||||
} catch (error) {
|
||||
consola.error("Failed to create README.md file:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function generateReadmeContent(options: ProjectConfig): string {
|
||||
const {
|
||||
projectName,
|
||||
packageManager,
|
||||
database,
|
||||
auth,
|
||||
addons = [],
|
||||
orm = "drizzle",
|
||||
runtime = "bun",
|
||||
frontend = ["tanstack-router"],
|
||||
backend = "hono",
|
||||
api = "trpc",
|
||||
} = options;
|
||||
|
||||
const isConvex = backend === "convex";
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasNative =
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles");
|
||||
const hasSvelte = frontend.includes("svelte");
|
||||
|
||||
const packageManagerRunCmd =
|
||||
packageManager === "npm" ? "npm run" : packageManager;
|
||||
|
||||
let webPort = "3001";
|
||||
if (hasReactRouter || hasSvelte) {
|
||||
webPort = "5173";
|
||||
}
|
||||
|
||||
const stackDescription = generateStackDescription(
|
||||
frontend,
|
||||
backend,
|
||||
api,
|
||||
isConvex,
|
||||
);
|
||||
|
||||
return `# ${projectName}
|
||||
|
||||
This project was created with [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack), a modern TypeScript stack${
|
||||
stackDescription ? ` that combines ${stackDescription}` : ""
|
||||
}.
|
||||
|
||||
## Features
|
||||
|
||||
${generateFeaturesList(
|
||||
database,
|
||||
auth,
|
||||
addons,
|
||||
orm,
|
||||
runtime,
|
||||
frontend,
|
||||
backend,
|
||||
api,
|
||||
)}
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
\`\`\`bash
|
||||
${packageManager} install
|
||||
\`\`\`
|
||||
${
|
||||
isConvex
|
||||
? `
|
||||
## Convex Setup
|
||||
|
||||
This project uses Convex as a backend. You'll need to set up Convex before running the app:
|
||||
|
||||
\`\`\`bash
|
||||
${packageManagerRunCmd} dev:setup
|
||||
\`\`\`
|
||||
|
||||
Follow the prompts to create a new Convex project and connect it to your application.`
|
||||
: generateDatabaseSetup(
|
||||
database,
|
||||
auth,
|
||||
packageManagerRunCmd,
|
||||
orm,
|
||||
options.dbSetup,
|
||||
)
|
||||
}
|
||||
|
||||
Then, run the development server:
|
||||
|
||||
\`\`\`bash
|
||||
${packageManagerRunCmd} dev
|
||||
\`\`\`
|
||||
|
||||
${generateRunningInstructions(frontend, backend, webPort, hasNative, isConvex)}
|
||||
|
||||
${
|
||||
addons.includes("pwa") && hasReactRouter
|
||||
? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n"
|
||||
: ""
|
||||
}
|
||||
|
||||
## Project Structure
|
||||
|
||||
\`\`\`
|
||||
${generateProjectStructure(
|
||||
projectName,
|
||||
frontend,
|
||||
backend,
|
||||
addons,
|
||||
isConvex,
|
||||
api,
|
||||
)}
|
||||
\`\`\`
|
||||
|
||||
## Available Scripts
|
||||
|
||||
${generateScriptsList(
|
||||
packageManagerRunCmd,
|
||||
database,
|
||||
orm,
|
||||
auth,
|
||||
hasNative,
|
||||
addons,
|
||||
backend,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateStackDescription(
|
||||
frontend: Frontend[],
|
||||
backend: string,
|
||||
api: API,
|
||||
isConvex: boolean,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasNext = frontend.includes("next");
|
||||
const hasTanstackStart = frontend.includes("tanstack-start");
|
||||
const hasSvelte = frontend.includes("svelte");
|
||||
const hasNuxt = frontend.includes("nuxt");
|
||||
const hasSolid = frontend.includes("solid");
|
||||
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
|
||||
|
||||
if (!hasFrontendNone) {
|
||||
if (hasTanstackRouter) {
|
||||
parts.push("React, TanStack Router");
|
||||
} else if (hasReactRouter) {
|
||||
parts.push("React, React Router");
|
||||
} else if (hasNext) {
|
||||
parts.push("Next.js");
|
||||
} else if (hasTanstackStart) {
|
||||
parts.push("React, TanStack Start");
|
||||
} else if (hasSvelte) {
|
||||
parts.push("SvelteKit");
|
||||
} else if (hasNuxt) {
|
||||
parts.push("Nuxt");
|
||||
} else if (hasSolid) {
|
||||
parts.push("SolidJS");
|
||||
}
|
||||
}
|
||||
|
||||
if (backend !== "none") {
|
||||
parts.push(backend[0].toUpperCase() + backend.slice(1));
|
||||
}
|
||||
|
||||
if (!isConvex && api !== "none") {
|
||||
parts.push(api.toUpperCase());
|
||||
}
|
||||
|
||||
return parts.length > 0 ? `${parts.join(", ")}, and more` : "";
|
||||
}
|
||||
|
||||
function generateRunningInstructions(
|
||||
frontend: Frontend[],
|
||||
backend: string,
|
||||
webPort: string,
|
||||
hasNative: boolean,
|
||||
isConvex: boolean,
|
||||
): string {
|
||||
const instructions: string[] = [];
|
||||
|
||||
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
|
||||
const isBackendNone = backend === "none";
|
||||
|
||||
if (!hasFrontendNone) {
|
||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasNext = frontend.includes("next");
|
||||
const hasTanstackStart = frontend.includes("tanstack-start");
|
||||
const hasSvelte = frontend.includes("svelte");
|
||||
const hasNuxt = frontend.includes("nuxt");
|
||||
const hasSolid = frontend.includes("solid");
|
||||
|
||||
if (
|
||||
hasTanstackRouter ||
|
||||
hasReactRouter ||
|
||||
hasNext ||
|
||||
hasTanstackStart ||
|
||||
hasSvelte ||
|
||||
hasNuxt ||
|
||||
hasSolid
|
||||
) {
|
||||
instructions.push(
|
||||
`Open [http://localhost:${webPort}](http://localhost:${webPort}) in your browser to see the web application.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNative) {
|
||||
instructions.push("Use the Expo Go app to run the mobile application.");
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
instructions.push(
|
||||
"Your app will connect to the Convex cloud backend automatically.",
|
||||
);
|
||||
} else if (!isBackendNone) {
|
||||
instructions.push(
|
||||
"The API is running at [http://localhost:3000](http://localhost:3000).",
|
||||
);
|
||||
}
|
||||
|
||||
return instructions.join("\n");
|
||||
}
|
||||
|
||||
function generateProjectStructure(
|
||||
projectName: string,
|
||||
frontend: Frontend[],
|
||||
backend: string,
|
||||
addons: Addons[],
|
||||
isConvex: boolean,
|
||||
api: API,
|
||||
): string {
|
||||
const structure: string[] = [`${projectName}/`, "├── apps/"];
|
||||
|
||||
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
|
||||
const isBackendNone = backend === "none";
|
||||
|
||||
if (!hasFrontendNone) {
|
||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasNext = frontend.includes("next");
|
||||
const hasTanstackStart = frontend.includes("tanstack-start");
|
||||
const hasSvelte = frontend.includes("svelte");
|
||||
const hasNuxt = frontend.includes("nuxt");
|
||||
const hasSolid = frontend.includes("solid");
|
||||
|
||||
if (
|
||||
hasTanstackRouter ||
|
||||
hasReactRouter ||
|
||||
hasNext ||
|
||||
hasTanstackStart ||
|
||||
hasSvelte ||
|
||||
hasNuxt ||
|
||||
hasSolid
|
||||
) {
|
||||
let frontendType = "";
|
||||
if (hasTanstackRouter) frontendType = "React + TanStack Router";
|
||||
else if (hasReactRouter) frontendType = "React + React Router";
|
||||
else if (hasNext) frontendType = "Next.js";
|
||||
else if (hasTanstackStart) frontendType = "React + TanStack Start";
|
||||
else if (hasSvelte) frontendType = "SvelteKit";
|
||||
else if (hasNuxt) frontendType = "Nuxt";
|
||||
else if (hasSolid) frontendType = "SolidJS";
|
||||
|
||||
structure.push(
|
||||
`│ ├── web/ # Frontend application (${frontendType})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hasNative =
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles");
|
||||
if (hasNative) {
|
||||
structure.push(
|
||||
"│ ├── native/ # Mobile application (React Native, Expo)",
|
||||
);
|
||||
}
|
||||
|
||||
if (addons.includes("starlight")) {
|
||||
structure.push(
|
||||
"│ ├── docs/ # Documentation site (Astro Starlight)",
|
||||
);
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
structure.push("├── packages/");
|
||||
structure.push(
|
||||
"│ └── backend/ # Convex backend functions and schema",
|
||||
);
|
||||
} else if (!isBackendNone) {
|
||||
const backendName = backend[0].toUpperCase() + backend.slice(1);
|
||||
const apiName = api !== "none" ? api.toUpperCase() : "";
|
||||
const backendDesc = apiName ? `${backendName}, ${apiName}` : backendName;
|
||||
structure.push(`│ └── server/ # Backend API (${backendDesc})`);
|
||||
}
|
||||
|
||||
return structure.join("\n");
|
||||
}
|
||||
|
||||
function generateFeaturesList(
|
||||
database: Database,
|
||||
auth: boolean,
|
||||
addons: Addons[],
|
||||
orm: ORM,
|
||||
runtime: Runtime,
|
||||
frontend: Frontend[],
|
||||
backend: string,
|
||||
api: API,
|
||||
): string {
|
||||
const isConvex = backend === "convex";
|
||||
const isBackendNone = backend === "none";
|
||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasNative =
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles");
|
||||
const hasNext = frontend.includes("next");
|
||||
const hasTanstackStart = frontend.includes("tanstack-start");
|
||||
const hasSvelte = frontend.includes("svelte");
|
||||
const hasNuxt = frontend.includes("nuxt");
|
||||
const hasSolid = frontend.includes("solid");
|
||||
const hasFrontendNone = frontend.length === 0 || frontend.includes("none");
|
||||
|
||||
const addonsList = [
|
||||
"- **TypeScript** - For type safety and improved developer experience",
|
||||
];
|
||||
|
||||
if (!hasFrontendNone) {
|
||||
if (hasTanstackRouter) {
|
||||
addonsList.push(
|
||||
"- **TanStack Router** - File-based routing with full type safety",
|
||||
);
|
||||
} else if (hasReactRouter) {
|
||||
addonsList.push("- **React Router** - Declarative routing for React");
|
||||
} else if (hasNext) {
|
||||
addonsList.push("- **Next.js** - Full-stack React framework");
|
||||
} else if (hasTanstackStart) {
|
||||
addonsList.push(
|
||||
"- **TanStack Start** - SSR framework with TanStack Router",
|
||||
);
|
||||
} else if (hasSvelte) {
|
||||
addonsList.push(
|
||||
"- **SvelteKit** - Web framework for building Svelte apps",
|
||||
);
|
||||
} else if (hasNuxt) {
|
||||
addonsList.push("- **Nuxt** - The Intuitive Vue Framework");
|
||||
} else if (hasSolid) {
|
||||
addonsList.push("- **SolidJS** - Simple and performant reactivity");
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNative) {
|
||||
addonsList.push("- **React Native** - Build mobile apps using React");
|
||||
addonsList.push("- **Expo** - Tools for React Native development");
|
||||
}
|
||||
|
||||
if (!hasFrontendNone) {
|
||||
addonsList.push(
|
||||
"- **TailwindCSS** - Utility-first CSS for rapid UI development",
|
||||
"- **shadcn/ui** - Reusable UI components",
|
||||
);
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
addonsList.push("- **Convex** - Reactive backend-as-a-service platform");
|
||||
} else if (!isBackendNone) {
|
||||
if (backend === "hono") {
|
||||
addonsList.push("- **Hono** - Lightweight, performant server framework");
|
||||
} else if (backend === "express") {
|
||||
addonsList.push("- **Express** - Fast, unopinionated web framework");
|
||||
} else if (backend === "fastify") {
|
||||
addonsList.push("- **Fastify** - Fast, low-overhead web framework");
|
||||
} else if (backend === "elysia") {
|
||||
addonsList.push("- **Elysia** - Type-safe, high-performance framework");
|
||||
} else if (backend === "next") {
|
||||
addonsList.push("- **Next.js** - Full-stack React framework");
|
||||
}
|
||||
|
||||
if (api === "trpc") {
|
||||
addonsList.push("- **tRPC** - End-to-end type-safe APIs");
|
||||
} else if (api === "orpc") {
|
||||
addonsList.push(
|
||||
"- **oRPC** - End-to-end type-safe APIs with OpenAPI integration",
|
||||
);
|
||||
}
|
||||
|
||||
if (runtime !== "none") {
|
||||
addonsList.push(
|
||||
`- **${
|
||||
runtime === "bun" ? "Bun" : runtime === "node" ? "Node.js" : runtime
|
||||
}** - Runtime environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (database !== "none" && !isConvex) {
|
||||
const ormName =
|
||||
orm === "drizzle"
|
||||
? "Drizzle"
|
||||
: orm === "prisma"
|
||||
? "Prisma"
|
||||
: orm === "mongoose"
|
||||
? "Mongoose"
|
||||
: "ORM";
|
||||
const dbName =
|
||||
database === "sqlite"
|
||||
? "SQLite/Turso"
|
||||
: database === "postgres"
|
||||
? "PostgreSQL"
|
||||
: database === "mysql"
|
||||
? "MySQL"
|
||||
: database === "mongodb"
|
||||
? "MongoDB"
|
||||
: "Database";
|
||||
|
||||
addonsList.push(
|
||||
`- **${ormName}** - TypeScript-first ORM`,
|
||||
`- **${dbName}** - Database engine`,
|
||||
);
|
||||
}
|
||||
|
||||
if (auth && !isConvex) {
|
||||
addonsList.push(
|
||||
"- **Authentication** - Email & password authentication with Better Auth",
|
||||
);
|
||||
}
|
||||
|
||||
for (const addon of addons) {
|
||||
if (addon === "pwa") {
|
||||
addonsList.push("- **PWA** - Progressive Web App support");
|
||||
} else if (addon === "tauri") {
|
||||
addonsList.push("- **Tauri** - Build native desktop applications");
|
||||
} else if (addon === "biome") {
|
||||
addonsList.push("- **Biome** - Linting and formatting");
|
||||
} else if (addon === "husky") {
|
||||
addonsList.push("- **Husky** - Git hooks for code quality");
|
||||
} else if (addon === "starlight") {
|
||||
addonsList.push("- **Starlight** - Documentation site with Astro");
|
||||
} else if (addon === "turborepo") {
|
||||
addonsList.push("- **Turborepo** - Optimized monorepo build system");
|
||||
}
|
||||
}
|
||||
|
||||
return addonsList.join("\n");
|
||||
}
|
||||
|
||||
function generateDatabaseSetup(
|
||||
database: Database,
|
||||
_auth: boolean,
|
||||
packageManagerRunCmd: string,
|
||||
orm: ORM,
|
||||
dbSetup: DatabaseSetup,
|
||||
): string {
|
||||
if (database === "none") {
|
||||
return "";
|
||||
}
|
||||
|
||||
let setup = "## Database Setup\n\n";
|
||||
|
||||
if (database === "sqlite") {
|
||||
setup += `This project uses SQLite${
|
||||
orm === "drizzle"
|
||||
? " with Drizzle ORM"
|
||||
: orm === "prisma"
|
||||
? " with Prisma"
|
||||
: ` with ${orm}`
|
||||
}.
|
||||
|
||||
1. Start the local SQLite database:
|
||||
${
|
||||
dbSetup === "d1"
|
||||
? "Local development for a Cloudflare D1 database will already be running as part of the `wrangler dev` command."
|
||||
: `\`\`\`bash
|
||||
cd apps/server && ${packageManagerRunCmd} db:local
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
2. Update your \`.env\` file in the \`apps/server\` directory with the appropriate connection details if needed.
|
||||
`;
|
||||
} else if (database === "postgres") {
|
||||
setup += `This project uses PostgreSQL${
|
||||
orm === "drizzle"
|
||||
? " with Drizzle ORM"
|
||||
: orm === "prisma"
|
||||
? " with Prisma"
|
||||
: ` with ${orm}`
|
||||
}.
|
||||
|
||||
1. Make sure you have a PostgreSQL database set up.
|
||||
2. Update your \`apps/server/.env\` file with your PostgreSQL connection details.
|
||||
`;
|
||||
} else if (database === "mysql") {
|
||||
setup += `This project uses MySQL${
|
||||
orm === "drizzle"
|
||||
? " with Drizzle ORM"
|
||||
: orm === "prisma"
|
||||
? " with Prisma"
|
||||
: ` with ${orm}`
|
||||
}.
|
||||
|
||||
1. Make sure you have a MySQL database set up.
|
||||
2. Update your \`apps/server/.env\` file with your MySQL connection details.
|
||||
`;
|
||||
} else if (database === "mongodb") {
|
||||
setup += `This project uses MongoDB ${
|
||||
orm === "mongoose"
|
||||
? "with Mongoose"
|
||||
: orm === "prisma"
|
||||
? "with Prisma ORM"
|
||||
: `with ${orm}`
|
||||
}.
|
||||
|
||||
1. Make sure you have MongoDB set up.
|
||||
2. Update your \`apps/server/.env\` file with your MongoDB connection URI.
|
||||
`;
|
||||
}
|
||||
|
||||
setup += `
|
||||
3. ${
|
||||
orm === "prisma"
|
||||
? `Generate the Prisma client and push the schema:
|
||||
\`\`\`bash
|
||||
${packageManagerRunCmd} db:push
|
||||
\`\`\``
|
||||
: orm === "drizzle"
|
||||
? `Apply the schema to your database:
|
||||
\`\`\`bash
|
||||
${packageManagerRunCmd} db:push
|
||||
\`\`\``
|
||||
: `Apply the schema to your database:
|
||||
\`\`\`bash
|
||||
${packageManagerRunCmd} db:push
|
||||
\`\`\``
|
||||
}
|
||||
`;
|
||||
|
||||
return setup;
|
||||
}
|
||||
|
||||
function generateScriptsList(
|
||||
packageManagerRunCmd: string,
|
||||
database: Database,
|
||||
orm: ORM,
|
||||
_auth: boolean,
|
||||
hasNative: boolean,
|
||||
addons: Addons[],
|
||||
backend: string,
|
||||
): string {
|
||||
const isConvex = backend === "convex";
|
||||
const isBackendNone = backend === "none";
|
||||
|
||||
let scripts = `- \`${packageManagerRunCmd} dev\`: Start all applications in development mode
|
||||
- \`${packageManagerRunCmd} build\`: Build all applications`;
|
||||
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} dev:web\`: Start only the web application`;
|
||||
|
||||
if (isConvex) {
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} dev:setup\`: Setup and configure your Convex project`;
|
||||
} else if (!isBackendNone) {
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} dev:server\`: Start only the server`;
|
||||
}
|
||||
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} check-types\`: Check TypeScript types across all apps`;
|
||||
|
||||
if (hasNative) {
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} dev:native\`: Start the React Native/Expo development server`;
|
||||
}
|
||||
|
||||
if (database !== "none" && !isConvex) {
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} db:push\`: Push schema changes to database
|
||||
- \`${packageManagerRunCmd} db:studio\`: Open database studio UI`;
|
||||
|
||||
if (database === "sqlite" && orm === "drizzle") {
|
||||
scripts += `
|
||||
- \`cd apps/server && ${packageManagerRunCmd} db:local\`: Start the local SQLite database`;
|
||||
}
|
||||
}
|
||||
|
||||
if (addons.includes("biome")) {
|
||||
scripts += `
|
||||
- \`${packageManagerRunCmd} check\`: Run Biome formatting and linting`;
|
||||
}
|
||||
|
||||
if (addons.includes("pwa")) {
|
||||
scripts += `
|
||||
- \`cd apps/web && ${packageManagerRunCmd} generate-pwa-assets\`: Generate PWA assets`;
|
||||
}
|
||||
|
||||
if (addons.includes("tauri")) {
|
||||
scripts += `
|
||||
- \`cd apps/web && ${packageManagerRunCmd} desktop:dev\`: Start Tauri desktop app in development
|
||||
- \`cd apps/web && ${packageManagerRunCmd} desktop:build\`: Build Tauri desktop app`;
|
||||
}
|
||||
|
||||
if (addons.includes("starlight")) {
|
||||
scripts += `
|
||||
- \`cd apps/docs && ${packageManagerRunCmd} dev\`: Start documentation site
|
||||
- \`cd apps/docs && ${packageManagerRunCmd} build\`: Build documentation site`;
|
||||
}
|
||||
|
||||
return scripts;
|
||||
}
|
||||
103
apps/cli/src/helpers/core/db-setup.ts
Normal file
103
apps/cli/src/helpers/core/db-setup.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import path from "node:path";
|
||||
import { spinner } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { setupCloudflareD1 } from "../database-providers/d1-setup";
|
||||
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
|
||||
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
||||
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
||||
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
|
||||
import { setupSupabase } from "../database-providers/supabase-setup";
|
||||
import { setupTurso } from "../database-providers/turso-setup";
|
||||
|
||||
export async function setupDatabase(config: ProjectConfig) {
|
||||
const { database, orm, dbSetup, backend, projectDir } = config;
|
||||
|
||||
if (backend === "convex" || database === "none") {
|
||||
if (backend !== "convex") {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const serverDbDir = path.join(serverDir, "src/db");
|
||||
if (await fs.pathExists(serverDbDir)) {
|
||||
await fs.remove(serverDbDir);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const s = spinner();
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
if (!(await fs.pathExists(serverDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (orm === "prisma") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@prisma/client"],
|
||||
devDependencies: ["prisma"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else if (orm === "drizzle") {
|
||||
if (database === "sqlite") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["drizzle-orm", "@libsql/client"],
|
||||
devDependencies: ["drizzle-kit"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else if (database === "postgres") {
|
||||
if (dbSetup === "neon") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["drizzle-orm", "@neondatabase/serverless", "ws"],
|
||||
devDependencies: ["drizzle-kit", "@types/ws"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else {
|
||||
await addPackageDependency({
|
||||
dependencies: ["drizzle-orm", "pg"],
|
||||
devDependencies: ["drizzle-kit", "@types/pg"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
} else if (database === "mysql") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["drizzle-orm", "mysql2"],
|
||||
devDependencies: ["drizzle-kit"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
} else if (orm === "mongoose") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["mongoose"],
|
||||
devDependencies: [],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (dbSetup === "docker") {
|
||||
await setupDockerCompose(config);
|
||||
} else if (database === "sqlite" && dbSetup === "turso") {
|
||||
await setupTurso(config);
|
||||
} else if (database === "sqlite" && dbSetup === "d1") {
|
||||
await setupCloudflareD1(config);
|
||||
} else if (database === "postgres") {
|
||||
if (dbSetup === "prisma-postgres") {
|
||||
await setupPrismaPostgres(config);
|
||||
} else if (dbSetup === "neon") {
|
||||
await setupNeonPostgres(config);
|
||||
} else if (dbSetup === "supabase") {
|
||||
await setupSupabase(config);
|
||||
}
|
||||
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
||||
await setupMongoDBAtlas(config);
|
||||
}
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Failed to set up database"));
|
||||
if (error instanceof Error) {
|
||||
consola.error(pc.red(error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
43
apps/cli/src/helpers/core/detect-project-config.ts
Normal file
43
apps/cli/src/helpers/core/detect-project-config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { readBtsConfig } from "../../utils/bts-config";
|
||||
|
||||
export async function detectProjectConfig(
|
||||
projectDir: string,
|
||||
): Promise<Partial<ProjectConfig> | null> {
|
||||
try {
|
||||
const btsConfig = await readBtsConfig(projectDir);
|
||||
if (btsConfig) {
|
||||
return {
|
||||
projectDir,
|
||||
projectName: path.basename(projectDir),
|
||||
database: btsConfig.database,
|
||||
orm: btsConfig.orm,
|
||||
backend: btsConfig.backend,
|
||||
runtime: btsConfig.runtime,
|
||||
frontend: btsConfig.frontend,
|
||||
addons: btsConfig.addons,
|
||||
examples: btsConfig.examples,
|
||||
auth: btsConfig.auth,
|
||||
packageManager: btsConfig.packageManager,
|
||||
dbSetup: btsConfig.dbSetup,
|
||||
api: btsConfig.api,
|
||||
webDeploy: btsConfig.webDeploy,
|
||||
serverDeploy: btsConfig.serverDeploy,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isBetterTStackProject(projectDir: string) {
|
||||
try {
|
||||
return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
298
apps/cli/src/helpers/core/env-setup.ts
Normal file
298
apps/cli/src/helpers/core/env-setup.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { generateAuthSecret } from "../addons/auth-setup";
|
||||
|
||||
export interface EnvVariable {
|
||||
key: string;
|
||||
value: string | null | undefined;
|
||||
condition: boolean;
|
||||
}
|
||||
|
||||
export async function addEnvVariablesToFile(
|
||||
filePath: string,
|
||||
variables: EnvVariable[],
|
||||
) {
|
||||
await fs.ensureDir(path.dirname(filePath));
|
||||
|
||||
let envContent = "";
|
||||
if (await fs.pathExists(filePath)) {
|
||||
envContent = await fs.readFile(filePath, "utf8");
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
let contentToAdd = "";
|
||||
const exampleVariables: string[] = [];
|
||||
|
||||
for (const { key, value, condition } of variables) {
|
||||
if (condition) {
|
||||
const regex = new RegExp(`^${key}=.*$`, "m");
|
||||
const valueToWrite = value ?? "";
|
||||
exampleVariables.push(`${key}=`);
|
||||
|
||||
if (regex.test(envContent)) {
|
||||
const existingMatch = envContent.match(regex);
|
||||
if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
|
||||
envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
|
||||
modified = true;
|
||||
}
|
||||
} else {
|
||||
contentToAdd += `${key}=${valueToWrite}\n`;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contentToAdd) {
|
||||
if (envContent.length > 0 && !envContent.endsWith("\n")) {
|
||||
envContent += "\n";
|
||||
}
|
||||
envContent += contentToAdd;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
await fs.writeFile(filePath, envContent.trimEnd());
|
||||
}
|
||||
|
||||
const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
|
||||
let exampleEnvContent = "";
|
||||
if (await fs.pathExists(exampleFilePath)) {
|
||||
exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
|
||||
}
|
||||
|
||||
let exampleModified = false;
|
||||
let exampleContentToAdd = "";
|
||||
|
||||
for (const exampleVar of exampleVariables) {
|
||||
const key = exampleVar.split("=")[0];
|
||||
const regex = new RegExp(`^${key}=.*$`, "m");
|
||||
if (!regex.test(exampleEnvContent)) {
|
||||
exampleContentToAdd += `${exampleVar}\n`;
|
||||
exampleModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (exampleContentToAdd) {
|
||||
if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) {
|
||||
exampleEnvContent += "\n";
|
||||
}
|
||||
exampleEnvContent += exampleContentToAdd;
|
||||
}
|
||||
|
||||
if (exampleModified || !(await fs.pathExists(exampleFilePath))) {
|
||||
await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupEnvironmentVariables(config: ProjectConfig) {
|
||||
const {
|
||||
backend,
|
||||
frontend,
|
||||
database,
|
||||
auth,
|
||||
examples,
|
||||
dbSetup,
|
||||
projectDir,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
} = config;
|
||||
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasTanStackRouter = frontend.includes("tanstack-router");
|
||||
const hasTanStackStart = frontend.includes("tanstack-start");
|
||||
const hasNextJs = frontend.includes("next");
|
||||
const hasNuxt = frontend.includes("nuxt");
|
||||
const hasSvelte = frontend.includes("svelte");
|
||||
const hasSolid = frontend.includes("solid");
|
||||
const hasWebFrontend =
|
||||
hasReactRouter ||
|
||||
hasTanStackRouter ||
|
||||
hasTanStackStart ||
|
||||
hasNextJs ||
|
||||
hasNuxt ||
|
||||
hasSolid ||
|
||||
hasSvelte;
|
||||
|
||||
if (hasWebFrontend) {
|
||||
const clientDir = path.join(projectDir, "apps/web");
|
||||
if (await fs.pathExists(clientDir)) {
|
||||
let envVarName = "VITE_SERVER_URL";
|
||||
let serverUrl = "http://localhost:3000";
|
||||
|
||||
if (hasNextJs) {
|
||||
envVarName = "NEXT_PUBLIC_SERVER_URL";
|
||||
} else if (hasNuxt) {
|
||||
envVarName = "NUXT_PUBLIC_SERVER_URL";
|
||||
} else if (hasSvelte) {
|
||||
envVarName = "PUBLIC_SERVER_URL";
|
||||
}
|
||||
|
||||
if (backend === "convex") {
|
||||
if (hasNextJs) envVarName = "NEXT_PUBLIC_CONVEX_URL";
|
||||
else if (hasNuxt) envVarName = "NUXT_PUBLIC_CONVEX_URL";
|
||||
else if (hasSvelte) envVarName = "PUBLIC_CONVEX_URL";
|
||||
else envVarName = "VITE_CONVEX_URL";
|
||||
|
||||
serverUrl = "https://<YOUR_CONVEX_URL>";
|
||||
}
|
||||
|
||||
const clientVars: EnvVariable[] = [
|
||||
{
|
||||
key: envVarName,
|
||||
value: serverUrl,
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles")
|
||||
) {
|
||||
const nativeDir = path.join(projectDir, "apps/native");
|
||||
if (await fs.pathExists(nativeDir)) {
|
||||
let envVarName = "EXPO_PUBLIC_SERVER_URL";
|
||||
let serverUrl = "http://localhost:3000";
|
||||
|
||||
if (backend === "convex") {
|
||||
envVarName = "EXPO_PUBLIC_CONVEX_URL";
|
||||
serverUrl = "https://<YOUR_CONVEX_URL>";
|
||||
}
|
||||
|
||||
const nativeVars: EnvVariable[] = [
|
||||
{
|
||||
key: envVarName,
|
||||
value: serverUrl,
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
||||
}
|
||||
}
|
||||
|
||||
if (backend === "convex") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
if (!(await fs.pathExists(serverDir))) {
|
||||
return;
|
||||
}
|
||||
const envPath = path.join(serverDir, ".env");
|
||||
|
||||
let corsOrigin = "http://localhost:3001";
|
||||
if (hasReactRouter || hasSvelte) {
|
||||
corsOrigin = "http://localhost:5173";
|
||||
}
|
||||
|
||||
let databaseUrl: string | null = null;
|
||||
const specializedSetup =
|
||||
dbSetup === "turso" ||
|
||||
dbSetup === "prisma-postgres" ||
|
||||
dbSetup === "mongodb-atlas" ||
|
||||
dbSetup === "neon" ||
|
||||
dbSetup === "supabase" ||
|
||||
dbSetup === "d1" ||
|
||||
dbSetup === "docker";
|
||||
|
||||
if (database !== "none" && !specializedSetup) {
|
||||
switch (database) {
|
||||
case "postgres":
|
||||
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
|
||||
break;
|
||||
case "mysql":
|
||||
databaseUrl = "mysql://root:password@localhost:3306/mydb";
|
||||
break;
|
||||
case "mongodb":
|
||||
databaseUrl = "mongodb://localhost:27017/mydatabase";
|
||||
break;
|
||||
case "sqlite":
|
||||
if (config.runtime === "workers") {
|
||||
databaseUrl = "http://127.0.0.1:8080";
|
||||
} else {
|
||||
databaseUrl = "file:./local.db";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const serverVars: EnvVariable[] = [
|
||||
{
|
||||
key: "CORS_ORIGIN",
|
||||
value: corsOrigin,
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "BETTER_AUTH_SECRET",
|
||||
value: generateAuthSecret(),
|
||||
condition: !!auth,
|
||||
},
|
||||
{
|
||||
key: "BETTER_AUTH_URL",
|
||||
value: "http://localhost:3000",
|
||||
condition: !!auth,
|
||||
},
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value: databaseUrl,
|
||||
condition: database !== "none" && !specializedSetup,
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
value: "",
|
||||
condition: examples?.includes("ai") || false,
|
||||
},
|
||||
];
|
||||
|
||||
await addEnvVariablesToFile(envPath, serverVars);
|
||||
|
||||
const isUnifiedAlchemy =
|
||||
webDeploy === "alchemy" && serverDeploy === "alchemy";
|
||||
const isIndividualAlchemy =
|
||||
webDeploy === "alchemy" || serverDeploy === "alchemy";
|
||||
|
||||
if (isUnifiedAlchemy) {
|
||||
const rootEnvPath = path.join(projectDir, ".env");
|
||||
const rootAlchemyVars: EnvVariable[] = [
|
||||
{
|
||||
key: "ALCHEMY_PASSWORD",
|
||||
value: "please-change-this",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
||||
} else if (isIndividualAlchemy) {
|
||||
if (webDeploy === "alchemy") {
|
||||
const webDir = path.join(projectDir, "apps/web");
|
||||
if (await fs.pathExists(webDir)) {
|
||||
const webAlchemyVars: EnvVariable[] = [
|
||||
{
|
||||
key: "ALCHEMY_PASSWORD",
|
||||
value: "please-change-this",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
||||
}
|
||||
}
|
||||
|
||||
if (serverDeploy === "alchemy") {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverDir)) {
|
||||
const serverAlchemyVars: EnvVariable[] = [
|
||||
{
|
||||
key: "ALCHEMY_PASSWORD",
|
||||
value: "please-change-this",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(
|
||||
path.join(serverDir, ".env"),
|
||||
serverAlchemyVars,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
apps/cli/src/helpers/core/git.ts
Normal file
31
apps/cli/src/helpers/core/git.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
|
||||
export async function initializeGit(projectDir: string, useGit: boolean) {
|
||||
if (!useGit) return;
|
||||
|
||||
const gitVersionResult = await $({
|
||||
cwd: projectDir,
|
||||
reject: false,
|
||||
stderr: "pipe",
|
||||
})`git --version`;
|
||||
|
||||
if (gitVersionResult.exitCode !== 0) {
|
||||
log.warn(pc.yellow("Git is not installed"));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await $({
|
||||
cwd: projectDir,
|
||||
reject: false,
|
||||
stderr: "pipe",
|
||||
})`git init`;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Git initialization failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
await $({ cwd: projectDir })`git add -A`;
|
||||
await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
|
||||
}
|
||||
32
apps/cli/src/helpers/core/install-dependencies.ts
Normal file
32
apps/cli/src/helpers/core/install-dependencies.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { spinner } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { Addons, PackageManager } from "../../types";
|
||||
|
||||
export async function installDependencies({
|
||||
projectDir,
|
||||
packageManager,
|
||||
}: {
|
||||
projectDir: string;
|
||||
packageManager: PackageManager;
|
||||
addons?: Addons[];
|
||||
}) {
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
s.start(`Running ${packageManager} install...`);
|
||||
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} install`;
|
||||
|
||||
s.stop("Dependencies installed successfully");
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Failed to install dependencies"));
|
||||
if (error instanceof Error) {
|
||||
consola.error(pc.red(`Installation error: ${error.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
410
apps/cli/src/helpers/core/post-installation.ts
Normal file
410
apps/cli/src/helpers/core/post-installation.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { consola } from "consola";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
ORM,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
} from "../../types";
|
||||
import { getDockerStatus } from "../../utils/docker-utils";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
|
||||
export async function displayPostInstallInstructions(
|
||||
config: ProjectConfig & { depsInstalled: boolean },
|
||||
) {
|
||||
const {
|
||||
database,
|
||||
relativePath,
|
||||
packageManager,
|
||||
depsInstalled,
|
||||
orm,
|
||||
addons,
|
||||
runtime,
|
||||
frontend,
|
||||
backend,
|
||||
dbSetup,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
} = config;
|
||||
|
||||
const isConvex = backend === "convex";
|
||||
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
||||
const cdCmd = `cd ${relativePath}`;
|
||||
const hasHuskyOrBiome =
|
||||
addons?.includes("husky") || addons?.includes("biome");
|
||||
|
||||
const databaseInstructions =
|
||||
!isConvex && database !== "none"
|
||||
? await getDatabaseInstructions(
|
||||
database,
|
||||
orm,
|
||||
runCmd,
|
||||
runtime,
|
||||
dbSetup,
|
||||
serverDeploy,
|
||||
)
|
||||
: "";
|
||||
|
||||
const tauriInstructions = addons?.includes("tauri")
|
||||
? getTauriInstructions(runCmd)
|
||||
: "";
|
||||
const lintingInstructions = hasHuskyOrBiome
|
||||
? getLintingInstructions(runCmd)
|
||||
: "";
|
||||
const nativeInstructions =
|
||||
frontend?.includes("native-nativewind") ||
|
||||
frontend?.includes("native-unistyles")
|
||||
? getNativeInstructions(isConvex)
|
||||
: "";
|
||||
const pwaInstructions =
|
||||
addons?.includes("pwa") && frontend?.includes("react-router")
|
||||
? getPwaInstructions()
|
||||
: "";
|
||||
const starlightInstructions = addons?.includes("starlight")
|
||||
? getStarlightInstructions(runCmd)
|
||||
: "";
|
||||
const wranglerDeployInstructions = getWranglerDeployInstructions(
|
||||
runCmd,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
);
|
||||
const alchemyDeployInstructions = getAlchemyDeployInstructions(
|
||||
runCmd,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
);
|
||||
|
||||
const hasWeb = frontend?.some((f) =>
|
||||
[
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"next",
|
||||
"tanstack-start",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
"solid",
|
||||
].includes(f),
|
||||
);
|
||||
const hasNative =
|
||||
frontend?.includes("native-nativewind") ||
|
||||
frontend?.includes("native-unistyles");
|
||||
|
||||
const bunWebNativeWarning =
|
||||
packageManager === "bun" && hasNative && hasWeb
|
||||
? getBunWebNativeWarning()
|
||||
: "";
|
||||
const noOrmWarning =
|
||||
!isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
|
||||
|
||||
const hasReactRouter = frontend?.includes("react-router");
|
||||
const hasSvelte = frontend?.includes("svelte");
|
||||
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
|
||||
|
||||
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
|
||||
|
||||
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
||||
let stepCounter = 2;
|
||||
|
||||
if (!depsInstalled) {
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(
|
||||
" (this will guide you through Convex project setup)",
|
||||
)}\n`;
|
||||
output += `${pc.cyan(
|
||||
`${stepCounter++}.`,
|
||||
)} Copy environment variables from\n${pc.white(
|
||||
" packages/backend/.env.local",
|
||||
)} to ${pc.white("apps/*/.env")}\n`;
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
|
||||
} else {
|
||||
if (runtime !== "workers") {
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
||||
}
|
||||
|
||||
if (runtime === "workers") {
|
||||
if (dbSetup === "d1") {
|
||||
output += `${pc.yellow(
|
||||
"IMPORTANT:",
|
||||
)} Complete D1 database setup first\n (see Database commands below)\n`;
|
||||
}
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
||||
if (serverDeploy === "wrangler") {
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output += `${pc.bold("Your project will be available at:")}\n`;
|
||||
|
||||
if (hasWeb) {
|
||||
output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
|
||||
} else if (!hasNative && !addons?.includes("starlight")) {
|
||||
output += `${pc.yellow(
|
||||
"NOTE:",
|
||||
)} You are creating a backend-only app\n (no frontend selected)\n`;
|
||||
}
|
||||
|
||||
if (!isConvex) {
|
||||
output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
|
||||
}
|
||||
|
||||
if (addons?.includes("starlight")) {
|
||||
output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
||||
}
|
||||
|
||||
if (addons?.includes("fumadocs")) {
|
||||
output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
||||
}
|
||||
|
||||
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
||||
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
||||
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
||||
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
||||
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
||||
if (wranglerDeployInstructions)
|
||||
output += `\n${wranglerDeployInstructions.trim()}\n`;
|
||||
if (alchemyDeployInstructions)
|
||||
output += `\n${alchemyDeployInstructions.trim()}\n`;
|
||||
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
||||
|
||||
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
||||
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
||||
|
||||
output += `\n${pc.bold("Update all dependencies:\n")}${pc.cyan(
|
||||
tazeCommand,
|
||||
)}\n\n`;
|
||||
output += `${pc.bold(
|
||||
"Like Better-T Stack?",
|
||||
)} Please consider giving us a star\n on GitHub:\n`;
|
||||
output += pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack");
|
||||
|
||||
consola.box(output);
|
||||
}
|
||||
|
||||
function getNativeInstructions(isConvex: boolean): string {
|
||||
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
|
||||
const exampleUrl = isConvex
|
||||
? "https://<YOUR_CONVEX_URL>"
|
||||
: "http://<YOUR_LOCAL_IP>:3000";
|
||||
const envFileName = ".env";
|
||||
const ipNote = isConvex
|
||||
? "your Convex deployment URL (find after running 'dev:setup')"
|
||||
: "your local IP address";
|
||||
|
||||
let instructions = `${pc.yellow(
|
||||
"NOTE:",
|
||||
)} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
|
||||
|
||||
if (isConvex) {
|
||||
instructions += `\n${pc.yellow(
|
||||
"IMPORTANT:",
|
||||
)} When using local development with Convex and native apps,\n ensure you use your local IP address instead of localhost or 127.0.0.1\n for proper connectivity.\n`;
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
function getLintingInstructions(runCmd?: string): string {
|
||||
return `${pc.bold("Linting and formatting:")}\n${pc.cyan(
|
||||
"•",
|
||||
)} Format and lint fix: ${`${runCmd} check`}\n`;
|
||||
}
|
||||
|
||||
async function getDatabaseInstructions(
|
||||
database: Database,
|
||||
orm?: ORM,
|
||||
runCmd?: string,
|
||||
_runtime?: Runtime,
|
||||
dbSetup?: DatabaseSetup,
|
||||
serverDeploy?: string,
|
||||
): Promise<string> {
|
||||
const instructions: string[] = [];
|
||||
|
||||
if (dbSetup === "docker") {
|
||||
const dockerStatus = await getDockerStatus(database);
|
||||
|
||||
if (dockerStatus.message) {
|
||||
instructions.push(dockerStatus.message);
|
||||
instructions.push("");
|
||||
}
|
||||
}
|
||||
|
||||
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
||||
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
||||
|
||||
instructions.push(
|
||||
`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(
|
||||
`${packageManager} wrangler login`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("2.")} Create D1 database: ${pc.white(
|
||||
`${packageManager} wrangler d1 create your-database-name`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan(
|
||||
"3.",
|
||||
)} Update apps/server/wrangler.jsonc with database_id and database_name`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("4.")} Generate migrations: ${pc.white(
|
||||
`cd apps/server && ${packageManager} db:generate`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("5.")} Apply migrations locally: ${pc.white(
|
||||
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("6.")} Apply migrations to production: ${pc.white(
|
||||
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dbSetup === "d1" && serverDeploy === "alchemy") {
|
||||
}
|
||||
|
||||
if (orm === "prisma") {
|
||||
if (dbSetup === "turso") {
|
||||
instructions.push(
|
||||
`${pc.yellow(
|
||||
"NOTE:",
|
||||
)} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`,
|
||||
);
|
||||
}
|
||||
|
||||
if (database === "mongodb" && dbSetup === "docker") {
|
||||
instructions.push(
|
||||
`${pc.yellow(
|
||||
"WARNING:",
|
||||
)} Prisma + MongoDB + Docker combination\n may not work.`,
|
||||
);
|
||||
}
|
||||
if (dbSetup === "docker") {
|
||||
instructions.push(
|
||||
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
|
||||
);
|
||||
}
|
||||
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
||||
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
||||
} else if (orm === "drizzle") {
|
||||
if (dbSetup === "docker") {
|
||||
instructions.push(
|
||||
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
|
||||
);
|
||||
}
|
||||
if (dbSetup !== "d1") {
|
||||
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
||||
}
|
||||
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
||||
if (database === "sqlite" && dbSetup !== "d1") {
|
||||
instructions.push(
|
||||
`${pc.cyan(
|
||||
"•",
|
||||
)} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`,
|
||||
);
|
||||
}
|
||||
} else if (orm === "mongoose") {
|
||||
if (dbSetup === "docker") {
|
||||
instructions.push(
|
||||
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
|
||||
);
|
||||
}
|
||||
} else if (orm === "none") {
|
||||
instructions.push(
|
||||
`${pc.yellow("NOTE:")} Manual database schema setup\n required.`,
|
||||
);
|
||||
}
|
||||
|
||||
return instructions.length
|
||||
? `${pc.bold("Database commands:")}\n${instructions.join("\n")}`
|
||||
: "";
|
||||
}
|
||||
|
||||
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.\n See: ${"https://v2.tauri.app/start/prerequisites/"}`;
|
||||
}
|
||||
|
||||
function getPwaInstructions(): string {
|
||||
return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
|
||||
"NOTE:",
|
||||
)} There is a known compatibility issue between VitePWA\n and React Router v7. See:\n https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
|
||||
}
|
||||
|
||||
function getStarlightInstructions(runCmd?: string): string {
|
||||
return `\n${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`}`;
|
||||
}
|
||||
|
||||
function getNoOrmWarning(): string {
|
||||
return `\n${pc.yellow(
|
||||
"WARNING:",
|
||||
)} Database selected without an ORM. Features requiring\n database access (e.g., examples, auth) need manual setup.`;
|
||||
}
|
||||
|
||||
function getBunWebNativeWarning(): string {
|
||||
return `\n${pc.yellow(
|
||||
"WARNING:",
|
||||
)} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
||||
}
|
||||
|
||||
function getWranglerDeployInstructions(
|
||||
runCmd?: string,
|
||||
webDeploy?: string,
|
||||
serverDeploy?: string,
|
||||
): string {
|
||||
const instructions: string[] = [];
|
||||
|
||||
if (webDeploy === "wrangler") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`,
|
||||
);
|
||||
}
|
||||
if (serverDeploy === "wrangler") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`,
|
||||
);
|
||||
}
|
||||
|
||||
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
||||
}
|
||||
|
||||
function getAlchemyDeployInstructions(
|
||||
runCmd?: string,
|
||||
webDeploy?: string,
|
||||
serverDeploy?: string,
|
||||
): string {
|
||||
const instructions: string[] = [];
|
||||
|
||||
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`,
|
||||
);
|
||||
} else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`,
|
||||
);
|
||||
} else if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`,
|
||||
);
|
||||
}
|
||||
|
||||
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
||||
}
|
||||
294
apps/cli/src/helpers/core/project-config.ts
Normal file
294
apps/cli/src/helpers/core/project-config.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function updatePackageConfigurations(
|
||||
projectDir: string,
|
||||
options: ProjectConfig,
|
||||
) {
|
||||
await updateRootPackageJson(projectDir, options);
|
||||
if (options.backend !== "convex") {
|
||||
await updateServerPackageJson(projectDir, options);
|
||||
} else {
|
||||
await updateConvexPackageJson(projectDir, options);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRootPackageJson(
|
||||
projectDir: string,
|
||||
options: ProjectConfig,
|
||||
) {
|
||||
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
||||
if (!(await fs.pathExists(rootPackageJsonPath))) return;
|
||||
|
||||
const packageJson = await fs.readJson(rootPackageJsonPath);
|
||||
packageJson.name = options.projectName;
|
||||
|
||||
if (!packageJson.scripts) {
|
||||
packageJson.scripts = {};
|
||||
}
|
||||
const scripts = packageJson.scripts;
|
||||
|
||||
const backendPackageName =
|
||||
options.backend === "convex" ? `@${options.projectName}/backend` : "server";
|
||||
|
||||
let serverDevScript = "";
|
||||
if (options.addons.includes("turborepo")) {
|
||||
serverDevScript = `turbo -F ${backendPackageName} dev`;
|
||||
} else if (options.packageManager === "bun") {
|
||||
serverDevScript = `bun run --filter ${backendPackageName} dev`;
|
||||
} else if (options.packageManager === "pnpm") {
|
||||
serverDevScript = `pnpm --filter ${backendPackageName} dev`;
|
||||
} else if (options.packageManager === "npm") {
|
||||
serverDevScript = `npm run dev --workspace ${backendPackageName}`;
|
||||
}
|
||||
|
||||
let devScript = "";
|
||||
if (options.packageManager === "pnpm") {
|
||||
devScript = "pnpm -r dev";
|
||||
} else if (options.packageManager === "npm") {
|
||||
devScript = "npm run dev --workspaces";
|
||||
} else if (options.packageManager === "bun") {
|
||||
devScript = "bun run --filter '*' dev";
|
||||
}
|
||||
|
||||
const needsDbScripts =
|
||||
options.backend !== "convex" &&
|
||||
options.database !== "none" &&
|
||||
options.orm !== "none" &&
|
||||
options.orm !== "mongoose";
|
||||
if (options.addons.includes("turborepo")) {
|
||||
scripts.dev = "turbo dev";
|
||||
scripts.build = "turbo build";
|
||||
scripts["check-types"] = "turbo check-types";
|
||||
scripts["dev:native"] = "turbo -F native dev";
|
||||
scripts["dev:web"] = "turbo -F web dev";
|
||||
scripts["dev:server"] = serverDevScript;
|
||||
if (options.backend === "convex") {
|
||||
scripts["dev:setup"] = `turbo -F ${backendPackageName} dev:setup`;
|
||||
}
|
||||
if (needsDbScripts) {
|
||||
scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
|
||||
scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`;
|
||||
if (options.orm === "prisma") {
|
||||
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
||||
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
||||
} else if (options.orm === "drizzle") {
|
||||
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
|
||||
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
|
||||
}
|
||||
}
|
||||
if (options.dbSetup === "docker") {
|
||||
scripts["db:start"] = `turbo -F ${backendPackageName} db:start`;
|
||||
scripts["db:watch"] = `turbo -F ${backendPackageName} db:watch`;
|
||||
scripts["db:stop"] = `turbo -F ${backendPackageName} db:stop`;
|
||||
scripts["db:down"] = `turbo -F ${backendPackageName} db:down`;
|
||||
}
|
||||
} else if (options.packageManager === "pnpm") {
|
||||
scripts.dev = devScript;
|
||||
scripts.build = "pnpm -r build";
|
||||
scripts["check-types"] = "pnpm -r check-types";
|
||||
scripts["dev:native"] = "pnpm --filter native dev";
|
||||
scripts["dev:web"] = "pnpm --filter web dev";
|
||||
scripts["dev:server"] = serverDevScript;
|
||||
if (options.backend === "convex") {
|
||||
scripts["dev:setup"] = `pnpm --filter ${backendPackageName} dev:setup`;
|
||||
}
|
||||
if (needsDbScripts) {
|
||||
scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
|
||||
scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`;
|
||||
if (options.orm === "prisma") {
|
||||
scripts["db:generate"] =
|
||||
`pnpm --filter ${backendPackageName} db:generate`;
|
||||
scripts["db:migrate"] =
|
||||
`pnpm --filter ${backendPackageName} db:migrate`;
|
||||
} else if (options.orm === "drizzle") {
|
||||
scripts["db:generate"] =
|
||||
`pnpm --filter ${backendPackageName} db:generate`;
|
||||
scripts["db:migrate"] =
|
||||
`pnpm --filter ${backendPackageName} db:migrate`;
|
||||
}
|
||||
}
|
||||
if (options.dbSetup === "docker") {
|
||||
scripts["db:start"] = `pnpm --filter ${backendPackageName} db:start`;
|
||||
scripts["db:watch"] = `pnpm --filter ${backendPackageName} db:watch`;
|
||||
scripts["db:stop"] = `pnpm --filter ${backendPackageName} db:stop`;
|
||||
scripts["db:down"] = `pnpm --filter ${backendPackageName} db:down`;
|
||||
}
|
||||
} else if (options.packageManager === "npm") {
|
||||
scripts.dev = devScript;
|
||||
scripts.build = "npm run build --workspaces";
|
||||
scripts["check-types"] = "npm run check-types --workspaces";
|
||||
scripts["dev:native"] = "npm run dev --workspace native";
|
||||
scripts["dev:web"] = "npm run dev --workspace web";
|
||||
scripts["dev:server"] = serverDevScript;
|
||||
if (options.backend === "convex") {
|
||||
scripts["dev:setup"] =
|
||||
`npm run dev:setup --workspace ${backendPackageName}`;
|
||||
}
|
||||
if (needsDbScripts) {
|
||||
scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
|
||||
scripts["db:studio"] =
|
||||
`npm run db:studio --workspace ${backendPackageName}`;
|
||||
if (options.orm === "prisma") {
|
||||
scripts["db:generate"] =
|
||||
`npm run db:generate --workspace ${backendPackageName}`;
|
||||
scripts["db:migrate"] =
|
||||
`npm run db:migrate --workspace ${backendPackageName}`;
|
||||
} else if (options.orm === "drizzle") {
|
||||
scripts["db:generate"] =
|
||||
`npm run db:generate --workspace ${backendPackageName}`;
|
||||
scripts["db:migrate"] =
|
||||
`npm run db:migrate --workspace ${backendPackageName}`;
|
||||
}
|
||||
}
|
||||
if (options.dbSetup === "docker") {
|
||||
scripts["db:start"] =
|
||||
`npm run db:start --workspace ${backendPackageName}`;
|
||||
scripts["db:watch"] =
|
||||
`npm run db:watch --workspace ${backendPackageName}`;
|
||||
scripts["db:stop"] = `npm run db:stop --workspace ${backendPackageName}`;
|
||||
scripts["db:down"] = `npm run db:down --workspace ${backendPackageName}`;
|
||||
}
|
||||
} else if (options.packageManager === "bun") {
|
||||
scripts.dev = devScript;
|
||||
scripts.build = "bun run --filter '*' build";
|
||||
scripts["check-types"] = "bun run --filter '*' check-types";
|
||||
scripts["dev:native"] = "bun run --filter native dev";
|
||||
scripts["dev:web"] = "bun run --filter web dev";
|
||||
scripts["dev:server"] = serverDevScript;
|
||||
if (options.backend === "convex") {
|
||||
scripts["dev:setup"] = `bun run --filter ${backendPackageName} dev:setup`;
|
||||
}
|
||||
if (needsDbScripts) {
|
||||
scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
|
||||
scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`;
|
||||
if (options.orm === "prisma") {
|
||||
scripts["db:generate"] =
|
||||
`bun run --filter ${backendPackageName} db:generate`;
|
||||
scripts["db:migrate"] =
|
||||
`bun run --filter ${backendPackageName} db:migrate`;
|
||||
} else if (options.orm === "drizzle") {
|
||||
scripts["db:generate"] =
|
||||
`bun run --filter ${backendPackageName} db:generate`;
|
||||
scripts["db:migrate"] =
|
||||
`bun run --filter ${backendPackageName} db:migrate`;
|
||||
}
|
||||
}
|
||||
if (options.dbSetup === "docker") {
|
||||
scripts["db:start"] = `bun run --filter ${backendPackageName} db:start`;
|
||||
scripts["db:watch"] = `bun run --filter ${backendPackageName} db:watch`;
|
||||
scripts["db:stop"] = `bun run --filter ${backendPackageName} db:stop`;
|
||||
scripts["db:down"] = `bun run --filter ${backendPackageName} db:down`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execa(options.packageManager, ["-v"], {
|
||||
cwd: projectDir,
|
||||
});
|
||||
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
||||
} catch (_e) {
|
||||
log.warn(`Could not determine ${options.packageManager} version.`);
|
||||
}
|
||||
|
||||
if (!packageJson.workspaces) {
|
||||
packageJson.workspaces = [];
|
||||
}
|
||||
const workspaces = packageJson.workspaces;
|
||||
|
||||
if (options.backend === "convex") {
|
||||
if (!workspaces.includes("packages/*")) {
|
||||
workspaces.push("packages/*");
|
||||
}
|
||||
const needsAppsDir =
|
||||
options.frontend.length > 0 || options.addons.includes("starlight");
|
||||
if (needsAppsDir && !workspaces.includes("apps/*")) {
|
||||
workspaces.push("apps/*");
|
||||
}
|
||||
} else {
|
||||
if (!workspaces.includes("apps/*")) {
|
||||
workspaces.push("apps/*");
|
||||
}
|
||||
if (!workspaces.includes("packages/*")) {
|
||||
workspaces.push("packages/*");
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
async function updateServerPackageJson(
|
||||
projectDir: string,
|
||||
options: ProjectConfig,
|
||||
) {
|
||||
const serverPackageJsonPath = path.join(
|
||||
projectDir,
|
||||
"apps/server/package.json",
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(serverPackageJsonPath))) return;
|
||||
|
||||
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
||||
|
||||
if (!serverPackageJson.scripts) {
|
||||
serverPackageJson.scripts = {};
|
||||
}
|
||||
const scripts = serverPackageJson.scripts;
|
||||
|
||||
if (options.database !== "none") {
|
||||
if (
|
||||
options.database === "sqlite" &&
|
||||
options.orm === "drizzle" &&
|
||||
options.dbSetup !== "d1"
|
||||
) {
|
||||
scripts["db:local"] = "turso dev --db-file local.db";
|
||||
}
|
||||
|
||||
if (options.orm === "prisma") {
|
||||
scripts["db:push"] = "prisma db push";
|
||||
scripts["db:studio"] = "prisma studio";
|
||||
scripts["db:generate"] = "prisma generate";
|
||||
scripts["db:migrate"] = "prisma migrate dev";
|
||||
} else if (options.orm === "drizzle") {
|
||||
scripts["db:push"] = "drizzle-kit push";
|
||||
scripts["db:studio"] = "drizzle-kit studio";
|
||||
scripts["db:generate"] = "drizzle-kit generate";
|
||||
scripts["db:migrate"] = "drizzle-kit migrate";
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dbSetup === "docker") {
|
||||
scripts["db:start"] = "docker compose up -d";
|
||||
scripts["db:watch"] = "docker compose up";
|
||||
scripts["db:stop"] = "docker compose stop";
|
||||
scripts["db:down"] = "docker compose down";
|
||||
}
|
||||
|
||||
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
|
||||
async function updateConvexPackageJson(
|
||||
projectDir: string,
|
||||
options: ProjectConfig,
|
||||
) {
|
||||
const convexPackageJsonPath = path.join(
|
||||
projectDir,
|
||||
"packages/backend/package.json",
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(convexPackageJsonPath))) return;
|
||||
|
||||
const convexPackageJson = await fs.readJson(convexPackageJsonPath);
|
||||
convexPackageJson.name = `@${options.projectName}/backend`;
|
||||
|
||||
if (!convexPackageJson.scripts) {
|
||||
convexPackageJson.scripts = {};
|
||||
}
|
||||
|
||||
await fs.writeJson(convexPackageJsonPath, convexPackageJson, { spaces: 2 });
|
||||
}
|
||||
76
apps/cli/src/helpers/core/runtime-setup.ts
Normal file
76
apps/cli/src/helpers/core/runtime-setup.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { Backend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupRuntime(config: ProjectConfig) {
|
||||
const { runtime, backend, projectDir } = config;
|
||||
|
||||
if (backend === "convex" || backend === "next" || runtime === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
if (!(await fs.pathExists(serverDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtime === "bun") {
|
||||
await setupBunRuntime(serverDir, backend);
|
||||
} else if (runtime === "node") {
|
||||
await setupNodeRuntime(serverDir, backend);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBunRuntime(serverDir: string, _backend: Backend) {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
dev: "bun run --hot src/index.ts",
|
||||
start: "bun run dist/index.js",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["@types/bun"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
|
||||
async function setupNodeRuntime(serverDir: string, backend: Backend) {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
dev: "tsx watch src/index.ts",
|
||||
start: "node dist/index.js",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["tsx", "@types/node"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
if (backend === "hono") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@hono/node-server"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else if (backend === "elysia") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@elysiajs/node"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
950
apps/cli/src/helpers/core/template-manager.ts
Normal file
950
apps/cli/src/helpers/core/template-manager.ts
Normal file
@@ -0,0 +1,950 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { glob } from "tinyglobby";
|
||||
import { PKG_ROOT } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { processTemplate } from "../../utils/template-processor";
|
||||
|
||||
export async function processAndCopyFiles(
|
||||
sourcePattern: string | string[],
|
||||
baseSourceDir: string,
|
||||
destDir: string,
|
||||
context: ProjectConfig,
|
||||
overwrite = true,
|
||||
ignorePatterns?: string[],
|
||||
) {
|
||||
const sourceFiles = await glob(sourcePattern, {
|
||||
cwd: baseSourceDir,
|
||||
dot: true,
|
||||
onlyFiles: true,
|
||||
absolute: false,
|
||||
ignore: ignorePatterns,
|
||||
});
|
||||
|
||||
for (const relativeSrcPath of sourceFiles) {
|
||||
const srcPath = path.join(baseSourceDir, relativeSrcPath);
|
||||
let relativeDestPath = relativeSrcPath;
|
||||
|
||||
if (relativeSrcPath.endsWith(".hbs")) {
|
||||
relativeDestPath = relativeSrcPath.slice(0, -4);
|
||||
}
|
||||
|
||||
const basename = path.basename(relativeDestPath);
|
||||
if (basename === "_gitignore") {
|
||||
relativeDestPath = path.join(
|
||||
path.dirname(relativeDestPath),
|
||||
".gitignore",
|
||||
);
|
||||
} else if (basename === "_npmrc") {
|
||||
relativeDestPath = path.join(path.dirname(relativeDestPath), ".npmrc");
|
||||
}
|
||||
|
||||
const destPath = path.join(destDir, relativeDestPath);
|
||||
|
||||
await fs.ensureDir(path.dirname(destPath));
|
||||
|
||||
if (!overwrite && (await fs.pathExists(destPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (srcPath.endsWith(".hbs")) {
|
||||
await processTemplate(srcPath, destPath, context);
|
||||
} else {
|
||||
await fs.copy(srcPath, destPath, { overwrite: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyBaseTemplate(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
const templateDir = path.join(PKG_ROOT, "templates/base");
|
||||
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
|
||||
}
|
||||
|
||||
export async function setupFrontendTemplates(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
const hasReactWeb = context.frontend.some((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||
);
|
||||
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||
const hasSvelteWeb = context.frontend.includes("svelte");
|
||||
const hasSolidWeb = context.frontend.includes("solid");
|
||||
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||
const _hasNative = hasNativeWind || hasUnistyles;
|
||||
const isConvex = context.backend === "convex";
|
||||
|
||||
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
await fs.ensureDir(webAppDir);
|
||||
|
||||
if (hasReactWeb) {
|
||||
const webBaseDir = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/frontend/react/web-base",
|
||||
);
|
||||
if (await fs.pathExists(webBaseDir)) {
|
||||
await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
const reactFramework = context.frontend.find((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||
f,
|
||||
),
|
||||
);
|
||||
if (reactFramework) {
|
||||
const frameworkSrcDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/frontend/react/${reactFramework}`,
|
||||
);
|
||||
if (await fs.pathExists(frameworkSrcDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
frameworkSrcDir,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!isConvex && context.api !== "none") {
|
||||
const apiWebBaseDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/api/${context.api}/web/react/base`,
|
||||
);
|
||||
if (await fs.pathExists(apiWebBaseDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
apiWebBaseDir,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (hasNuxtWeb) {
|
||||
const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
|
||||
if (await fs.pathExists(nuxtBaseDir)) {
|
||||
await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!isConvex && context.api === "orpc") {
|
||||
const apiWebNuxtDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/api/${context.api}/web/nuxt`,
|
||||
);
|
||||
if (await fs.pathExists(apiWebNuxtDir)) {
|
||||
await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
} else if (hasSvelteWeb) {
|
||||
const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
|
||||
if (await fs.pathExists(svelteBaseDir)) {
|
||||
await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!isConvex && context.api === "orpc") {
|
||||
const apiWebSvelteDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/api/${context.api}/web/svelte`,
|
||||
);
|
||||
if (await fs.pathExists(apiWebSvelteDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
apiWebSvelteDir,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
} else if (hasSolidWeb) {
|
||||
const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
|
||||
if (await fs.pathExists(solidBaseDir)) {
|
||||
await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!isConvex && context.api === "orpc") {
|
||||
const apiWebSolidDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/api/${context.api}/web/solid`,
|
||||
);
|
||||
if (await fs.pathExists(apiWebSolidDir)) {
|
||||
await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNativeWind || hasUnistyles) {
|
||||
const nativeAppDir = path.join(projectDir, "apps/native");
|
||||
await fs.ensureDir(nativeAppDir);
|
||||
|
||||
const nativeBaseCommonDir = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/frontend/native/native-base",
|
||||
);
|
||||
if (await fs.pathExists(nativeBaseCommonDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
nativeBaseCommonDir,
|
||||
nativeAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
let nativeFrameworkPath = "";
|
||||
if (hasNativeWind) {
|
||||
nativeFrameworkPath = "nativewind";
|
||||
} else if (hasUnistyles) {
|
||||
nativeFrameworkPath = "unistyles";
|
||||
}
|
||||
|
||||
const nativeSpecificDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/frontend/native/${nativeFrameworkPath}`,
|
||||
);
|
||||
if (await fs.pathExists(nativeSpecificDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
nativeSpecificDir,
|
||||
nativeAppDir,
|
||||
context,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
|
||||
const apiNativeSrcDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/api/${context.api}/native`,
|
||||
);
|
||||
if (await fs.pathExists(apiNativeSrcDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
apiNativeSrcDir,
|
||||
nativeAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupBackendFramework(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (context.backend === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
|
||||
if (context.backend === "convex") {
|
||||
if (await fs.pathExists(serverAppDir)) {
|
||||
await fs.remove(serverAppDir);
|
||||
}
|
||||
|
||||
const convexBackendDestDir = path.join(projectDir, "packages/backend");
|
||||
const convexSrcDir = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/backend/convex/packages/backend",
|
||||
);
|
||||
await fs.ensureDir(convexBackendDestDir);
|
||||
if (await fs.pathExists(convexSrcDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
convexSrcDir,
|
||||
convexBackendDestDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.ensureDir(serverAppDir);
|
||||
|
||||
const serverBaseDir = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/backend/server/server-base",
|
||||
);
|
||||
if (await fs.pathExists(serverBaseDir)) {
|
||||
await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
|
||||
} else {
|
||||
}
|
||||
|
||||
const frameworkSrcDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/backend/server/${context.backend}`,
|
||||
);
|
||||
if (await fs.pathExists(frameworkSrcDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
frameworkSrcDir,
|
||||
serverAppDir,
|
||||
context,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
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,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
const apiServerFrameworkDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/api/${context.api}/server/${context.backend}`,
|
||||
);
|
||||
if (await fs.pathExists(apiServerFrameworkDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
apiServerFrameworkDir,
|
||||
serverAppDir,
|
||||
context,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDbOrmTemplates(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (
|
||||
context.backend === "convex" ||
|
||||
context.orm === "none" ||
|
||||
context.database === "none"
|
||||
)
|
||||
return;
|
||||
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
await fs.ensureDir(serverAppDir);
|
||||
|
||||
const dbOrmSrcDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/db/${context.orm}/${context.database}`,
|
||||
);
|
||||
|
||||
if (await fs.pathExists(dbOrmSrcDir)) {
|
||||
await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupAuthTemplate(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (context.backend === "convex" || !context.auth) return;
|
||||
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
const nativeAppDir = path.join(projectDir, "apps/native");
|
||||
|
||||
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
||||
const webAppDirExists = await fs.pathExists(webAppDir);
|
||||
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
||||
|
||||
const hasReactWeb = context.frontend.some((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||
);
|
||||
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||
const hasSvelteWeb = context.frontend.includes("svelte");
|
||||
const hasSolidWeb = context.frontend.includes("solid");
|
||||
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||
const hasNative = hasNativeWind || hasUnistyles;
|
||||
|
||||
if (serverAppDirExists) {
|
||||
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
|
||||
if (await fs.pathExists(authServerBaseSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
authServerBaseSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (context.backend === "next") {
|
||||
const authServerNextSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/auth/server/next",
|
||||
);
|
||||
if (await fs.pathExists(authServerNextSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
authServerNextSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
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}`,
|
||||
);
|
||||
} else if (orm === "mongoose") {
|
||||
authDbSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/auth/server/db/mongoose/${db}`,
|
||||
);
|
||||
}
|
||||
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
|
||||
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
|
||||
} else if (authDbSrc) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) &&
|
||||
webAppDirExists
|
||||
) {
|
||||
if (hasReactWeb) {
|
||||
const authWebBaseSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/auth/web/react/base",
|
||||
);
|
||||
if (await fs.pathExists(authWebBaseSrc)) {
|
||||
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
|
||||
const reactFramework = context.frontend.find((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||
f,
|
||||
),
|
||||
);
|
||||
if (reactFramework) {
|
||||
const authWebFrameworkSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/auth/web/react/${reactFramework}`,
|
||||
);
|
||||
if (await fs.pathExists(authWebFrameworkSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
authWebFrameworkSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
} else if (hasNuxtWeb) {
|
||||
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
|
||||
if (await fs.pathExists(authWebNuxtSrc)) {
|
||||
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
} else if (hasSvelteWeb) {
|
||||
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
|
||||
if (await fs.pathExists(authWebSvelteSrc)) {
|
||||
await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
} else if (hasSolidWeb) {
|
||||
const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
|
||||
if (await fs.pathExists(authWebSolidSrc)) {
|
||||
await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNative && nativeAppDirExists) {
|
||||
const authNativeBaseSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/auth/native/native-base",
|
||||
);
|
||||
if (await fs.pathExists(authNativeBaseSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
authNativeBaseSrc,
|
||||
nativeAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
let nativeFrameworkAuthPath = "";
|
||||
if (hasNativeWind) {
|
||||
nativeFrameworkAuthPath = "nativewind";
|
||||
} else if (hasUnistyles) {
|
||||
nativeFrameworkAuthPath = "unistyles";
|
||||
}
|
||||
|
||||
if (nativeFrameworkAuthPath) {
|
||||
const authNativeFrameworkSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/auth/native/${nativeFrameworkAuthPath}`,
|
||||
);
|
||||
if (await fs.pathExists(authNativeFrameworkSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
authNativeFrameworkSrc,
|
||||
nativeAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupAddonsTemplate(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (!context.addons || context.addons.length === 0) return;
|
||||
|
||||
for (const addon of context.addons) {
|
||||
if (addon === "none") continue;
|
||||
|
||||
if (addon === "ruler") continue;
|
||||
|
||||
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
|
||||
let addonDestDir = projectDir;
|
||||
|
||||
if (addon === "pwa") {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) {
|
||||
continue;
|
||||
}
|
||||
addonDestDir = webAppDir;
|
||||
if (context.frontend.includes("next")) {
|
||||
addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
|
||||
} else if (
|
||||
context.frontend.some((f) =>
|
||||
["tanstack-router", "react-router", "solid"].includes(f),
|
||||
)
|
||||
) {
|
||||
addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (await fs.pathExists(addonSrcDir)) {
|
||||
await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupExamplesTemplate(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (
|
||||
!context.examples ||
|
||||
context.examples.length === 0 ||
|
||||
context.examples[0] === "none"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
|
||||
const serverAppDirExists = await fs.pathExists(serverAppDir);
|
||||
const webAppDirExists = await fs.pathExists(webAppDir);
|
||||
const nativeAppDir = path.join(projectDir, "apps/native");
|
||||
const nativeAppDirExists = await fs.pathExists(nativeAppDir);
|
||||
|
||||
const hasReactWeb = context.frontend.some((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||
);
|
||||
const hasNuxtWeb = context.frontend.includes("nuxt");
|
||||
const hasSvelteWeb = context.frontend.includes("svelte");
|
||||
const hasSolidWeb = context.frontend.includes("solid");
|
||||
|
||||
for (const example of context.examples) {
|
||||
if (example === "none") continue;
|
||||
|
||||
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
|
||||
|
||||
if (
|
||||
serverAppDirExists &&
|
||||
context.backend !== "convex" &&
|
||||
context.backend !== "none"
|
||||
) {
|
||||
const exampleServerSrc = path.join(exampleBaseDir, "server");
|
||||
|
||||
if (example === "ai" && context.backend === "next") {
|
||||
const aiNextServerSrc = path.join(exampleServerSrc, "next");
|
||||
if (await fs.pathExists(aiNextServerSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
aiNextServerSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.orm !== "none" && context.database !== "none") {
|
||||
const exampleOrmBaseSrc = path.join(
|
||||
exampleServerSrc,
|
||||
context.orm,
|
||||
"base",
|
||||
);
|
||||
if (await fs.pathExists(exampleOrmBaseSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleOrmBaseSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
const exampleDbSchemaSrc = path.join(
|
||||
exampleServerSrc,
|
||||
context.orm,
|
||||
context.database,
|
||||
);
|
||||
if (await fs.pathExists(exampleDbSchemaSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleDbSchemaSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (webAppDirExists) {
|
||||
if (hasReactWeb) {
|
||||
const exampleWebSrc = path.join(exampleBaseDir, "web/react");
|
||||
if (await fs.pathExists(exampleWebSrc)) {
|
||||
const reactFramework = context.frontend.find((f) =>
|
||||
[
|
||||
"next",
|
||||
"react-router",
|
||||
"tanstack-router",
|
||||
"tanstack-start",
|
||||
].includes(f),
|
||||
);
|
||||
if (reactFramework) {
|
||||
const exampleWebFrameworkSrc = path.join(
|
||||
exampleWebSrc,
|
||||
reactFramework,
|
||||
);
|
||||
if (await fs.pathExists(exampleWebFrameworkSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleWebFrameworkSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (hasNuxtWeb) {
|
||||
const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
|
||||
if (await fs.pathExists(exampleWebNuxtSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleWebNuxtSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
} else if (hasSvelteWeb) {
|
||||
const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
|
||||
if (await fs.pathExists(exampleWebSvelteSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleWebSvelteSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
} else if (hasSolidWeb) {
|
||||
const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
|
||||
if (await fs.pathExists(exampleWebSolidSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleWebSolidSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeAppDirExists) {
|
||||
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||
|
||||
if (hasNativeWind || hasUnistyles) {
|
||||
let nativeFramework = "";
|
||||
if (hasNativeWind) {
|
||||
nativeFramework = "nativewind";
|
||||
} else if (hasUnistyles) {
|
||||
nativeFramework = "unistyles";
|
||||
}
|
||||
|
||||
const exampleNativeSrc = path.join(
|
||||
exampleBaseDir,
|
||||
`native/${nativeFramework}`,
|
||||
);
|
||||
if (await fs.pathExists(exampleNativeSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
exampleNativeSrc,
|
||||
nativeAppDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleExtras(projectDir: string, context: ProjectConfig) {
|
||||
const extrasDir = path.join(PKG_ROOT, "templates/extras");
|
||||
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||
const hasNative = hasNativeWind || hasUnistyles;
|
||||
|
||||
if (context.packageManager === "pnpm") {
|
||||
const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
|
||||
const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
|
||||
if (await fs.pathExists(pnpmWorkspaceSrc)) {
|
||||
await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.packageManager === "bun") {
|
||||
const bunfigSrc = path.join(extrasDir, "bunfig.toml.hbs");
|
||||
if (await fs.pathExists(bunfigSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"bunfig.toml.hbs",
|
||||
extrasDir,
|
||||
projectDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
context.packageManager === "pnpm" &&
|
||||
(hasNative || context.frontend.includes("nuxt"))
|
||||
) {
|
||||
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
||||
if (await fs.pathExists(npmrcTemplateSrc)) {
|
||||
await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDockerComposeTemplates(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (context.dbSetup !== "docker" || context.database === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
const dockerSrcDir = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/db-setup/docker-compose/${context.database}`,
|
||||
);
|
||||
|
||||
if (await fs.pathExists(dockerSrcDir)) {
|
||||
await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDeploymentTemplates(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") {
|
||||
if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
|
||||
const alchemyTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/deploy/alchemy",
|
||||
);
|
||||
if (await fs.pathExists(alchemyTemplateSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"alchemy.run.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
projectDir,
|
||||
context,
|
||||
);
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverAppDir)) {
|
||||
await processAndCopyFiles(
|
||||
"env.d.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
await processAndCopyFiles(
|
||||
"wrangler.jsonc.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (context.webDeploy === "alchemy") {
|
||||
const alchemyTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/deploy/alchemy",
|
||||
);
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (
|
||||
(await fs.pathExists(alchemyTemplateSrc)) &&
|
||||
(await fs.pathExists(webAppDir))
|
||||
) {
|
||||
await processAndCopyFiles(
|
||||
"alchemy.run.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.serverDeploy === "alchemy") {
|
||||
const alchemyTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/deploy/alchemy",
|
||||
);
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
if (
|
||||
(await fs.pathExists(alchemyTemplateSrc)) &&
|
||||
(await fs.pathExists(serverAppDir))
|
||||
) {
|
||||
await processAndCopyFiles(
|
||||
"alchemy.run.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
await processAndCopyFiles(
|
||||
"env.d.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
await processAndCopyFiles(
|
||||
"wrangler.jsonc.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (await fs.pathExists(webAppDir)) {
|
||||
const frontends = context.frontend;
|
||||
|
||||
const templateMap: Record<string, string> = {
|
||||
"tanstack-router": "react/tanstack-router",
|
||||
"tanstack-start": "react/tanstack-start",
|
||||
"react-router": "react/react-router",
|
||||
solid: "solid",
|
||||
next: "react/next",
|
||||
nuxt: "nuxt",
|
||||
svelte: "svelte",
|
||||
};
|
||||
|
||||
for (const f of frontends) {
|
||||
if (templateMap[f]) {
|
||||
const deployTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/deploy/${context.webDeploy}/web/${templateMap[f]}`,
|
||||
);
|
||||
if (await fs.pathExists(deployTemplateSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
deployTemplateSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverAppDir)) {
|
||||
const deployTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/deploy/${context.serverDeploy}/server`,
|
||||
);
|
||||
if (await fs.pathExists(deployTemplateSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
deployTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user