Skip to content
Open
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
7 changes: 4 additions & 3 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Generate a Better Auth Cloudflare project with D1, KV, R2, or Hyperdrive. This C

☁️ Handles Cloudflare resource creation:

- Runs `wrangler d1/kv/r2 create` commands and configures `wrangler.toml`
- Runs `wrangler d1/kv/r2 create` commands and configures your wrangler config file

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and I prefer the CLI generate use toml by default still

- Sets up Hyperdrive connections and auth integrations

📦 Runs initial setup: `@better-auth/cli generate`, `drizzle-kit generate`, and optionally applies migrations
Expand Down Expand Up @@ -69,7 +69,7 @@ npx @better-auth-cloudflare/cli migrate # Interactive
npx @better-auth-cloudflare/cli migrate --migrate-target=dev # Non-interactive
```

The migrate command automatically detects your database configuration from `wrangler.toml`. It supports:
The migrate command automatically detects your database configuration from your wrangler config file (`wrangler.json`, `wrangler.jsonc`, or `wrangler.toml`). It supports:

- **D1 databases**: Offers migration options (dev/remote)
- **Hyperdrive databases**: Shows informational message
Expand Down Expand Up @@ -121,6 +121,7 @@ The migrate command automatically detects your database configuration from `wran

```
--migrate-target=<target> For migrate command: dev | remote | skip (default: skip)
--config=<path> Path to wrangler config file (default: auto-detect wrangler.json/jsonc/toml)
```

## Examples
Expand Down Expand Up @@ -191,7 +192,7 @@ Creates a new Better Auth Cloudflare project from Hono or OpenNext.js templates,
**Error `...Error [ERR_REQUIRE_ESM]: require() of ES Module...`**:

Loading ECMAScript modules using `require()` should be supported by your nodejs.
Make sure your node version is at least `v23.0.0`, `v22.12.0`, or `v20.19.0`, depending on the major version you use.
Make sure your node version is at least `v23.0.0`, `v22.12.0`, or `v20.19.0`, depending on the major version you use.
Read more [here](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require)

