Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
83 changes: 82 additions & 1 deletion apps/cli/test/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,43 @@ describe("Authentication Configurations", () => {
});

expectSuccess(result);
expect(result.projectDir).toBeDefined();
const projectDir = result.projectDir as string;
const authPackageJson = await fs.readJson(
path.join(projectDir, "packages/auth/package.json"),
);
expect(authPackageJson.dependencies.mongodb).toBe("^7.2.0");

const dbIndex = await fs.readFile(path.join(projectDir, "packages/db/src/index.ts"), "utf8");
expect(dbIndex).toContain("await mongoose.connect(env.DATABASE_URL);");
expect(dbIndex).toContain("mongoose.connection.getClient().db()");
expect(dbIndex).not.toContain(".catch(");
expect(dbIndex).not.toContain("myDB");

const todosRoute = await fs.readFile(
path.join(projectDir, "apps/web/src/routes/todos.tsx"),
"utf8",
);
expect(todosRoute).toContain("type TodoId = string");
expect(todosRoute).toContain("const handleToggleTodo = (id: TodoId");
expect(todosRoute).toContain("const handleDeleteTodo = (id: TodoId");

const todoRouter = await fs.readFile(
path.join(projectDir, "packages/api/src/routers/todo.ts"),
"utf8",
);
expect(todoRouter).toContain('import "@better-auth-mongodb/db";');
expect(todoRouter).toContain("id: todo.id");

const authModels = await fs.readFile(
path.join(projectDir, "packages/db/src/models/auth.model.ts"),
"utf8",
);
expect(authModels).toContain("const { ObjectId } = Schema.Types");
expect(authModels).toContain("_id: { type: ObjectId, auto: true }");
expect(authModels).toContain('userId: { type: ObjectId, ref: "User", required: true }');
expect(authModels).toContain("sessionSchema.index({ userId: 1 })");
expect(authModels).toContain("verificationSchema.index({ identifier: 1 })");
});

it("should add nextCookies plugin for Next.js self backend", async () => {
Expand Down Expand Up @@ -187,6 +224,50 @@ describe("Authentication Configurations", () => {
});

expectSuccess(result);
if (!result.projectDir) {
throw new Error("Expected projectDir to be defined");
}

const packageJson = await fs.readJson(path.join(result.projectDir, "package.json"));
const backendPackageJson = await fs.readJson(
path.join(result.projectDir, "packages/backend/package.json"),
);
const webPackageJson = await fs.readJson(
path.join(result.projectDir, "apps/web/package.json"),
);
const authFile = await fs.readFile(
path.join(result.projectDir, "packages/backend/convex/auth.ts"),
"utf8",
);
const httpFile = await fs.readFile(
path.join(result.projectDir, "packages/backend/convex/http.ts"),
"utf8",
);
const authClientFile = await fs.readFile(
path.join(result.projectDir, "apps/web/src/lib/auth-client.ts"),
"utf8",
);
const convexTsconfig = await fs.readFile(
path.join(result.projectDir, "packages/backend/convex/tsconfig.json"),
"utf8",
);
const convexEnvFile = await fs.readFile(
path.join(result.projectDir, "packages/backend/.env.local"),
"utf8",
);

expect(packageJson.workspaces.catalog["better-auth"]).toBe("~1.6.9");
expect(packageJson.workspaces.catalog["@convex-dev/better-auth"]).toBe("^0.12.2");
expect(backendPackageJson.dependencies["better-auth"]).toBe("catalog:");
expect(webPackageJson.dependencies["better-auth"]).toBe("catalog:");
expect(authFile).toContain("baseURL: process.env.CONVEX_SITE_URL");
expect(httpFile).toContain("authComponent.registerRoutes(http, createAuth, { cors: true })");
expect(authClientFile).toContain("plugins: [convexClient(), crossDomainClient()]");
expect(convexTsconfig).toContain('"types": ["node"]');
expect(convexEnvFile).toContain(
"# npx convex env set CONVEX_SITE_URL https://<YOUR_CONVEX_SITE_URL>",
);
expect(convexEnvFile).toContain("# CONVEX_SITE_URL=");
});

