Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DEFAULT_CONFIG_BASE = {
database: "sqlite",
orm: "drizzle",
auth: "better-auth",
payments: "none",
addons: ["turborepo"],
examples: [],
git: true,
Expand All @@ -39,8 +40,8 @@ export function getDefaultConfig() {
export const DEFAULT_CONFIG = getDefaultConfig();

export const dependencyVersionMap = {
"better-auth": "^1.3.9",
"@better-auth/expo": "^1.3.9",
"better-auth": "^1.3.10",
"@better-auth/expo": "^1.3.10",

"@clerk/nextjs": "^6.31.5",
"@clerk/clerk-react": "^5.45.0",
Expand Down Expand Up @@ -155,6 +156,9 @@ export const dependencyVersionMap = {
nitropack: "^2.12.4",

dotenv: "^17.2.1",

"@polar-sh/better-auth": "^1.1.3",
"@polar-sh/sdk": "^0.34.16",
} as const;

export type AvailableDependencies = keyof typeof dependencyVersionMap;
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/helpers/core/add-addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ 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 { validateAddonCompatibility } from "../../utils/compatibility-rules";
import { exitWithError } from "../../utils/errors";
import { setupAddons } from "../addons/addons-setup";
import {
Expand Down Expand Up @@ -45,6 +45,7 @@ export async function addAddonsToProject(
addons: input.addons,
examples: detectedConfig.examples || [],
auth: detectedConfig.auth || "none",
payments: detectedConfig.payments || "none",
git: false,
packageManager:
input.packageManager || detectedConfig.packageManager || "npm",
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/helpers/core/add-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function addDeploymentToProject(
addons: detectedConfig.addons || [],
examples: detectedConfig.examples || [],
auth: detectedConfig.auth || "none",
payments: detectedConfig.payments || "none",
git: false,
packageManager:
input.packageManager || detectedConfig.packageManager || "npm",
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/helpers/core/command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export async function createProjectHandler(
addons: [],
examples: [],
auth: "none",
payments: "none",
git: false,
packageManager: "npm",
install: false,
Expand Down Expand Up @@ -272,6 +273,7 @@ export async function addAddonsHandler(input: AddInput) {
const addonsPrompt = await getAddonsToAdd(
detectedConfig.frontend || [],
detectedConfig.addons || [],
detectedConfig.auth,
);

if (addonsPrompt.length > 0) {
Expand Down
9 changes: 9 additions & 0 deletions apps/cli/src/helpers/core/create-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { createReadme } from "./create-readme";
import { setupEnvironmentVariables } from "./env-setup";
import { initializeGit } from "./git";
import { installDependencies } from "./install-dependencies";
import { setupPayments } from "./payments-setup";
import { displayPostInstallInstructions } from "./post-installation";
import { updatePackageConfigurations } from "./project-config";
import {
Expand All @@ -30,6 +31,7 @@ import {
setupDockerComposeTemplates,
setupExamplesTemplate,
setupFrontendTemplates,
setupPaymentsTemplate,
} from "./template-manager";

export async function createProject(
Expand All @@ -50,6 +52,9 @@ export async function createProject(
await setupDockerComposeTemplates(projectDir, options);
}
await setupAuthTemplate(projectDir, options);
if (options.payments && options.payments !== "none") {
await setupPaymentsTemplate(projectDir, options);
}
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamplesTemplate(projectDir, options);
}
Expand All @@ -76,6 +81,10 @@ export async function createProject(
await setupAuth(options);
}

if (options.payments && options.payments !== "none") {
await setupPayments(options);
}

await handleExtras(projectDir, options);

await setupEnvironmentVariables(options);
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/helpers/core/detect-project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function detectProjectConfig(projectDir: string) {
addons: btsConfig.addons,
examples: btsConfig.examples,
auth: btsConfig.auth,
payments: btsConfig.payments,
packageManager: btsConfig.packageManager,
dbSetup: btsConfig.dbSetup,
api: btsConfig.api,
Expand Down
10 changes: 10 additions & 0 deletions apps/cli/src/helpers/core/env-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
value: "",
condition: examples?.includes("ai") || false,
},
{
key: "POLAR_ACCESS_TOKEN",
value: "",
condition: config.payments === "polar",
},
{
key: "POLAR_SUCCESS_URL",
value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
condition: config.payments === "polar",
},
];

await addEnvVariablesToFile(envPath, serverVars);
Expand Down
50 changes: 50 additions & 0 deletions apps/cli/src/helpers/core/payments-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";

export async function setupPayments(config: ProjectConfig) {
const { payments, projectDir, frontend } = config;

if (!payments || payments === "none") {
return;
}

const serverDir = path.join(projectDir, "apps/server");
const clientDir = path.join(projectDir, "apps/web");

const serverDirExists = await fs.pathExists(serverDir);
const clientDirExists = await fs.pathExists(clientDir);

if (!serverDirExists) {
return;
}

if (payments === "polar") {
await addPackageDependency({
dependencies: ["@polar-sh/better-auth", "@polar-sh/sdk"],
projectDir: serverDir,
});

if (clientDirExists) {
const hasWebFrontend = frontend.some((f) =>
[
"react-router",
"tanstack-router",
"tanstack-start",
"next",
"nuxt",
"svelte",
"solid",
].includes(f),
);

if (hasWebFrontend) {
await addPackageDependency({
dependencies: ["@polar-sh/better-auth"],
projectDir: clientDir,
});
}
}
}
}
9 changes: 9 additions & 0 deletions apps/cli/src/helpers/core/post-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export async function displayPostInstallInstructions(
: "";
const clerkInstructions =
isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
const polarInstructions =
config.payments === "polar" && config.auth === "better-auth"
? getPolarInstructions()
: "";
const wranglerDeployInstructions = getWranglerDeployInstructions(
runCmd,
webDeploy,
Expand Down Expand Up @@ -188,6 +192,7 @@ export async function displayPostInstallInstructions(
output += `\n${alchemyDeployInstructions.trim()}\n`;
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;

if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
Expand Down Expand Up @@ -447,6 +452,10 @@ function getClerkInstructions() {
return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("•")} Follow the guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.cyan("•")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.cyan("•")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env`;
}

function getPolarInstructions() {
return `${pc.bold("Polar Payments Setup:")}\n${pc.cyan("•")} Get access token & product ID from ${pc.underline("https://sandbox.polar.sh/")}\n${pc.cyan("•")} Set POLAR_ACCESS_TOKEN in apps/server/.env`;
}

function getAlchemyDeployInstructions(
runCmd?: string,
webDeploy?: string,
Expand Down
96 changes: 96 additions & 0 deletions apps/cli/src/helpers/core/template-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,102 @@ export async function setupAuthTemplate(
}
}

export async function setupPaymentsTemplate(
projectDir: string,
context: ProjectConfig,
) {
if (!context.payments || context.payments === "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);

if (serverAppDirExists && context.backend !== "convex") {
const paymentsServerSrc = path.join(
PKG_ROOT,
`templates/payments/${context.payments}/server/base`,
);
if (await fs.pathExists(paymentsServerSrc)) {
await processAndCopyFiles(
"**/*",
paymentsServerSrc,
serverAppDir,
context,
);
}
}

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");

if (
webAppDirExists &&
(hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb)
) {
if (hasReactWeb) {
const reactFramework = context.frontend.find((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
f,
),
);
if (reactFramework) {
const paymentsWebSrc = path.join(
PKG_ROOT,
`templates/payments/${context.payments}/web/react/${reactFramework}`,
);
if (await fs.pathExists(paymentsWebSrc)) {
await processAndCopyFiles("**/*", paymentsWebSrc, webAppDir, context);
}
}
} else if (hasNuxtWeb) {
const paymentsWebNuxtSrc = path.join(
PKG_ROOT,
`templates/payments/${context.payments}/web/nuxt`,
);
if (await fs.pathExists(paymentsWebNuxtSrc)) {
await processAndCopyFiles(
"**/*",
paymentsWebNuxtSrc,
webAppDir,
context,
);
}
} else if (hasSvelteWeb) {
const paymentsWebSvelteSrc = path.join(
PKG_ROOT,
`templates/payments/${context.payments}/web/svelte`,
);
if (await fs.pathExists(paymentsWebSvelteSrc)) {
await processAndCopyFiles(
"**/*",
paymentsWebSvelteSrc,
webAppDir,
context,
);
}
} else if (hasSolidWeb) {
const paymentsWebSolidSrc = path.join(
PKG_ROOT,
`templates/payments/${context.payments}/web/solid`,
);
if (await fs.pathExists(paymentsWebSolidSrc)) {
await processAndCopyFiles(
"**/*",
paymentsWebSolidSrc,
webAppDir,
context,
);
}
}
}
}

export async function setupAddonsTemplate(
projectDir: string,
context: ProjectConfig,
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ORMSchema,
type PackageManager,
PackageManagerSchema,
PaymentsSchema,
type ProjectConfig,
ProjectNameSchema,
type Runtime,
Expand Down Expand Up @@ -80,6 +81,7 @@ export const router = t.router({
database: DatabaseSchema.optional(),
orm: ORMSchema.optional(),
auth: AuthSchema.optional(),
payments: PaymentsSchema.optional(),
frontend: z.array(FrontendSchema).optional(),
addons: z.array(AddonsSchema).optional(),
examples: z.array(ExamplesSchema).optional(),
Expand Down
13 changes: 10 additions & 3 deletions apps/cli/src/prompts/addons.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { groupMultiselect, isCancel } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import { type Addons, AddonsSchema, type Frontend } from "../types";
import { type Addons, AddonsSchema, type Auth, type Frontend } from "../types";
import {
getCompatibleAddons,
validateAddonCompatibility,
} from "../utils/addon-compatibility";
} from "../utils/compatibility-rules";
import { exitCancelled } from "../utils/errors";

type AddonOption = {
Expand Down Expand Up @@ -75,6 +75,7 @@ const ADDON_GROUPS = {
export async function getAddonsChoice(
addons?: Addons[],
frontends?: Frontend[],
auth?: Auth,
) {
if (addons !== undefined) return addons;

Expand All @@ -88,7 +89,11 @@ export async function getAddonsChoice(
const frontendsArray = frontends || [];

for (const addon of allAddons) {
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray);
const { isCompatible } = validateAddonCompatibility(
addon,
frontendsArray,
auth,
);
if (!isCompatible) continue;

const { label, hint } = getAddonDisplay(addon);
Expand Down Expand Up @@ -131,6 +136,7 @@ export async function getAddonsChoice(
export async function getAddonsToAdd(
frontend: Frontend[],
existingAddons: Addons[] = [],
auth?: Auth,
) {
const groupedOptions: Record<string, AddonOption[]> = {
Documentation: [],
Expand All @@ -144,6 +150,7 @@ export async function getAddonsToAdd(
AddonsSchema.options.filter((addon) => addon !== "none"),
frontendArray,
existingAddons,
auth,
);

for (const addon of compatibleAddons) {
Expand Down
Loading