mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add polar as better-auth plugin (#578)
This commit is contained in:
@@ -13,6 +13,7 @@ export const DEFAULT_CONFIG_BASE = {
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
auth: "better-auth",
|
||||
payments: "none",
|
||||
addons: ["turborepo"],
|
||||
examples: [],
|
||||
git: true,
|
||||
@@ -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",
|
||||
@@ -139,9 +140,9 @@ export const dependencyVersionMap = {
|
||||
"@tanstack/react-query-devtools": "^5.85.5",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
|
||||
"@tanstack/solid-query": "^5.75.0",
|
||||
"@tanstack/solid-query-devtools": "^5.75.0",
|
||||
"@tanstack/solid-router-devtools": "^1.131.25",
|
||||
"@tanstack/solid-query": "^5.87.4",
|
||||
"@tanstack/solid-query-devtools": "^5.87.4",
|
||||
"@tanstack/solid-router-devtools": "^1.131.44",
|
||||
|
||||
wrangler: "^4.23.0",
|
||||
"@cloudflare/vite-plugin": "^1.9.0",
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -103,6 +103,7 @@ export async function createProjectHandler(
|
||||
addons: [],
|
||||
examples: [],
|
||||
auth: "none",
|
||||
payments: "none",
|
||||
git: false,
|
||||
packageManager: "npm",
|
||||
install: false,
|
||||
@@ -272,6 +273,7 @@ export async function addAddonsHandler(input: AddInput) {
|
||||
const addonsPrompt = await getAddonsToAdd(
|
||||
detectedConfig.frontend || [],
|
||||
detectedConfig.addons || [],
|
||||
detectedConfig.auth,
|
||||
);
|
||||
|
||||
if (addonsPrompt.length > 0) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
setupDockerComposeTemplates,
|
||||
setupExamplesTemplate,
|
||||
setupFrontendTemplates,
|
||||
setupPaymentsTemplate,
|
||||
} from "./template-manager";
|
||||
|
||||
export async function createProject(
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
50
apps/cli/src/helpers/core/payments-setup.ts
Normal file
50
apps/cli/src/helpers/core/payments-setup.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ORMSchema,
|
||||
type PackageManager,
|
||||
PackageManagerSchema,
|
||||
PaymentsSchema,
|
||||
type ProjectConfig,
|
||||
ProjectNameSchema,
|
||||
type Runtime,
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = {
|
||||
@@ -75,6 +75,7 @@ const ADDON_GROUPS = {
|
||||
export async function getAddonsChoice(
|
||||
addons?: Addons[],
|
||||
frontends?: Frontend[],
|
||||
auth?: Auth,
|
||||
) {
|
||||
if (addons !== undefined) return addons;
|
||||
|
||||
@@ -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);
|
||||
@@ -131,6 +136,7 @@ export async function getAddonsChoice(
|
||||
export async function getAddonsToAdd(
|
||||
frontend: Frontend[],
|
||||
existingAddons: Addons[] = [],
|
||||
auth?: Auth,
|
||||
) {
|
||||
const groupedOptions: Record<string, AddonOption[]> = {
|
||||
Documentation: [],
|
||||
@@ -144,6 +150,7 @@ export async function getAddonsToAdd(
|
||||
AddonsSchema.options.filter((addon) => addon !== "none"),
|
||||
frontendArray,
|
||||
existingAddons,
|
||||
auth,
|
||||
);
|
||||
|
||||
for (const addon of compatibleAddons) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
Frontend,
|
||||
ORM,
|
||||
PackageManager,
|
||||
Payments,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
ServerDeploy,
|
||||
@@ -28,6 +29,7 @@ import { getGitChoice } from "./git";
|
||||
import { getinstallChoice } from "./install";
|
||||
import { getORMChoice } from "./orm";
|
||||
import { getPackageManagerChoice } from "./package-manager";
|
||||
import { getPaymentsChoice } from "./payments";
|
||||
import { getRuntimeChoice } from "./runtime";
|
||||
import { getServerDeploymentChoice } from "./server-deploy";
|
||||
import { getDeploymentChoice } from "./web-deploy";
|
||||
@@ -40,6 +42,7 @@ type PromptGroupResults = {
|
||||
orm: ORM;
|
||||
api: API;
|
||||
auth: Auth;
|
||||
payments: Payments;
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
dbSetup: DatabaseSetup;
|
||||
@@ -87,7 +90,15 @@ export async function gatherConfig(
|
||||
results.backend,
|
||||
results.frontend,
|
||||
),
|
||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||
payments: ({ results }) =>
|
||||
getPaymentsChoice(
|
||||
flags.payments,
|
||||
results.auth,
|
||||
results.backend,
|
||||
results.frontend,
|
||||
),
|
||||
addons: ({ results }) =>
|
||||
getAddonsChoice(flags.addons, results.frontend, results.auth),
|
||||
examples: ({ results }) =>
|
||||
getExamplesChoice(
|
||||
flags.examples,
|
||||
@@ -137,6 +148,7 @@ export async function gatherConfig(
|
||||
database: result.database,
|
||||
orm: result.orm,
|
||||
auth: result.auth,
|
||||
payments: result.payments,
|
||||
addons: result.addons,
|
||||
examples: result.examples,
|
||||
git: result.git,
|
||||
|
||||
46
apps/cli/src/prompts/payments.ts
Normal file
46
apps/cli/src/prompts/payments.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Auth, Backend, Frontend, Payments } from "../types";
|
||||
import { splitFrontends } from "../utils/compatibility-rules";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getPaymentsChoice(
|
||||
payments?: Payments,
|
||||
auth?: Auth,
|
||||
backend?: Backend,
|
||||
frontends?: Frontend[],
|
||||
) {
|
||||
if (payments !== undefined) return payments;
|
||||
|
||||
const isPolarCompatible =
|
||||
auth === "better-auth" &&
|
||||
backend !== "convex" &&
|
||||
(frontends?.length === 0 || splitFrontends(frontends).web.length > 0);
|
||||
|
||||
if (!isPolarCompatible) {
|
||||
return "none" as Payments;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "polar" as Payments,
|
||||
label: "Polar",
|
||||
hint: "Turn your software into a business. 6 lines of code.",
|
||||
},
|
||||
{
|
||||
value: "none" as Payments,
|
||||
label: "None",
|
||||
hint: "No payments integration",
|
||||
},
|
||||
];
|
||||
|
||||
const response = await select<Payments>({
|
||||
message: "Select payments provider",
|
||||
options,
|
||||
initialValue: DEFAULT_CONFIG.payments,
|
||||
});
|
||||
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -86,6 +86,11 @@ export const AuthSchema = z
|
||||
.describe("Authentication provider");
|
||||
export type Auth = z.infer<typeof AuthSchema>;
|
||||
|
||||
export const PaymentsSchema = z
|
||||
.enum(["polar", "none"])
|
||||
.describe("Payments provider");
|
||||
export type Payments = z.infer<typeof PaymentsSchema>;
|
||||
|
||||
export const ProjectNameSchema = z
|
||||
.string()
|
||||
.min(1, "Project name cannot be empty")
|
||||
@@ -132,6 +137,7 @@ export type CreateInput = {
|
||||
database?: Database;
|
||||
orm?: ORM;
|
||||
auth?: Auth;
|
||||
payments?: Payments;
|
||||
frontend?: Frontend[];
|
||||
addons?: Addons[];
|
||||
examples?: Examples[];
|
||||
@@ -175,6 +181,7 @@ export interface ProjectConfig {
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
auth: Auth;
|
||||
payments: Payments;
|
||||
git: boolean;
|
||||
packageManager: PackageManager;
|
||||
install: boolean;
|
||||
@@ -195,6 +202,7 @@ export interface BetterTStackConfig {
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
auth: Auth;
|
||||
payments: Payments;
|
||||
packageManager: PackageManager;
|
||||
dbSetup: DatabaseSetup;
|
||||
api: API;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ADDON_COMPATIBILITY } from "../constants";
|
||||
import type { Addons, Frontend } from "../types";
|
||||
|
||||
export function validateAddonCompatibility(
|
||||
addon: Addons,
|
||||
frontend: Frontend[],
|
||||
): { isCompatible: boolean; reason?: string } {
|
||||
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
||||
|
||||
if (compatibleFrontends.length === 0) {
|
||||
return { isCompatible: true };
|
||||
}
|
||||
|
||||
const hasCompatibleFrontend = frontend.some((f) =>
|
||||
(compatibleFrontends as readonly string[]).includes(f),
|
||||
);
|
||||
|
||||
if (!hasCompatibleFrontend) {
|
||||
const frontendList = compatibleFrontends.join(", ");
|
||||
return {
|
||||
isCompatible: false,
|
||||
reason: `${addon} addon requires one of these frontends: ${frontendList}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isCompatible: true };
|
||||
}
|
||||
|
||||
export function getCompatibleAddons(
|
||||
allAddons: Addons[],
|
||||
frontend: Frontend[],
|
||||
existingAddons: Addons[] = [],
|
||||
) {
|
||||
return allAddons.filter((addon) => {
|
||||
if (existingAddons.includes(addon)) return false;
|
||||
|
||||
if (addon === "none") return false;
|
||||
|
||||
const { isCompatible } = validateAddonCompatibility(addon, frontend);
|
||||
return isCompatible;
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
||||
addons: projectConfig.addons,
|
||||
examples: projectConfig.examples,
|
||||
auth: projectConfig.auth,
|
||||
payments: projectConfig.payments,
|
||||
packageManager: projectConfig.packageManager,
|
||||
dbSetup: projectConfig.dbSetup,
|
||||
api: projectConfig.api,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ADDON_COMPATIBILITY } from "../constants";
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
Auth,
|
||||
Backend,
|
||||
CLIInput,
|
||||
Frontend,
|
||||
Payments,
|
||||
ProjectConfig,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
import { validateAddonCompatibility } from "./addon-compatibility";
|
||||
import { WEB_FRAMEWORKS } from "./compatibility";
|
||||
import { exitWithError } from "./errors";
|
||||
|
||||
@@ -191,15 +193,57 @@ export function validateServerDeployRequiresBackend(
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAddonCompatibility(
|
||||
addon: Addons,
|
||||
frontend: Frontend[],
|
||||
_auth?: Auth,
|
||||
): { isCompatible: boolean; reason?: string } {
|
||||
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
||||
|
||||
if (compatibleFrontends.length > 0) {
|
||||
const hasCompatibleFrontend = frontend.some((f) =>
|
||||
(compatibleFrontends as readonly string[]).includes(f),
|
||||
);
|
||||
|
||||
if (!hasCompatibleFrontend) {
|
||||
const frontendList = compatibleFrontends.join(", ");
|
||||
return {
|
||||
isCompatible: false,
|
||||
reason: `${addon} addon requires one of these frontends: ${frontendList}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isCompatible: true };
|
||||
}
|
||||
|
||||
export function getCompatibleAddons(
|
||||
allAddons: Addons[],
|
||||
frontend: Frontend[],
|
||||
existingAddons: Addons[] = [],
|
||||
auth?: Auth,
|
||||
) {
|
||||
return allAddons.filter((addon) => {
|
||||
if (existingAddons.includes(addon)) return false;
|
||||
|
||||
if (addon === "none") return false;
|
||||
|
||||
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
|
||||
return isCompatible;
|
||||
});
|
||||
}
|
||||
|
||||
export function validateAddonsAgainstFrontends(
|
||||
addons: Addons[] = [],
|
||||
frontends: Frontend[] = [],
|
||||
auth?: Auth,
|
||||
) {
|
||||
for (const addon of addons) {
|
||||
if (addon === "none") continue;
|
||||
const { isCompatible, reason } = validateAddonCompatibility(
|
||||
addon,
|
||||
frontends,
|
||||
auth,
|
||||
);
|
||||
if (!isCompatible) {
|
||||
exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
||||
@@ -207,6 +251,36 @@ export function validateAddonsAgainstFrontends(
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePaymentsCompatibility(
|
||||
payments: Payments | undefined,
|
||||
auth: Auth | undefined,
|
||||
backend: Backend | undefined,
|
||||
frontends: Frontend[] = [],
|
||||
) {
|
||||
if (!payments || payments === "none") return;
|
||||
|
||||
if (payments === "polar") {
|
||||
if (!auth || auth === "none" || auth !== "better-auth") {
|
||||
exitWithError(
|
||||
"Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.",
|
||||
);
|
||||
}
|
||||
|
||||
if (backend === "convex") {
|
||||
exitWithError(
|
||||
"Polar payments is not compatible with Convex backend. Please use a different backend or choose a different payments provider.",
|
||||
);
|
||||
}
|
||||
|
||||
const { web } = splitFrontends(frontends);
|
||||
if (web.length === 0 && frontends.length > 0) {
|
||||
exitWithError(
|
||||
"Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateExamplesCompatibility(
|
||||
examples: string[] | undefined,
|
||||
backend: ProjectConfig["backend"] | undefined,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
DatabaseSetup,
|
||||
ORM,
|
||||
PackageManager,
|
||||
Payments,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
ServerDeploy,
|
||||
@@ -56,6 +57,10 @@ export function processFlags(options: CLIInput, projectName?: string) {
|
||||
config.auth = options.auth as Auth;
|
||||
}
|
||||
|
||||
if (options.payments !== undefined) {
|
||||
config.payments = options.payments as Payments;
|
||||
}
|
||||
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
validateAddonsAgainstFrontends,
|
||||
validateApiFrontendCompatibility,
|
||||
validateExamplesCompatibility,
|
||||
validatePaymentsCompatibility,
|
||||
validateServerDeployRequiresBackend,
|
||||
validateWebDeployRequiresWebFrontend,
|
||||
validateWorkersCompatibility,
|
||||
@@ -417,7 +418,7 @@ export function validateFullConfig(
|
||||
}
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
@@ -441,8 +442,19 @@ export function validateConfigForProgrammaticUse(
|
||||
|
||||
validateApiFrontendCompatibility(config.api, config.frontend);
|
||||
|
||||
validatePaymentsCompatibility(
|
||||
config.payments,
|
||||
config.auth,
|
||||
config.backend,
|
||||
config.frontend,
|
||||
);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
||||
validateAddonsAgainstFrontends(
|
||||
config.addons,
|
||||
config.frontend,
|
||||
config.auth,
|
||||
);
|
||||
}
|
||||
|
||||
validateExamplesCompatibility(
|
||||
|
||||
@@ -43,6 +43,10 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
||||
configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
|
||||
}
|
||||
|
||||
if (config.payments !== undefined) {
|
||||
configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
|
||||
}
|
||||
|
||||
if (config.addons !== undefined) {
|
||||
const addons = Array.isArray(config.addons)
|
||||
? config.addons
|
||||
|
||||
@@ -15,6 +15,7 @@ export function generateReproducibleCommand(config: ProjectConfig) {
|
||||
flags.push(`--orm ${config.orm}`);
|
||||
flags.push(`--api ${config.api}`);
|
||||
flags.push(`--auth ${config.auth}`);
|
||||
flags.push(`--payments ${config.payments}`);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
flags.push(`--addons ${config.addons.join(" ")}`);
|
||||
|
||||
Reference in New Issue
Block a user