it("should scaffold react-router with Convex Better Auth wiring", async () => {
Expand Down Expand Up @@ -227,7 +308,7 @@ describe("Authentication Configurations", () => {

expect(rootFile).toContain("ConvexBetterAuthProvider");
expect(rootFile).toContain('import { authClient } from "@/lib/auth-client";');
expect(authClientFile).toContain("crossDomainClient(), convexClient()");
expect(authClientFile).toContain("convexClient(), crossDomainClient()");
expect(dashboardFile).toContain("Authenticated");
expect(dashboardFile).toContain("Unauthenticated");
});
Expand Down
141 changes: 141 additions & 0 deletions apps/cli/test/pnpm-workspace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, expect, it } from "bun:test";
import { readFile } from "node:fs/promises";
import path from "node:path";

import yaml from "yaml";

import { expectSuccess, runTRPCTest, type TestConfig } from "./test-utils";

async function readPnpmWorkspace(config: TestConfig) {
const result = await runTRPCTest({
...config,
packageManager: "pnpm",
install: false,
git: false,
});

expectSuccess(result);

const workspacePath = path.join(result.projectDir!, "pnpm-workspace.yaml");
const content = await readFile(workspacePath, "utf8");
return yaml.parse(content) as { allowBuilds?: Record<string, boolean> };
}

describe("pnpm workspace", () => {
it("adds build approvals for the Convex Better Auth Cloudflare stack", async () => {
const workspace = await readPnpmWorkspace({
projectName: "pnpm-convex-cloudflare",
frontend: ["tanstack-start"],
backend: "convex",
runtime: "none",
api: "none",
database: "none",
orm: "none",
auth: "better-auth",
payments: "none",
addons: ["turborepo"],
examples: ["todo"],
dbSetup: "none",
webDeploy: "cloudflare",
serverDeploy: "none",
});

expect(workspace.allowBuilds).toMatchObject({
esbuild: true,
msw: true,
sharp: true,
workerd: true,
});
});

it("adds build approvals for Prisma engines", async () => {
const workspace = await readPnpmWorkspace({
projectName: "pnpm-prisma-engines",
frontend: ["none"],
backend: "hono",
runtime: "bun",
api: "none",
database: "sqlite",
orm: "prisma",
auth: "none",
payments: "none",
addons: ["none"],
examples: ["none"],
dbSetup: "none",
webDeploy: "none",
serverDeploy: "none",
});

expect(workspace.allowBuilds).toMatchObject({
"@prisma/engines": true,
prisma: true,
});
});

it("adds build approvals for node runtime and workspace addons", async () => {
const workspace = await readPnpmWorkspace({
projectName: "pnpm-node-addons",
frontend: ["none"],
backend: "hono",
runtime: "node",
api: "none",
database: "none",
orm: "none",
auth: "none",
payments: "none",
addons: ["nx", "lefthook"],
examples: ["none"],
dbSetup: "none",
webDeploy: "none",
serverDeploy: "none",
});

expect(workspace.allowBuilds).toMatchObject({
esbuild: true,
lefthook: true,
nx: true,
});
});

it("adds only the React UI approval when no other build-script deps are expected", async () => {
const workspace = await readPnpmWorkspace({
projectName: "pnpm-react-ui",
frontend: ["tanstack-router"],
backend: "none",
runtime: "none",
api: "none",
database: "none",
orm: "none",
auth: "none",
payments: "none",
addons: ["none"],
examples: ["none"],
dbSetup: "none",
webDeploy: "none",
serverDeploy: "none",
});

expect(workspace.allowBuilds).toEqual({ msw: true });
});

it("does not add build approvals for stacks without lifecycle-script dependencies", async () => {
const workspace = await readPnpmWorkspace({
projectName: "pnpm-svelte",
frontend: ["svelte"],
backend: "none",
runtime: "none",
api: "none",
database: "none",
orm: "none",
auth: "none",
payments: "none",
addons: ["none"],
examples: ["none"],
dbSetup: "none",
webDeploy: "none",
serverDeploy: "none",
});

expect(workspace.allowBuilds).toBeUndefined();
});
});
13 changes: 9 additions & 4 deletions packages/template-generator/src/processors/auth-deps.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ProjectConfig } from "@better-t-stack/types";

import type { VirtualFileSystem } from "../core/virtual-fs";
import { addPackageDependency } from "../utils/add-deps";
import { addPackageDependency, type AvailableDependencies } from "../utils/add-deps";

const CONVEX_BETTER_AUTH_VERSION = "1.6.9";
// Intentional: @convex-dev/better-auth currently documents a pinned Better Auth range.
const CONVEX_BETTER_AUTH_VERSION = "~1.6.9";

export function processAuthDeps(vfs: VirtualFileSystem, config: ProjectConfig): void {
const { auth, backend } = config;
Expand Down Expand Up @@ -124,7 +125,7 @@ function processConvexAuthDeps(vfs: VirtualFileSystem, config: ProjectConfig): v
}