## Related
Expand Down
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"devDependencies": {
"@iarna/toml": "^2.2.5",
"jsonc-simple-parser": "^1.0.0",
"@types/node": "^22.7.5",
"@types/prompts": "^2.4.9",
"bun-types": "^1.1.33",
Expand Down
42 changes: 26 additions & 16 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
extractD1DatabaseId,
extractHyperdriveId,
extractKvNamespaceId,
findWranglerConfig,
findWranglerConfigWithExplicitPath,
initializeGitRepository,
parseWranglerConfig,
parseWranglerToml,
replaceDemoCorsOrigin,
updateD1BlockWithId,
Expand Down Expand Up @@ -779,27 +782,30 @@ async function migrate(cliArgs?: CliArgs) {
// Check for updates in the background
checkForUpdates();

// Check if we're in a project directory by looking for wrangler.toml
const wranglerPath = join(process.cwd(), "wrangler.toml");
if (!existsSync(wranglerPath)) {
fatal("No wrangler.toml found. Please run this command from a Cloudflare Workers project directory.");
}
// Check for explicit config path from --config flag
const explicitConfigPath = cliArgs?.config as string | undefined;

// Read and parse wrangler.toml to detect database configurations
debugLog(`Reading wrangler.toml from: ${wranglerPath}`);
let wranglerContent: string;
try {
wranglerContent = readFileSync(wranglerPath, "utf8");
} catch (e) {
fatal("Failed to read wrangler.toml");
return;
// Find wrangler config file (supports wrangler.json, wrangler.jsonc, wrangler.toml)
const config = explicitConfigPath
? findWranglerConfigWithExplicitPath(explicitConfigPath)
: findWranglerConfig(process.cwd());

Comment on lines +785 to +792
if (!config) {
fatal(
"No wrangler config found. Looked for wrangler.json, wrangler.jsonc, and wrangler.toml in " +
(explicitConfigPath ? explicitConfigPath : process.cwd()) +
". Use --config to specify a custom path."
);
}

const { databases, hasMultipleDatabases } = parseWranglerToml(wranglerContent);
debugLog(`Found ${databases.length} database configuration(s) in wrangler.toml`);
// config is guaranteed non-null after fatal call above
const wranglerConfig = config!;
debugLog(`Reading wrangler config from: ${wranglerConfig.path}`);
const { databases, hasMultipleDatabases } = parseWranglerConfig(wranglerConfig.content, wranglerConfig.format);
debugLog(`Found ${databases.length} database configuration(s) in ${wranglerConfig.path}`);

if (databases.length === 0) {
fatal("No database configurations found in wrangler.toml. Please configure a D1 or Hyperdrive database.");
fatal("No database configurations found in wrangler config. Please configure a D1 or Hyperdrive database.");
}

const pm = detectPackageManager(process.cwd());
Expand Down Expand Up @@ -2296,6 +2302,7 @@ function printHelp() {
`\n` +
`Migrate command arguments:\n` +
` --migrate-target=<target> For migrate command: dev | remote | skip (default: skip)\n` +
` --config=<path> Path to wrangler config file (default: auto-detect wrangler.json/jsonc/toml)\n` +
`\n` +
`Examples:\n` +
` # Create a Hono app with D1 database\n` +
Expand Down Expand Up @@ -2326,6 +2333,9 @@ function printHelp() {
` # Run migration workflow with non-interactive target\n` +
` npx @better-auth-cloudflare/cli migrate --migrate-target=dev\n` +
`\n` +
` # Run migration with explicit wrangler config file\n` +
` npx @better-auth-cloudflare/cli migrate --config=wrangler.jsonc\n` +
`\n` +
`Creates a new Better Auth Cloudflare project from Hono or OpenNext.js templates,\n` +
`optionally creating Cloudflare D1, KV, R2, or Hyperdrive resources for you.\n` +
`The migrate command runs auth:update, db:generate, and optionally db:migrate.\n` +
Expand Down
146 changes: 146 additions & 0 deletions cli/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,152 @@ export function parseWranglerToml(tomlContent: string): {
};
}

export type WranglerConfigFormat = "toml" | "json" | "jsonc";

export interface WranglerConfigResult {
path: string;
content: string;
format: WranglerConfigFormat;
}

export function findWranglerConfig(cwd: string = process.cwd()): WranglerConfigResult | null {
const { existsSync } = require("fs");
const { join } = require("path");

const configFiles: Array<{ name: string; format: WranglerConfigFormat }> = [
{ name: "wrangler.json", format: "json" },
{ name: "wrangler.jsonc", format: "jsonc" },
{ name: "wrangler.toml", format: "toml" },
];

for (const configFile of configFiles) {
const configPath = join(cwd, configFile.name);
if (existsSync(configPath)) {
const { readFileSync } = require("fs");
const content = readFileSync(configPath, "utf8");
return {
path: configPath,
content,
format: configFile.format,
};
}
}

return null;
}

export function findWranglerConfigWithExplicitPath(configPath: string): WranglerConfigResult | null {
const { existsSync, readFileSync } = require("fs");
const { extname, resolve } = require("path");

const resolvedPath = resolve(configPath);

if (!existsSync(resolvedPath)) {
return null;
}

const ext = extname(configPath).toLowerCase();
let format: WranglerConfigFormat;

switch (ext) {
case ".json":
format = "json";
break;
case ".jsonc":
format = "jsonc";
break;
case ".toml":
format = "toml";
break;
default:
return null;
}

const content = readFileSync(resolvedPath, "utf8");
return {
path: resolvedPath,
content,
format,
};
}

export function parseWranglerConfig(
content: string,
format: WranglerConfigFormat
): {
databases: DatabaseConfig[];
hasMultipleDatabases: boolean;
} {
const databases: DatabaseConfig[] = [];

Comment on lines +256 to +257
if (format === "toml") {
const toml = require("@iarna/toml");
const parsed = toml.parse(content);
return parseWranglerConfigObject(parsed);
}

const jsonc = require("jsonc-simple-parser");
const parsed = jsonc.parse(content);
return parseWranglerConfigObject(parsed);
}

function parseWranglerConfigObject(parsed: Record<string, unknown>): {
databases: DatabaseConfig[];
hasMultipleDatabases: boolean;
} {
const databases: DatabaseConfig[] = [];

const d1Databases = parsed.d1_databases as Array<Record<string, unknown>> | undefined;
if (d1Databases && Array.isArray(d1Databases)) {
Comment on lines +269 to +276

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer something more typesafe, but obviously let's not concern ourselves with irrelevant wrangler fields

for (const db of d1Databases) {
const binding = db.binding as string | undefined;
if (binding) {
databases.push({
type: "d1",
binding,
name: db.database_name as string | undefined,
id: db.database_id as string | undefined,
});
}
}
}

const hyperdriveConfigs = parsed.hyperdrive as Record<string, unknown> | undefined;
if (hyperdriveConfigs) {
if (Array.isArray(hyperdriveConfigs)) {
for (const hd of hyperdriveConfigs) {
const binding = hd.binding as string | undefined;
if (binding) {
databases.push({
type: "hyperdrive",
binding,
id: hd.id as string | undefined,
});
}
}
} else if (typeof hyperdriveConfigs === "object") {
for (const hd of Object.values(hyperdriveConfigs)) {
if (typeof hd === "object" && hd !== null) {
const hdObj = hd as Record<string, unknown>;
const binding = hdObj.binding as string | undefined;
if (binding) {
databases.push({
type: "hyperdrive",
binding,
id: hdObj.id as string | undefined,
});
}
}
}
}
}

return {
databases,
hasMultipleDatabases: databases.length > 1,
};
}

// Functions to extract IDs from wrangler command responses
export function extractD1DatabaseId(wranglerOutput: string): string | null {
try {
Expand Down
Loading
Loading