#!/usr/bin/env bun import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { generate, collectTables } from "./contracts/typegen/generator.ts"; import { format } from "./core/format.ts"; import { loadConfig } from "./core/config.ts"; import { setupWrangler } from "./adapters/cloudflare-wrangler.ts"; import { setupVercel } from "./adapters/vercel-vercel.ts"; import { backportProject } from "./core/backport.ts"; import { createTemplate, muddleTemplates, type MuddleTemplate } from "./core/templates.ts"; function progress(event: { current: number; total: number; route: string }): void { const width = 20; const filled = Math.round((event.current / Math.max(1, event.total)) * width); console.log(`[${"#".repeat(filled)}${".".repeat(width - filled)}] ${event.current}/${event.total} ${event.route}`); } async function dev(entry: string, root: string, port: number): Promise { const moduleUrl = pathToFileURL(entry).href; const source = `const project = await import(${JSON.stringify(moduleUrl)}); if (!project.app || typeof project.app.fetch !== "function") throw new Error("Project entry must export an app with fetch(request)"); const server = Bun.serve({ port: ${port}, fetch: (request) => project.app.fetch(request) }); console.log(\`muddle dev listening on \${server.url}\`); await new Promise(() => {});`; const child = Bun.spawn([process.execPath, "--watch", "--eval", source], { cwd: root, stdin: "inherit", stdout: "inherit", stderr: "inherit", }); const stop = () => child.kill(); process.once("SIGINT", stop); process.once("SIGTERM", stop); await child.exited; } async function main(): Promise { const args = process.argv.slice(2); const command = args[0] ?? "help"; const portFlag = args.indexOf("--port"); const requestedPort = portFlag === -1 ? undefined : Number(args[portFlag + 1]); const root = args.slice(1).find((argument, index, values) => { if (argument.startsWith("--")) return false; return values[index - 1] !== "--port"; }) ?? process.cwd(); if (requestedPort !== undefined && (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65_535)) { throw new Error("--port must be an integer from 1 through 65535"); } if (command === "typegen") { const result = await generate({ root }); for (const file of result.written) console.log(`muddle generated ${relative(process.cwd(), file)}`); return; } if (command === "backport") { const source = args[1] ? resolve(args[1]) : process.cwd(); const destination = args[2] ? resolve(args[2]) : resolve(source, "muddle"); const result = await backportProject(source, destination); console.log(`muddle backported ${result.source} project to ${result.output} (${result.files.length} files)`); return; } if (command === "templates") { const name = args[1] as MuddleTemplate | undefined; if (!name) { console.log(`Available templates: ${muddleTemplates.join(", ")}`); console.log("Usage: muddle templates [destination]"); return; } if (!muddleTemplates.includes(name)) throw new Error(`Unknown template ${name}. Choose one of: ${muddleTemplates.join(", ")}`); const result = await createTemplate(name, args[2] ? resolve(args[2]) : resolve(process.cwd(), name)); console.log(`muddle created ${result.name} template at ${result.output} (${result.files.length} files)`); return; } if (command === "repo") { const target = resolve(root); const tables = await collectTables(target, ["**/*.table.ts"]); const out = join(target, "repositories"); await mkdir(out, { recursive: true }); for (const table of tables) { const rowType = table.fields .map((field) => { const tsType = field.type; const optional = field.nullable ? "?" : ""; return `\t${field.property}${optional}: ${tsType};`; }) .join("\n"); const typeName = `${table.name[0]?.toUpperCase() ?? ""}${table.name.slice(1)}Row`; const source = `import { Repository } from "@muddle/database";\n\nexport type ${typeName} = {\n${rowType}\n};\n\nexport class ${table.name[0]?.toUpperCase() ?? ""}${table.name.slice(1)}Repository extends Repository<${typeName}> {}\n`; const file = join(out, `${table.name}.repository.ts`); await writeFile(file, source); console.log(`muddle generated ${relative(target, file)}`); } return; } if (command === "fmt") { const check = process.argv.includes("--check"); const changed = await format(resolve(root === "--check" ? process.cwd() : root), { check }); for (const file of changed) console.log(`${check ? "Unformatted" : "Formatted"} ${file}`); if (check && changed.length) process.exitCode = 1; return; } if (command === "build" || command === "dev") { const target = resolve(root); const config = await loadConfig(target); const entry = resolve(target, config.entry ?? "index.ts"); if (!existsSync(entry)) throw new Error(`No index.ts found in ${root}`); if (config.adapter?.name === "cloudflare-wrangler") await setupWrangler(target); if (config.adapter?.name === "vercel-vercel") await setupVercel(target); const project = await import(entry) as { build?: (options?: { progress?: typeof progress }) => Promise; app?: unknown }; if (command === "build") { if (!project.build) throw new Error("Project must export build() for muddle build"); await project.build({ progress }); return; } else if (command === "dev") { if (!project.app || typeof (project.app as { fetch?: unknown }).fetch !== "function") { throw new Error("Project entry must export an app with fetch(request) for muddle dev"); } await dev(entry, target, requestedPort ?? config.adapter?.port ?? 3000); return; } return; } console.log("muddle [project-root] [--port number]"); } await main();