function processStandardAuthDeps(vfs: VirtualFileSystem, config: ProjectConfig): void {
const { auth, backend, frontend } = config;
const { auth, backend, frontend, orm } = config;
const authPath = "packages/auth/package.json";
const apiPath = "packages/api/package.json";
const webPath = "apps/web/package.json";
Expand Down Expand Up @@ -202,7 +203,11 @@ function processStandardAuthDeps(vfs: VirtualFileSystem, config: ProjectConfig):
}
} else if (auth === "better-auth") {
if (authExists) {
addPackageDependency({ vfs, packagePath: authPath, dependencies: ["better-auth"] });
const authDependencies: AvailableDependencies[] = ["better-auth"];
if (orm === "mongoose") {
authDependencies.push("mongodb");
}
addPackageDependency({ vfs, packagePath: authPath, dependencies: authDependencies });
if (hasNative) {
addPackageDependency({ vfs, packagePath: authPath, dependencies: ["@better-auth/expo"] });
}
Expand Down
7 changes: 4 additions & 3 deletions packages/template-generator/src/processors/db-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ function processPrismaDeps(
const { database, dbSetup } = config;

if (database === "mongodb") {
// Intentional: Prisma ORM v7 does not support MongoDB yet, so MongoDB stays on v6.
addPackageDependency({
vfs,
packagePath: dbPkgPath,
customDependencies: { "@prisma/client": "6.19.0" },
customDevDependencies: { prisma: "6.19.0" },
customDependencies: { "@prisma/client": "6.19.3" },
customDevDependencies: { prisma: "6.19.3" },
});
if (webExists) {
addPackageDependency({
vfs,
packagePath: webPkgPath,
customDependencies: { "@prisma/client": "6.19.0" },
customDependencies: { "@prisma/client": "6.19.3" },
});
}
return;
Expand Down
15 changes: 14 additions & 1 deletion packages/template-generator/src/processors/env-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ function buildConvexBackendVars(
auth: ProjectConfig["auth"],
examples: ProjectConfig["examples"],
): EnvVariable[] {
const hasReactRouter = frontend.includes("react-router");
const hasTanStackRouter = frontend.includes("tanstack-router");
const hasNextJs = frontend.includes("next");
const hasNative =
frontend.includes("native-bare") ||
Expand Down Expand Up @@ -294,6 +296,15 @@ function buildConvexBackendVars(
}

if (auth === "better-auth") {
if (hasReactRouter || hasTanStackRouter) {
vars.push({
key: "CONVEX_SITE_URL",
value: "",
condition: true,
comment: "Same as CONVEX_URL but ends in .site",
});
}

if (hasNative) {
vars.push({
key: "EXPO_PUBLIC_CONVEX_SITE_URL",
Expand Down Expand Up @@ -336,6 +347,8 @@ function buildConvexCommentBlocks(
auth: ProjectConfig["auth"],
examples: ProjectConfig["examples"],
): string {
const needsConvexSiteUrl =
frontend.includes("react-router") || frontend.includes("tanstack-router");
const hasNative =
frontend.includes("native-bare") ||
frontend.includes("native-uniwind") ||
Expand Down Expand Up @@ -370,7 +383,7 @@ function buildConvexCommentBlocks(
if (auth === "better-auth") {
commentBlocks += `# Set Convex environment variables
# npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
${hasWeb || hasNative ? `# npx convex env set SITE_URL ${defaultSiteUrl}\n` : ""}`;
${needsConvexSiteUrl ? "# npx convex env set CONVEX_SITE_URL https://<YOUR_CONVEX_SITE_URL>\n" : ""}${hasWeb || hasNative ? `# npx convex env set SITE_URL ${defaultSiteUrl}\n` : ""}`;
}

return commentBlocks;
Expand Down
21 changes: 10 additions & 11 deletions packages/template-generator/src/template-handlers/extras.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type { ProjectConfig } from "@better-t-stack/types";

import type { VirtualFileSystem } from "../core/virtual-fs";
import {
type TemplateData,
hasTemplatesWithPrefix,
processTemplatesFromPrefix,
processSingleTemplate,
} from "./utils";
import { type TemplateData, processSingleTemplate } from "./utils";

export async function processExtrasTemplates(
vfs: VirtualFileSystem,
Expand All @@ -19,17 +14,21 @@ export async function processExtrasTemplates(
const hasNuxt = config.frontend.includes("nuxt");

if (config.packageManager === "pnpm") {
if (hasTemplatesWithPrefix(templates, "extras")) {
processTemplatesFromPrefix(vfs, templates, "extras/pnpm-workspace.yaml", "", config);
}
processSingleTemplate(
vfs,
templates,
"extras/pnpm-workspace.yaml",
"pnpm-workspace.yaml",
config,
);
}

if (config.packageManager === "bun") {
processTemplatesFromPrefix(vfs, templates, "extras/bunfig.toml", "", config);
processSingleTemplate(vfs, templates, "extras/bunfig.toml", "bunfig.toml", config);
}

if (config.packageManager === "pnpm" && (hasNative || hasNuxt)) {
processTemplatesFromPrefix(vfs, templates, "extras/_npmrc", "", config);
processSingleTemplate(vfs, templates, "extras/_npmrc", ".npmrc", config);
}

if (
Expand Down
Loading
Loading