UNPKG

create-dynamic-app

Version:

CLI tool to generate sample applications using Dynamic's web3 authentication

447 lines (380 loc) 11.6 kB
#!/usr/bin/env node import { execSync } from "node:child_process" import { promises as fs } from "node:fs" import chalk from "chalk" import figlet from "figlet" import { select, input } from "@inquirer/prompts" import { simpleGit } from "simple-git" import { generateNextApp } from "./generate-next" import { generateReactApp } from "./generate-react" import type { Chain } from "./types" import { promptForChains, chainOptions } from "./utils" const FRAMEWORKS = { NEXT: "nextjs", REACT: "react", REACT_NATIVE: "react-native", SCAFFOLD_ETH: "scaffold-eth", } as const type Framework = (typeof FRAMEWORKS)[keyof typeof FRAMEWORKS] const PACKAGE_MANAGERS = { NPM: "npm", YARN: "yarn", PNPM: "pnpm", BUN: "bun", } as const type PackageManager = (typeof PACKAGE_MANAGERS)[keyof typeof PACKAGE_MANAGERS] const ETH_LIBRARIES = { VIEM: "viem", ETHERS: "ethers", } as const type EthLibrary = (typeof ETH_LIBRARIES)[keyof typeof ETH_LIBRARIES] const FRAMEWORK_CHOICES = [ { name: "NextJS", value: FRAMEWORKS.NEXT }, { name: "ReactJS", value: FRAMEWORKS.REACT }, { name: "React Native", value: FRAMEWORKS.REACT_NATIVE }, ] const LIBRARY_CHOICES = [ { name: "Happy with Viem", value: ETH_LIBRARIES.VIEM }, { name: "I need Ethers", value: ETH_LIBRARIES.ETHERS }, ] const PACKAGE_MANAGER_CHOICES = [ { name: "npm", value: PACKAGE_MANAGERS.NPM }, { name: "yarn", value: PACKAGE_MANAGERS.YARN }, { name: "pnpm", value: PACKAGE_MANAGERS.PNPM }, { name: "bun", value: PACKAGE_MANAGERS.BUN }, ] const REPO_BASE_URL = "https://github.com/dynamic-labs/" interface CommandLineArgs { projectName?: string packageManager?: PackageManager framework?: Framework library?: EthLibrary wagmi?: boolean chains?: Chain[] } function showHelp(): never { console.log(` ${chalk.yellow("CREATE DYNAMIC APP")} - A CLI tool to generate web3 applications with Dynamic authentication ${chalk.cyan("USAGE:")} npx create-dynamic-app [project-name] [options] ${chalk.cyan("OPTIONS:")} --help, -h Show this help message --framework, -f <framework> Specify framework (nextjs, react, react-native) --pm, --package-manager <manager> Specify package manager (npm, yarn, pnpm, bun) --library, -l <library> Specify Ethereum library (viem, ethers) --wagmi <boolean> Use Wagmi with Viem (true, false) --chains, -c <chains> Comma-separated list of chains to include (ethereum,solana,flow,starknet,algorand,cosmos,bitcoin,sui) ${chalk.cyan("EXAMPLES:")} npx create-dynamic-app my-app npx create-dynamic-app my-app --pm yarn npx create-dynamic-app my-app --framework nextjs --chains ethereum,solana npx create-dynamic-app my-app --framework react --library viem --wagmi true --pm pnpm `) process.exit(0) } function parseArgs(): CommandLineArgs { const args = process.argv.slice(2) const result: CommandLineArgs = {} const projectNameIndex = args.findIndex((arg) => !arg.startsWith("-")) if (projectNameIndex !== -1) { result.projectName = args[projectNameIndex] } for (let i = 0; i < args.length; i++) { const arg = args[i] const nextArg = args[i + 1] const hasValue = nextArg && !nextArg.startsWith("-") switch (arg) { case "--pm": case "--package-manager": if ( hasValue && Object.values(PACKAGE_MANAGERS).includes( nextArg.toLowerCase() as PackageManager ) ) { result.packageManager = nextArg.toLowerCase() as PackageManager i++ } break case "--framework": case "-f": if ( hasValue && Object.values(FRAMEWORKS).includes(nextArg.toLowerCase() as Framework) ) { result.framework = nextArg.toLowerCase() as Framework i++ } break case "--library": case "-l": if ( hasValue && Object.values(ETH_LIBRARIES).includes( nextArg.toLowerCase() as EthLibrary ) ) { result.library = nextArg.toLowerCase() as EthLibrary i++ } break case "--wagmi": if (hasValue) { result.wagmi = nextArg.toLowerCase() === "true" i++ } break case "--chains": case "-c": if (hasValue) { const requestedChains = nextArg.toLowerCase().split(",") const validChains = chainOptions.filter((chain) => requestedChains.includes(chain.name.toLowerCase()) ) if (validChains.length > 0) { result.chains = validChains i++ } } break } } return result } async function checkDirectoryExists(directory: string): Promise<boolean> { try { await fs.access(directory) return true } catch { return false } } function generateRepoUrl( framework: Framework, library = "", wagmi = "" ): string { switch (framework) { case FRAMEWORKS.REACT_NATIVE: return `${REPO_BASE_URL}react-native-expo` case FRAMEWORKS.SCAFFOLD_ETH: return `${REPO_BASE_URL}hackathon-starter-kit` default: return `${REPO_BASE_URL}${framework}${library ? `-${library}` : ""}${ wagmi ? `-${wagmi}` : "" }` } } async function cloneRepository( repoUrl: string, directoryName: string ): Promise<boolean> { try { await simpleGit().clone(repoUrl, directoryName) await simpleGit(directoryName).removeRemote("origin") return true } catch (error) { console.error( chalk.red("Failed to clone repository:"), error instanceof Error ? error.message : String(error) ) return false } } function getInstallCommand(packageManager: PackageManager): string { const commands = { [PACKAGE_MANAGERS.NPM]: "npm install", [PACKAGE_MANAGERS.YARN]: "yarn", [PACKAGE_MANAGERS.PNPM]: "pnpm install", [PACKAGE_MANAGERS.BUN]: "bun install", } return commands[packageManager] || "npm install" } function getRunCommand(packageManager: PackageManager): string { const commands = { [PACKAGE_MANAGERS.NPM]: "npm run dev", [PACKAGE_MANAGERS.YARN]: "yarn dev", [PACKAGE_MANAGERS.PNPM]: "pnpm dev", [PACKAGE_MANAGERS.BUN]: "bun dev", } return commands[packageManager] || "npm run dev" } async function generateApp( directoryName: string, framework: Framework, library: string, wagmi: string, selectedChains: Chain[] ): Promise<void> { const useViem = library === ETH_LIBRARIES.VIEM const useWagmi = wagmi === "wagmi" if (framework === FRAMEWORKS.NEXT) { await generateNextApp( process.cwd(), directoryName, useViem, useWagmi, selectedChains ) } else if (framework === FRAMEWORKS.REACT) { await generateReactApp( process.cwd(), directoryName, useViem, useWagmi, selectedChains ) } } async function handleSpecialFrameworks( framework: Framework, directoryName: string, packageManager: PackageManager ): Promise<void> { const repoUrl = generateRepoUrl(framework as Framework) const success = await cloneRepository(repoUrl, directoryName) if (!success) return const installCommand = getInstallCommand(packageManager) const setupMessage = framework === FRAMEWORKS.SCAFFOLD_ETH ? `Project setup complete! Cd into the ${directoryName} directory and run ${installCommand}.` : `Project setup complete! Check it out in the ${directoryName} directory.` console.log(chalk.green(setupMessage)) } async function askForNewDirectory(originalDirectory: string): Promise<string> { const newDirectory = await input({ message: `The directory "${originalDirectory}" already exists. Enter a new directory name:`, validate: (value) => value ? true : "Please enter a valid directory name.", }) return (await checkDirectoryExists(newDirectory)) ? askForNewDirectory(newDirectory) : newDirectory } function getDocsUrl(framework: Framework): string { return framework === FRAMEWORKS.SCAFFOLD_ETH ? "https://github.com/dynamic-labs/hackathon-starter-kit" : "https://app.dynamic.xyz/dashboard/developer/api" } function getDocsMessage(framework: Framework, docsUrl: string): string { switch (framework) { case FRAMEWORKS.SCAFFOLD_ETH: return `Check out the documentation for the Scaffold Eth Hacker Edition at ${docsUrl} to get started!` default: return `Make sure to grab your Environment ID from ${docsUrl} and add it to the DynamicContextProvider!` } } function displayWelcomeBanner(): void { try { console.log( chalk.yellow( figlet.textSync("Create Dynamic App", { horizontalLayout: "full" }) ) ) } catch { console.log(chalk.yellow("\n=== CREATE DYNAMIC APP ===\n")) } console.log( chalk.green( "Welcome to the Dynamic App Creator! Follow the prompts to set up your project." ) ) } export async function main(): Promise<void> { const args = parseArgs() const framework = args.framework || (await select({ message: "What framework would you like to use?", choices: FRAMEWORK_CHOICES, })) let directoryName = args.projectName || (framework === FRAMEWORKS.SCAFFOLD_ETH ? "my-hacker-project" : "my-dynamic-project") if (await checkDirectoryExists(directoryName)) { directoryName = await askForNewDirectory(directoryName) } const packageManager = args.packageManager || ((await select({ message: "Which package manager would you like to use?", choices: PACKAGE_MANAGER_CHOICES, default: PACKAGE_MANAGERS.NPM, })) as PackageManager) if ( framework === FRAMEWORKS.REACT_NATIVE || framework === FRAMEWORKS.SCAFFOLD_ETH ) { await handleSpecialFrameworks(framework, directoryName, packageManager) const docsUrl = getDocsUrl(framework) console.log(chalk.magenta(getDocsMessage(framework, docsUrl))) return } const selectedChains = args.chains || (await promptForChains()) const hasEthereum = selectedChains.some((chain) => chain.name === "Ethereum") let library = "" let wagmi = "" if (hasEthereum) { library = args.library || (await select({ message: "For Ethereum interactions, are you happy with Viem, or do you need Ethers?", choices: LIBRARY_CHOICES, })) if (library === ETH_LIBRARIES.VIEM) { wagmi = args.wagmi ? "wagmi" : await select({ message: "Do you want to use Wagmi on top of Viem?", choices: [ { name: "No", value: "" }, { name: "Yes", value: "wagmi" }, ], }) } } await generateApp(directoryName, framework, library, wagmi, selectedChains) const installCommand = getInstallCommand(packageManager) const runCommand = getRunCommand(packageManager) console.log( chalk.blue( `Installing dependencies in ${directoryName} using ${packageManager}... This might take a moment.` ) ) try { execSync(installCommand, { cwd: directoryName, stdio: "inherit" }) console.log(chalk.green("Dependencies installed successfully!")) console.log(chalk.yellow("\nNext steps:")) console.log(chalk.cyan(`1. cd ${directoryName}`)) console.log( chalk.cyan(`2. Run ${runCommand} to start the development server`) ) } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) console.error(chalk.red(`Failed to install dependencies: ${errorMessage}`)) console.log( chalk.yellow( `Please try running '${installCommand}' manually in the ${directoryName} directory.` ) ) } const docsUrl = getDocsUrl(framework) console.log(chalk.magenta(getDocsMessage(framework, docsUrl))) } if (process.argv.includes("--help") || process.argv.includes("-h")) { showHelp() } displayWelcomeBanner() if (!process.env.TEST_ENV) { await main().catch((error) => { console.error( chalk.red("An error occurred:"), error instanceof Error ? error.message : String(error) ) process.exit(1) }) }