UNPKG

@better-auth/cli

Version:
1,217 lines (1,197 loc) 118 kB
#!/usr/bin/env node import { Command } from "commander"; import * as fs$2 from "node:fs"; import fs, { existsSync, readFileSync } from "node:fs"; import fs$1 from "node:fs/promises"; import * as path$1 from "node:path"; import path from "node:path"; import { createTelemetry, getTelemetryAuthConfig } from "@better-auth/telemetry"; import { getAdapter, getAuthTables, getMigrations } from "better-auth/db"; import chalk from "chalk"; import prompts from "prompts"; import yoctoSpinner from "yocto-spinner"; import * as z from "zod/v4"; import { initGetFieldName, initGetModelName } from "better-auth/adapters"; import prettier, { format } from "prettier"; import { capitalizeFirstLetter } from "@better-auth/core/utils"; import { produceSchema } from "@mrleebo/prisma-ast"; import babelPresetReact from "@babel/preset-react"; import babelPresetTypeScript from "@babel/preset-typescript"; import { BetterAuthError } from "@better-auth/core/error"; import { loadConfig } from "c12"; import { exec, execSync } from "node:child_process"; import * as os$1 from "node:os"; import os from "node:os"; import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts"; import { parse } from "dotenv"; import semver from "semver"; import Crypto from "node:crypto"; import { createAuthClient } from "better-auth/client"; import { deviceAuthorizationClient } from "better-auth/client/plugins"; import open from "open"; import { base64 } from "@better-auth/utils/base64"; import "dotenv/config"; //#region src/generators/drizzle.ts function convertToSnakeCase(str, camelCase) { if (camelCase) return str; return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase(); } const generateDrizzleSchema = async ({ options, file, adapter }) => { const tables = getAuthTables(options); const filePath = file || "./auth-schema.ts"; const databaseType = adapter.options?.provider; if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`); const fileExist = existsSync(filePath); let code = generateImport({ databaseType, tables, options }); const getModelName = initGetModelName({ schema: tables, usePlural: adapter.options?.adapterConfig?.usePlural }); const getFieldName = initGetFieldName({ schema: tables, usePlural: adapter.options?.adapterConfig?.usePlural }); for (const tableKey in tables) { const table = tables[tableKey]; const modelName = getModelName(tableKey); const fields = table.fields; function getType(name, field) { if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`); name = convertToSnakeCase(name, adapter.options?.camelCase); if (field.references?.field === "id") { const useNumberId$1 = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; if (useNumberId$1) if (databaseType === "pg") return `integer('${name}')`; else if (databaseType === "mysql") return `int('${name}')`; else return `integer('${name}')`; if (useUUIDs && databaseType === "pg") return `uuid('${name}')`; if (field.references.field) { if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`; } return `text('${name}')`; } const type = field.type; if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return { sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`, pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`, mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])` }[databaseType]; else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`); const dbTypeMap = { string: { sqlite: `text('${name}')`, pg: `text('${name}')`, mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')` }, boolean: { sqlite: `integer('${name}', { mode: 'boolean' })`, pg: `boolean('${name}')`, mysql: `boolean('${name}')` }, number: { sqlite: `integer('${name}')`, pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`, mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')` }, date: { sqlite: `integer('${name}', { mode: 'timestamp_ms' })`, pg: `timestamp('${name}')`, mysql: `timestamp('${name}', { fsp: 3 })` }, "number[]": { sqlite: `text('${name}', { mode: "json" })`, pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`, mysql: `text('${name}', { mode: 'json' })` }, "string[]": { sqlite: `text('${name}', { mode: "json" })`, pg: `text('${name}').array()`, mysql: `text('${name}', { mode: "json" })` }, json: { sqlite: `text('${name}', { mode: "json" })`, pg: `jsonb('${name}')`, mysql: `json('${name}', { mode: "json" })` } }[type]; if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`); return dbTypeMap[databaseType]; } let id = ""; const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial"; if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`; else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`; else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`; else id = `int("id").autoincrement().primaryKey()`; else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`; else if (databaseType === "pg") id = `text('id').primaryKey()`; else id = `text('id').primaryKey()`; let indexes = []; const assignIndexes = (indexes$1) => { if (!indexes$1.length) return ""; let code$1 = [`, (table) => [`]; for (const index of indexes$1) code$1.push(` ${index.type}("${index.name}").on(table.${index.on}),`); code$1.push(`]`); return code$1.join("\n"); }; const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", { id: ${id}, ${Object.keys(fields).map((field) => { const attr = fields[field]; const fieldName = attr.fieldName || field; let type = getType(fieldName, attr); if (attr.index && !attr.unique) indexes.push({ type: "index", name: `${modelName}_${fieldName}_idx`, on: fieldName }); else if (attr.index && attr.unique) indexes.push({ type: "uniqueIndex", name: `${modelName}_${fieldName}_uidx`, on: fieldName }); if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") { if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`; else type += `.defaultNow()`; } else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`; else type += `.default(${attr.defaultValue})`; if (attr.onUpdate && attr.type === "date") { if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`; } return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({ model: attr.references.model, field: attr.references.field })}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`; }).join(",\n ")} }${assignIndexes(indexes)});`; code += `\n${schema}\n`; } let relationsString = ""; for (const tableKey in tables) { const table = tables[tableKey]; const modelName = getModelName(tableKey); const oneRelations = []; const manyRelations = []; const manyRelationsSet = /* @__PURE__ */ new Set(); const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references); for (const [fieldName, field] of foreignFields) { const referencedModel = field.references.model; const relationKey = getModelName(referencedModel); const fieldRef = `${getModelName(tableKey)}.${getFieldName({ model: tableKey, field: fieldName })}`; const referenceRef = `${getModelName(referencedModel)}.${getFieldName({ model: referencedModel, field: field.references.field || "id" })}`; oneRelations.push({ key: relationKey, model: getModelName(referencedModel), type: "one", reference: { field: fieldRef, references: referenceRef, fieldName } }); } const otherModels = Object.entries(tables).filter(([modelName$1]) => modelName$1 !== tableKey); const modelRelationsMap = /* @__PURE__ */ new Map(); for (const [modelName$1, otherTable] of otherModels) { const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey)); if (foreignKeysPointingHere.length === 0) continue; const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique); const hasMany$1 = foreignKeysPointingHere.some(([_, field]) => !field.unique); modelRelationsMap.set(modelName$1, { modelName: modelName$1, hasUnique, hasMany: hasMany$1 }); } for (const { modelName: modelName$1, hasMany: hasMany$1 } of modelRelationsMap.values()) { const relationType = hasMany$1 ? "many" : "one"; let relationKey = getModelName(modelName$1); if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`; if (!manyRelationsSet.has(relationKey)) { manyRelationsSet.add(relationKey); manyRelations.push({ key: relationKey, model: getModelName(modelName$1), type: relationType }); } } const relationsByModel = /* @__PURE__ */ new Map(); for (const relation of oneRelations) if (relation.reference) { const modelKey = relation.key; if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []); relationsByModel.get(modelKey).push(relation); } const duplicateRelations = []; const singleRelations = []; for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations); else singleRelations.push(relations[0]); for (const relation of duplicateRelations) if (relation.reference) { const fieldName = relation.reference.fieldName; const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({ ${relation.key}: one(${relation.model}, { fields: [${relation.reference.field}], references: [${relation.reference.references}], }) }))`; relationsString += `\n${tableRelation}\n`; } const hasOne = singleRelations.length > 0; const hasMany = manyRelations.length > 0; if (hasOne && hasMany) { const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({ ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, { fields: [${relation.reference.field}], references: [${relation.reference.references}], })` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""} ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")} }))`; relationsString += `\n${tableRelation}\n`; } else if (hasOne) { const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({ ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, { fields: [${relation.reference.field}], references: [${relation.reference.references}], })` : "").filter((x) => x !== "").join(",\n ")} }))`; relationsString += `\n${tableRelation}\n`; } else if (hasMany) { const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({ ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")} }))`; relationsString += `\n${tableRelation}\n`; } } code += `\n${relationsString}`; return { code: await prettier.format(code, { parser: "typescript" }), fileName: filePath, overwrite: fileExist }; }; function generateImport({ databaseType, tables, options }) { const rootImports = ["relations"]; const coreImports = []; let hasBigint = false; let hasJson = false; for (const table of Object.values(tables)) { for (const field of Object.values(table.fields)) { if (field.bigint) hasBigint = true; if (field.type === "json") hasJson = true; } if (hasJson && hasBigint) break; } const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; coreImports.push(`${databaseType}Table`); coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text"); coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : ""); coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : ""); if (databaseType === "mysql") { const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint)); if (!!useNumberId || hasNonBigintNumber) coreImports.push("int"); if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum"); } else if (databaseType === "pg") { if (useUUIDs) rootImports.push("sql"); const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint)); const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id")); if (hasNonBigintNumber || (options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial") && hasFkToId) coreImports.push("integer"); } else coreImports.push("integer"); if (databaseType === "pg" && useUUIDs) coreImports.push("uuid"); if (hasJson) { if (databaseType === "pg") coreImports.push("jsonb"); if (databaseType === "mysql") coreImports.push("json"); } if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql"); const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique)); const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index)); if (hasIndexes) coreImports.push("index"); if (hasUniqueIndexes) coreImports.push("uniqueIndex"); return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`; } //#endregion //#region src/generators/kysely.ts const generateMigrations = async ({ options, file }) => { const { compileMigrations } = await getMigrations(options); const migrations = await compileMigrations(); return { code: migrations.trim() === ";" ? "" : migrations, fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql` }; }; //#endregion //#region src/utils/get-package-info.ts function getPackageInfo(cwd) { const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json"); return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); } function getPrismaVersion(cwd) { try { const packageInfo = getPackageInfo(cwd); const prismaVersion = packageInfo.dependencies?.prisma || packageInfo.devDependencies?.prisma || packageInfo.dependencies?.["@prisma/client"] || packageInfo.devDependencies?.["@prisma/client"]; if (!prismaVersion) return null; const match = prismaVersion.match(/(\d+)/); return match ? parseInt(match[1], 10) : null; } catch { return null; } } //#endregion //#region src/generators/prisma.ts const generatePrismaSchema = async ({ adapter, options, file }) => { const provider = adapter.options?.provider || "postgresql"; const tables = getAuthTables(options); const filePath = file || "./prisma/schema.prisma"; const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath)); const getModelName = initGetModelName({ schema: getAuthTables(options), usePlural: adapter.options?.adapterConfig?.usePlural }); const getFieldName = initGetFieldName({ schema: getAuthTables(options), usePlural: false }); let schemaPrisma = ""; if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(path.join(process.cwd(), filePath), "utf-8"); else schemaPrisma = getNewPrisma(provider, process.cwd()); const prismaVersion = getPrismaVersion(process.cwd()); if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => { const generator = builder.findByType("generator", { name: "client" }); if (generator && generator.properties) { const providerProp = generator.properties.find((prop) => prop.type === "assignment" && prop.key === "provider"); if (providerProp && providerProp.value === "\"prisma-client-js\"") providerProp.value = "\"prisma-client\""; } }); const manyToManyRelations = /* @__PURE__ */ new Map(); for (const table in tables) { const fields = tables[table]?.fields; for (const field in fields) { const attr = fields[field]; if (attr.references) { const referencedOriginalModel = attr.references.model; const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel)); if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set()); const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table)); manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap); } } } const indexedFields = /* @__PURE__ */ new Map(); for (const table in tables) { const fields = tables[table]?.fields; const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table)); indexedFields.set(modelName, []); for (const field in fields) { const attr = fields[field]; if (attr.index && !attr.unique) { const fieldName = attr.fieldName || field; indexedFields.get(modelName).push(fieldName); } } } const schema = produceSchema(schemaPrisma, (builder) => { for (const table in tables) { const originalTableName = table; const customModelName = tables[table]?.modelName || table; const modelName = capitalizeFirstLetter(getModelName(customModelName)); const fields = tables[table]?.fields; function getType({ isBigint, isOptional, type }) { if (type === "string") return isOptional ? "String?" : "String"; if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt"; if (type === "number") return isOptional ? "Int?" : "Int"; if (type === "boolean") return isOptional ? "Boolean?" : "Boolean"; if (type === "date") return isOptional ? "DateTime?" : "DateTime"; if (type === "json") { if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String"; return isOptional ? "Json?" : "Json"; } if (type === "string[]") { if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String"; return "String[]"; } if (type === "number[]") { if (provider === "sqlite" || provider === "mysql") return "String"; return "Int[]"; } } const prismaModel = builder.findByType("model", { name: modelName }); if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`); else { const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; if (useNumberId) builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())"); else if (useUUIDs && provider === "postgresql") builder.model(modelName).field("id", "String").attribute("id").attribute("default(dbgenerated(\"pg_catalog.gen_random_uuid()\"))").attribute("db.Uuid"); else builder.model(modelName).field("id", "String").attribute("id"); } for (const field in fields) { const attr = fields[field]; const fieldName = attr.fieldName || field; if (prismaModel) { if (builder.findByType("field", { name: fieldName, within: prismaModel.properties })) continue; } const useUUIDs = options.advanced?.database?.generateId === "uuid"; const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial"; const fieldBuilder = builder.model(modelName).field(fieldName, field === "id" && useNumberId ? getType({ isBigint: false, isOptional: false, type: "number" }) : getType({ isBigint: attr?.bigint || false, isOptional: !attr?.required, type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type })); if (field === "id") { fieldBuilder.attribute("id"); if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`); } if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`); if (attr.defaultValue !== void 0) { if (Array.isArray(attr.defaultValue)) { if (attr.type === "json") { if (Object.prototype.toString.call(attr.defaultValue[0]) === "[object Object]") { fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`); continue; } let jsonArray = []; for (const value of attr.defaultValue) jsonArray.push(value); fieldBuilder.attribute(`default("${JSON.stringify(jsonArray).replace(/"/g, "\\\"")}")`); continue; } if (attr.defaultValue.length === 0) { fieldBuilder.attribute(`default([])`); continue; } else if (typeof attr.defaultValue[0] === "string" && attr.type === "string[]") { let valueArray = []; for (const value of attr.defaultValue) valueArray.push(JSON.stringify(value)); fieldBuilder.attribute(`default([${valueArray}])`); } else if (typeof attr.defaultValue[0] === "number") { let valueArray = []; for (const value of attr.defaultValue) valueArray.push(`${value}`); fieldBuilder.attribute(`default([${valueArray}])`); } } else if (typeof attr.defaultValue === "object" && !Array.isArray(attr.defaultValue) && attr.defaultValue !== null) { if (Object.entries(attr.defaultValue).length === 0) { fieldBuilder.attribute(`default("{}")`); continue; } fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`); } if (field === "createdAt") fieldBuilder.attribute("default(now())"); else if (typeof attr.defaultValue === "string" && provider !== "mysql") fieldBuilder.attribute(`default("${attr.defaultValue}")`); else if (typeof attr.defaultValue === "boolean" || typeof attr.defaultValue === "number") fieldBuilder.attribute(`default(${attr.defaultValue})`); else if (typeof attr.defaultValue === "function") {} } if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt"); else if (attr.onUpdate) {} if (attr.references) { if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`); const referencedOriginalModelName = getModelName(attr.references.model); const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName; let action = "Cascade"; if (attr.references.onDelete === "no action") action = "NoAction"; else if (attr.references.onDelete === "set null") action = "SetNull"; else if (attr.references.onDelete === "set default") action = "SetDefault"; else if (attr.references.onDelete === "restrict") action = "Restrict"; const relationField = `relation(fields: [${getFieldName({ model: originalTableName, field: fieldName })}], references: [${getFieldName({ model: attr.references.model, field: attr.references.field })}], onDelete: ${action})`; builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${!attr.required ? "?" : ""}`).attribute(relationField); } if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text"); } if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) { const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel); const relatedFields = relatedTableName ? tables[relatedTableName]?.fields : {}; const [_fieldKey, fkFieldAttr] = Object.entries(relatedFields || {}).find(([_fieldName, fieldAttr]) => fieldAttr.references && getModelName(fieldAttr.references.model) === getModelName(originalTableName)) || []; const isUnique = fkFieldAttr?.unique === true; const fieldName = isUnique || adapter.options?.usePlural === true ? `${relatedModel.toLowerCase()}` : `${relatedModel.toLowerCase()}s`; if (!builder.findByType("field", { name: fieldName, within: prismaModel?.properties })) builder.model(modelName).field(fieldName, `${relatedModel}${isUnique ? "?" : "[]"}`); } const indexedFieldsForModel = indexedFields.get(modelName); if (indexedFieldsForModel && indexedFieldsForModel.length > 0) for (const fieldName of indexedFieldsForModel) { if (prismaModel) { if (prismaModel.properties.some((v) => v.type === "attribute" && v.name === "index" && JSON.stringify(v.args[0]?.value).includes(fieldName))) continue; } const field = Object.entries(fields).find(([key, attr]) => (attr.fieldName || key) === fieldName)?.[1]; let indexField = fieldName; if (provider === "mysql" && field && field.type === "string") { const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; if (field.references?.field === "id" && (useNumberId || useUUIDs)) indexField = `${fieldName}`; else indexField = `${fieldName}(length: 191)`; } builder.model(modelName).blockAttribute(`index([${indexField}])`); } const hasAttribute = builder.findByType("attribute", { name: "map", within: prismaModel?.properties }); const hasChanged = customModelName !== originalTableName; if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${getModelName(hasChanged ? customModelName : originalTableName)}`); } }); const schemaChanged = schema.trim() !== schemaPrisma.trim(); return { code: schemaChanged ? schema : "", fileName: filePath, overwrite: schemaPrismaExist && schemaChanged }; }; const getNewPrisma = (provider, cwd) => { const prismaVersion = getPrismaVersion(cwd); return `generator client { provider = "${prismaVersion && prismaVersion >= 7 ? "prisma-client" : "prisma-client-js"}" } datasource db { provider = "${provider}" url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`} }`; }; //#endregion //#region src/generators/index.ts const adapters = { prisma: generatePrismaSchema, drizzle: generateDrizzleSchema, kysely: generateMigrations }; const generateSchema = (opts) => { const adapter = opts.adapter; const generator = adapter.id in adapters ? adapters[adapter.id] : null; if (generator) return generator(opts); if (adapter.createSchema) return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({ code, fileName, overwrite })); console.error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`); process.exit(1); }; //#endregion //#region src/utils/add-cloudflare-modules.ts const createModule = () => { return `data:text/javascript;charset=utf-8,${encodeURIComponent(` const createStub = (label) => { const handler = { get(_, prop) { if (prop === "toString") return () => label; if (prop === "valueOf") return () => label; if (prop === Symbol.toPrimitive) return () => label; if (prop === Symbol.toStringTag) return "Object"; if (prop === "then") return undefined; return createStub(label + "." + String(prop)); }, apply(_, __, args) { return createStub(label + "()") }, construct() { return createStub(label + "#instance"); }, }; const fn = () => createStub(label + "()"); return new Proxy(fn, handler); }; class WorkerEntrypoint { constructor(ctx, env) { this.ctx = ctx; this.env = env; } } class DurableObject { constructor(state, env) { this.state = state; this.env = env; } } class RpcTarget { constructor(value) { this.value = value; } } const RpcStub = RpcTarget; const env = createStub("env"); const caches = createStub("caches"); const scheduler = createStub("scheduler"); const executionCtx = createStub("executionCtx"); export { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, caches, env, executionCtx, scheduler }; const defaultExport = { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, caches, env, executionCtx, scheduler, }; export default defaultExport; // jiti dirty hack: .unknown `)}`; }; const CLOUDFLARE_STUB_MODULE = createModule(); function addCloudflareModules(aliases, _cwd) { if (!aliases["cloudflare:workers"]) aliases["cloudflare:workers"] = CLOUDFLARE_STUB_MODULE; if (!aliases["cloudflare:test"]) aliases["cloudflare:test"] = CLOUDFLARE_STUB_MODULE; } //#endregion //#region src/utils/add-svelte-kit-env-modules.ts /** * Adds SvelteKit environment modules and path aliases * @param aliases - The aliases object to populate * @param cwd - Current working directory (optional, defaults to process.cwd()) */ function addSvelteKitEnvModules(aliases, cwd) { const workingDir = cwd || process.cwd(); aliases["$env/dynamic/private"] = createDataUriModule(createDynamicEnvModule()); aliases["$env/dynamic/public"] = createDataUriModule(createDynamicEnvModule()); aliases["$env/static/private"] = createDataUriModule(createStaticEnvModule(filterPrivateEnv("PUBLIC_", ""))); aliases["$env/static/public"] = createDataUriModule(createStaticEnvModule(filterPublicEnv("PUBLIC_", ""))); const svelteKitAliases = getSvelteKitPathAliases(workingDir); Object.assign(aliases, svelteKitAliases); } function getSvelteKitPathAliases(cwd) { const aliases = {}; const packageJsonPath = path.join(cwd, "package.json"); const svelteConfigPath = path.join(cwd, "svelte.config.js"); const svelteConfigTsPath = path.join(cwd, "svelte.config.ts"); let isSvelteKitProject = false; if (fs.existsSync(packageJsonPath)) try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); isSvelteKitProject = !!{ ...packageJson.dependencies, ...packageJson.devDependencies }["@sveltejs/kit"]; } catch {} if (!isSvelteKitProject) isSvelteKitProject = fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath); if (!isSvelteKitProject) return aliases; const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")]; for (const libPath of libPaths) if (fs.existsSync(libPath)) { aliases["$lib"] = libPath; for (const subPath of [ "server", "utils", "components", "stores" ]) { const subDir = path.join(libPath, subPath); if (fs.existsSync(subDir)) aliases[`$lib/${subPath}`] = subDir; } break; } aliases["$app/server"] = createDataUriModule(createAppServerModule()); const customAliases = getSvelteConfigAliases(cwd); Object.assign(aliases, customAliases); return aliases; } function getSvelteConfigAliases(cwd) { const aliases = {}; const configPaths = [path.join(cwd, "svelte.config.js"), path.join(cwd, "svelte.config.ts")]; for (const configPath of configPaths) if (fs.existsSync(configPath)) { try { const aliasMatch = fs.readFileSync(configPath, "utf-8").match(/alias\s*:\s*\{([^}]+)\}/); if (aliasMatch && aliasMatch[1]) { const aliasMatches = aliasMatch[1].matchAll(/['"`](\$[^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g); for (const match of aliasMatches) { const [, alias, target] = match; if (alias && target) { aliases[alias + "/*"] = path.resolve(cwd, target) + "/*"; aliases[alias] = path.resolve(cwd, target); } } } } catch {} break; } return aliases; } function createAppServerModule() { return ` // $app/server stub for CLI compatibility export default {}; // jiti dirty hack: .unknown `; } function createDataUriModule(module) { return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`; } function createStaticEnvModule(env) { return ` ${Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`).join("\n")} // jiti dirty hack: .unknown `; } function createDynamicEnvModule() { return ` export const env = process.env; // jiti dirty hack: .unknown `; } function filterPrivateEnv(publicPrefix, privatePrefix) { return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix)))); } function filterPublicEnv(publicPrefix, privatePrefix) { return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(publicPrefix) && (privatePrefix === "" || !k.startsWith(privatePrefix)))); } const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; const reserved = new Set([ "do", "if", "in", "for", "let", "new", "try", "var", "case", "else", "enum", "eval", "null", "this", "true", "void", "with", "await", "break", "catch", "class", "const", "false", "super", "throw", "while", "yield", "delete", "export", "import", "public", "return", "static", "switch", "typeof", "default", "extends", "finally", "package", "private", "continue", "debugger", "function", "arguments", "interface", "protected", "implements", "instanceof" ]); //#endregion //#region src/utils/get-tsconfig-info.ts function stripJsonComments(jsonString) { return jsonString.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).replace(/,(?=\s*[}\]])/g, ""); } function getTsconfigInfo(cwd, flatPath) { let tsConfigPath; if (flatPath) tsConfigPath = flatPath; else tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json"); try { const text$1 = fs.readFileSync(tsConfigPath, "utf-8"); return JSON.parse(stripJsonComments(text$1)); } catch (error) { throw error; } } //#endregion //#region src/utils/get-config.ts let possiblePaths = [ "auth.ts", "auth.tsx", "auth.js", "auth.jsx", "auth.server.js", "auth.server.ts", "auth/index.ts", "auth/index.tsx", "auth/index.js", "auth/index.jsx", "auth/index.server.js", "auth/index.server.ts" ]; possiblePaths = [ ...possiblePaths, ...possiblePaths.map((it) => `lib/server/${it}`), ...possiblePaths.map((it) => `server/auth/${it}`), ...possiblePaths.map((it) => `server/${it}`), ...possiblePaths.map((it) => `auth/${it}`), ...possiblePaths.map((it) => `lib/${it}`), ...possiblePaths.map((it) => `utils/${it}`) ]; possiblePaths = [ ...possiblePaths, ...possiblePaths.map((it) => `src/${it}`), ...possiblePaths.map((it) => `app/${it}`) ]; function resolveReferencePath(configDir, refPath) { const resolvedPath = path.resolve(configDir, refPath); if (refPath.endsWith(".json")) return resolvedPath; if (fs.existsSync(resolvedPath)) try { if (fs.statSync(resolvedPath).isFile()) return resolvedPath; } catch {} return path.resolve(configDir, refPath, "tsconfig.json"); } function getPathAliasesRecursive(tsconfigPath, visited = /* @__PURE__ */ new Set()) { if (visited.has(tsconfigPath)) return {}; visited.add(tsconfigPath); if (!fs.existsSync(tsconfigPath)) { console.warn(`Referenced tsconfig not found: ${tsconfigPath}`); return {}; } try { const tsConfig = getTsconfigInfo(void 0, tsconfigPath); const { paths = {}, baseUrl = "." } = tsConfig.compilerOptions || {}; const result = {}; const configDir = path.dirname(tsconfigPath); const obj = Object.entries(paths); for (const [alias, aliasPaths] of obj) for (const aliasedPath of aliasPaths) { const resolvedBaseUrl = path.resolve(configDir, baseUrl); const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias; const finalAliasedPath = aliasedPath.slice(-1) === "*" ? aliasedPath.slice(0, -1) : aliasedPath; result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath); } if (tsConfig.references) for (const ref of tsConfig.references) { const refAliases = getPathAliasesRecursive(resolveReferencePath(configDir, ref.path), visited); for (const [alias, aliasPath] of Object.entries(refAliases)) if (!(alias in result)) result[alias] = aliasPath; } return result; } catch (error) { console.warn(`Error parsing tsconfig at ${tsconfigPath}: ${error}`); return {}; } } function getPathAliases(cwd) { const tsConfigPath = path.join(cwd, "tsconfig.json"); if (!fs.existsSync(tsConfigPath)) return null; try { const result = getPathAliasesRecursive(tsConfigPath); addSvelteKitEnvModules(result); addCloudflareModules(result); return result; } catch (error) { console.error(error); throw new BetterAuthError("Error parsing tsconfig.json"); } } /** * .tsx files are not supported by Jiti. */ const jitiOptions = (cwd) => { const alias = getPathAliases(cwd) || {}; return { transformOptions: { babel: { presets: [[babelPresetTypeScript, { isTSX: true, allExtensions: true }], [babelPresetReact, { runtime: "automatic" }]] } }, extensions: [ ".ts", ".tsx", ".js", ".jsx" ], alias }; }; const isDefaultExport = (object) => { return typeof object === "object" && object !== null && !Array.isArray(object) && Object.keys(object).length > 0 && "options" in object; }; async function getConfig({ cwd, configPath, shouldThrowOnError = false }) { try { let configFile = null; if (configPath) { let resolvedPath = path.join(cwd, configPath); if (existsSync(configPath)) resolvedPath = configPath; const { config } = await loadConfig({ configFile: resolvedPath, dotenv: true, jitiOptions: jitiOptions(cwd), cwd }); if (!("auth" in config) && !isDefaultExport(config)) { if (shouldThrowOnError) throw new Error(`Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`); console.error(`[#better-auth]: Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`); process.exit(1); } configFile = "auth" in config ? config.auth?.options : config.options; } if (!configFile) for (const possiblePath of possiblePaths) try { const { config } = await loadConfig({ configFile: possiblePath, jitiOptions: jitiOptions(cwd), cwd }); if (Object.keys(config).length > 0) { configFile = config.auth?.options || config.default?.options || null; if (!configFile) { if (shouldThrowOnError) throw new Error("Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth."); console.error("[#better-auth]: Couldn't read your auth config."); console.log(""); console.log("[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth."); process.exit(1); } break; } } catch (e) { if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module")) { if (shouldThrowOnError) throw new Error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`); console.error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`); process.exit(1); } if (shouldThrowOnError) throw e; console.error("[#better-auth]: Couldn't read your auth config.", e); process.exit(1); } return configFile; } catch (e) { if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module")) { if (shouldThrowOnError) throw new Error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`); console.error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`); process.exit(1); } if (shouldThrowOnError) throw e; console.error("Couldn't read your auth config.", e); process.exit(1); } } //#endregion //#region src/commands/generate.ts async function generateAction(opts) { const options = z.object({ cwd: z.string(), config: z.string().optional(), output: z.string().optional(), y: z.boolean().optional(), yes: z.boolean().optional() }).parse(opts); const cwd = path.resolve(options.cwd); if (!existsSync(cwd)) { console.error(`The directory "${cwd}" does not exist.`); process.exit(1); } const config = await getConfig({ cwd, configPath: options.config }); if (!config) { console.error("No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag."); return; } const adapter = await getAdapter(config).catch((e) => { console.error(e.message); process.exit(1); }); const spinner$1 = yoctoSpinner({ text: "preparing schema..." }).start(); const schema = await generateSchema({ adapter, file: options.output, options: config }); spinner$1.stop(); if (!schema.code) { console.log("Your schema is already up to date."); try { await (await createTelemetry(config)).publish({ type: "cli_generate", payload: { outcome: "no_changes", config: getTelemetryAuthConfig(config, { adapter: adapter.id, database: typeof config.database === "function" ? "adapter" : "kysely" }) } }); } catch {} process.exit(0); } if (schema.overwrite) { let confirm$2 = options.y || options.yes; if (!confirm$2) confirm$2 = (await prompts({ type: "confirm", name: "confirm", message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(`${schema.overwrite ? "overwrite" : "append"}`)} the schema to the file?` })).confirm; if (confirm$2) { if (!existsSync(path.join(cwd, schema.fileName))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true }); if (schema.overwrite) await fs$1.writeFile(path.join(cwd, schema.fileName), schema.code); else await fs$1.appendFile(path.join(cwd, schema.fileName), schema.code); console.log(`🚀 Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`); try { await (await createTelemetry(config)).publish({ type: "cli_generate", payload: { outcome: schema.overwrite ? "overwritten" : "appended", config: getTelemetryAuthConfig(config) } }); } catch {} process.exit(0); } else { console.error("Schema generation aborted."); try { await (await createTelemetry(config)).publish({ type: "cli_generate", payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) } }); } catch {} process.exit(1); } } if (options.y) { console.warn("WARNING: --y is deprecated. Consider -y or --yes"); options.yes = true; } let confirm$1 = options.yes; if (!confirm$1) confirm$1 = (await prompts({ type: "confirm", name: "confirm", message: `Do you want to generate the schema to ${chalk.yellow(schema.fileName)}?` })).confirm; if (!confirm$1) { console.error("Schema generation aborted."); try { await (await createTelemetry(config)).publish({ type: "cli_generate", payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) } }); } catch {} process.exit(1); } if (!options.output) { if (!existsSync(path.dirname(path.join(cwd, schema.fileName)))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true }); } await fs$1.writeFile(options.output || path.join(cwd, schema.fileName), schema.code); console.log(`🚀 Schema was generated successfully!`); try { await (await createTelemetry(config)).publish({ type: "cli_generate", payload: { outcome: "generated", config: getTelemetryAuthConfig(config) } }); } catch {} process.exit(0); } const generate = new Command("generate").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the configuration file. defaults to the first configuration file found.").option("--output <output>", "the file to output to the generated schema").option("-y, --yes", "automatically answer yes to all prompts", false).option("--y", "(deprecated) same as --yes", false).action(generateAction); //#endregion //#region src/commands/info.ts function getSystemInfo() { const platform = os.platform(); const arch = os.arch(); const version = os.version(); const release = os.release(); const cpus = os.cpus(); const memory = os.totalmem(); const freeMemory = os.freemem(); return { platform, arch, version, release, cpuCount: cpus.length, cpuModel: cpus[0]?.model || "Unknown", totalMemory: `${(memory / 1024 / 1024 / 1024).toFixed(2)} GB`, freeMemory: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB` }; } function getNodeInfo() { return { version: process.version, env: process.env.NODE_ENV || "development" }; } function getPackageManager$1() { const userAgent = process.env.npm_config_user_agent || ""; if (userAgent.includes("yarn")) return { name: "yarn", version: getVersion("yarn") }; if (userAgent.includes("pnpm")) return { name: "pnpm", version: getVersion("pnpm") }; if (userAgent.includes("bun")) return { name: "bun", version: getVersion("bun") }; return { name: "npm", version: getVersion("npm") }; } function getVersion(command) { try { return execSync(`${command} --version`, { encoding: "utf8" }).trim(); } catch { return "Not installed"; } } function getFrameworkInfo(projectRoot) { const packageJsonPath = path.join(projectRoot, "package.json"); if (!existsSync(packageJsonPath)) return null; try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const frameworks = { next: deps["next"], react: deps["react"], vue: deps["vue"], nuxt: deps["nuxt"], svelte: deps["svelte"], "@sveltejs/kit": deps["@sveltejs/kit"], express: deps["express"], fastify: deps["fastify"], hono: deps["hono"], remix: deps["@remix-run/react"], astro: deps["astro"], solid: deps["solid-js"], qwik: deps["@builder.io/qwik"] }; const installedFrameworks = Object.entries(frameworks).filter(([_, version]) => version).map(([name, version]) => ({ name, version })); return installedFrameworks.length > 0 ? installedFrameworks : null; } catch { return null; } } function getDatabaseInfo(projectRoot) { const packageJsonPath = path.join(projectRoot, "package.json"); if (!existsSync(packageJsonPath)) return null; try { const packageJson = JSON.parse(readFileSync(packageJsonPat