UNPKG

create-dynamic-app

Version:

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

237 lines (215 loc) 6.69 kB
import chalk from "chalk" import fetch from "node-fetch" import { execSync } from "node:child_process" import fs from "node:fs/promises" import { existsSync } from "node:fs" import path from "node:path" import { generateDynamicLibContent, generateLayoutContent, generateMethodsContent, generateProvidersContent, methodsCss, pageContent, pageCssContent, useDarkMode, wagmiContent, } from "./next-templates" import { addSolanaDependencies, createConfigOverrides, createNonSolanaConfig, } from "./next-templates/solana-config" import { DEPENDENCY_VERSIONS } from "./react-templates" import type { AssetInfo, Chain, PackageJson } from "./types" interface FileToCreate { path: string content: string } /** * Detects the package manager being used in the current project * @returns The package manager flag for create-next-app */ const detectPackageManager = (): string => { try { // Check for lockfiles to determine the package manager if (existsSync("bun.lock")) { return "--use-bun" } if (existsSync("pnpm-lock.yaml")) { return "--use-pnpm" } if (existsSync("yarn.lock")) { return "--use-yarn" } // Default to npm if no lockfile is found return "--use-npm" } catch { console.warn( chalk.yellow("Failed to detect package manager, defaulting to npm") ) return "--use-npm" } } export const generateNextApp = async ( parentDir: string, appName: string, useViem: boolean, useWagmi: boolean, selectedChains: Chain[] ): Promise<void> => { const baseDir = path.resolve(parentDir, appName) const hasSolana = selectedChains.some((chain) => chain.name === "Solana") const packageManagerFlag = detectPackageManager() console.log(chalk.blue(`Creating Next.js app: ${appName}`)) execSync( `npx create-next-app@latest ${baseDir} --yes --no-tailwind ${packageManagerFlag}`, { stdio: "inherit" } ) // Remove existing .git directory and re-initialize Git try { await fs.rm(path.join(baseDir, ".git"), { recursive: true, force: true }) console.log(chalk.blue("Initializing new Git repository...")) execSync("git init", { cwd: baseDir, stdio: "inherit" }) execSync("git add .", { cwd: baseDir, stdio: "inherit" }) execSync('git commit -m "initial commit from create dynamic app"', { cwd: baseDir, stdio: "inherit", }) console.log(chalk.green("Created initial commit.")) } catch (error) { console.error(chalk.red("Failed to re-initialize Git repository:", error)) } const packageJsonPath = path.join(baseDir, "package.json") let packageJson: PackageJson = JSON.parse( await fs.readFile(packageJsonPath, "utf8") ) packageJson.dependencies = { ...packageJson.dependencies, "@dynamic-labs/sdk-react-core": DEPENDENCY_VERSIONS.dynamicSdk, ...(useViem && selectedChains.some((chain) => chain.name === "Ethereum") ? { viem: DEPENDENCY_VERSIONS.viem } : !useViem && selectedChains.some((chain) => chain.name === "Ethereum") ? { "@dynamic-labs/ethers-v6": DEPENDENCY_VERSIONS.dynamicEthers, ethers: DEPENDENCY_VERSIONS.ethers, } : {}), ...(useWagmi && { "@dynamic-labs/wagmi-connector": DEPENDENCY_VERSIONS.dynamicSdk, "@tanstack/react-query": DEPENDENCY_VERSIONS.tanstackQuery, wagmi: DEPENDENCY_VERSIONS.wagmi, }), ...Object.fromEntries( selectedChains.map((chain) => [ chain.package, DEPENDENCY_VERSIONS.dynamicSdk, ]) ), "crypto-browserify": "^3.12.0", "stream-browserify": "^3.0.0", process: "^0.11.10", } if (hasSolana) { packageJson = addSolanaDependencies(packageJson) } await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) const libDir = path.join(baseDir, "lib") await fs.mkdir(libDir, { recursive: true }) const componentsDir = path.join(baseDir, "app/components") await fs.mkdir(componentsDir, { recursive: true }) const filesToCreate: FileToCreate[] = [ { path: path.join(baseDir, "app", "layout.tsx"), content: generateLayoutContent(), }, { path: path.join(baseDir, "app", "page.tsx"), content: pageContent }, { path: path.join(baseDir, "app", "page.css"), content: pageCssContent }, { path: path.join(baseDir, "app/components", "Methods.tsx"), content: generateMethodsContent(selectedChains, useViem), }, { path: path.join(baseDir, "app/components", "Methods.css"), content: methodsCss, }, { path: path.join(baseDir, "lib", "providers.tsx"), content: generateProvidersContent(useWagmi, selectedChains), }, { path: path.join(baseDir, "lib", "useDarkMode.ts"), content: useDarkMode, }, { path: path.join(baseDir, "lib", "dynamic.ts"), content: generateDynamicLibContent(useViem, useWagmi, selectedChains), }, ...(useWagmi ? [{ path: path.join(baseDir, "lib", "wagmi.ts"), content: wagmiContent }] : []), ] await Promise.all( filesToCreate.map((file) => fs.writeFile(file.path, file.content)) ) if (hasSolana) { await fs.writeFile( path.join(baseDir, "next.config.js"), createConfigOverrides() ) return } await fs.writeFile( path.join(baseDir, "next.config.js"), createNonSolanaConfig() ) await copyAssets(baseDir) } const copyAssets = async (baseDir: string): Promise<void> => { // Skip asset downloads in CI if (process.env.CI === "true") { console.log("Skipping asset downloads in CI environment") return } const publicDir = path.join(baseDir, "public") // Hardcoded remote URLs for assets const assetUrls: AssetInfo[] = [ { name: "image-light", url: "https://cdn.prod.website-files.com/626692727bba3f384e008e8a/66e1b9f35ab65dff341d8266_image-light.png", }, { name: "image-dark", url: "https://cdn.prod.website-files.com/626692727bba3f384e008e8a/66e1b9f31261f4025001cbff_image-dark.png", }, { name: "logo-light", url: "https://cdn.prod.website-files.com/626692727bba3f384e008e8a/66e1b9f3915ce2792a3677b1_logo-light.png", }, { name: "logo-dark", url: "https://cdn.prod.website-files.com/626692727bba3f384e008e8a/66e1b9e7144147903a39ab97_logo-dark.png", }, ] try { // Ensure the public directory exists await fs.mkdir(publicDir, { recursive: true }) // Download and save each asset await Promise.all( assetUrls.map(async (asset) => { const response = await fetch(asset.url) if (response.ok) { const fileName = `${asset.name}.png` const filePath = path.join(publicDir, fileName) const arrayBuffer = await response.arrayBuffer() await fs.writeFile(filePath, Buffer.from(arrayBuffer)) } }) ) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) console.error( chalk.red(`Error copying assets from remote URLs: ${errorMessage}`) ) } }