create-dynamic-app
Version:
CLI tool to generate sample applications using Dynamic's web3 authentication
447 lines (380 loc) • 11.6 kB
text/typescript
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)
})
}