UNPKG

create-dynamic-app

Version:

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

265 lines (238 loc) 8.21 kB
import chalk from "chalk" import fetch from "node-fetch" import { execSync, spawnSync } from "node:child_process" import fs from "node:fs/promises" import { existsSync } from "node:fs" import path from "node:path" import { addSolanaDependencies, appCSSContent, appTsxContent, DEPENDENCY_VERSIONS, ENV_FILE_TEMPLATE, generateMainTsxContent, generateMethodsTsxContent, generateReactReadme, generateViteConfigForSolana, GIT_CONFIG, indexCSSContent, indexHTMLContent, methodsCssContent, updatePackageJsonScripts, useDarkModeContent, } from "./react-templates" import type { AssetInfo, Chain, PackageJson } from "./types" /** * Detects the package manager being used in the current project * @returns The package manager command to use with create-vite */ const detectPackageManagerForVite = (): string => { try { // Check for lockfiles to determine the package manager if (existsSync("bun.lock")) { return "bun create" } else if (existsSync("pnpm-lock.yaml")) { return "pnpm create" } else if (existsSync("yarn.lock")) { return "yarn create" } else { // Default to npm if no lockfile is found return "npm create" } } catch { console.warn( chalk.yellow("Failed to detect package manager, defaulting to npm") ) return "npm create" } } export const generateReactApp = 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 hasEthereum = selectedChains.some((chain) => chain.name === "Ethereum") const packageManagerCmd = detectPackageManagerForVite() const isBun = packageManagerCmd.startsWith("bun") const viteCommand = isBun ? `${packageManagerCmd} vite@latest ${appName} --template react-ts` : `${packageManagerCmd} vite@latest ${appName} -- --template react-ts` console.log(chalk.blue(`Creating React app with Vite: ${appName}`)) execSync(viteCommand, { cwd: parentDir, }) const packageJsonPath = path.join(baseDir, "package.json") let packageJson: PackageJson = JSON.parse( await fs.readFile(packageJsonPath, "utf8") ) packageJson.type = "module" packageJson = updatePackageJsonScripts(packageJson) packageJson.dependencies = { "@dynamic-labs/sdk-react-core": DEPENDENCY_VERSIONS.dynamicSdk, react: DEPENDENCY_VERSIONS.react, "react-dom": DEPENDENCY_VERSIONS.reactDom, ...(useViem && hasEthereum ? { viem: DEPENDENCY_VERSIONS.viem } : !useViem && hasEthereum ? { "@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, ]) ), } if (hasSolana) { packageJson = addSolanaDependencies(packageJson) packageJson.devDependencies = { ...(packageJson.devDependencies ?? {}), "vite-plugin-node-polyfills": DEPENDENCY_VERSIONS.nodePolyfills, } } packageJson.devDependencies = { ...packageJson.devDependencies, "@vitejs/plugin-react": DEPENDENCY_VERSIONS.vitejsPluginReact, "@types/react": DEPENDENCY_VERSIONS.react, "@types/react-dom": DEPENDENCY_VERSIONS.reactDom, eslint: DEPENDENCY_VERSIONS.eslint, "eslint-plugin-react-hooks": DEPENDENCY_VERSIONS.eslintPluginReactHooks, "eslint-plugin-react-refresh": DEPENDENCY_VERSIONS.eslintPluginReactRefresh, typescript: DEPENDENCY_VERSIONS.typescript, vite: DEPENDENCY_VERSIONS.vite, } await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) const viteConfigContent = generateViteConfigForSolana(selectedChains) await fs.writeFile(path.join(baseDir, "vite.config.ts"), viteConfigContent) const libDir = path.join(baseDir, "src", "lib") await fs.mkdir(libDir, { recursive: true }) await fs.writeFile(path.join(libDir, "useDarkMode.ts"), useDarkModeContent) const componentsDir = path.join(baseDir, "src", "components") await fs.mkdir(componentsDir, { recursive: true }) await fs.writeFile(path.join(baseDir, "src", "App.css"), appCSSContent) await fs.writeFile(path.join(baseDir, "src", "App.tsx"), appTsxContent) const mainTsxContent = generateMainTsxContent( useViem, useWagmi, selectedChains ) await fs.writeFile(path.join(baseDir, "src", "main.tsx"), mainTsxContent) await fs.writeFile(path.join(baseDir, "index.html"), indexHTMLContent) await fs.writeFile(path.join(baseDir, "src", "index.css"), indexCSSContent) const methodsTsxContent = generateMethodsTsxContent(selectedChains, useViem) await fs.writeFile(path.join(componentsDir, "Methods.tsx"), methodsTsxContent) await fs.writeFile(path.join(componentsDir, "Methods.css"), methodsCssContent) await fs.writeFile(path.join(baseDir, ".env"), ENV_FILE_TEMPLATE) await fs.writeFile(path.join(baseDir, ".env.example"), ENV_FILE_TEMPLATE) const readmeContent = generateReactReadme(appName) await fs.writeFile(path.join(baseDir, "README.md"), readmeContent) console.log(chalk.blue("Initializing git repository...")) if (process.env.CI !== "true") { try { // Initialize repository with proper config spawnSync("git", ["config", "user.email", "test@example.com"], { cwd: baseDir, stdio: "pipe", }) spawnSync("git", ["config", "user.name", "Test User"], { cwd: baseDir, stdio: "pipe", }) spawnSync("git", ["init"], { cwd: baseDir, stdio: "pipe" }) // Add and commit files spawnSync("git", ["add", "."], { cwd: baseDir, stdio: "pipe" }) spawnSync("git", ["commit", "-m", GIT_CONFIG.commitMessage], { cwd: baseDir, stdio: "pipe", }) console.log(chalk.green("✅ Git repository initialized successfully!")) } catch (error) { console.warn( chalk.yellow( "Couldn't initialize git repository. This is not critical for the app to work." ) ) if (process.env.DEBUG) { console.error(error) } } } else { console.log(chalk.yellow("Skipping git initialization in CI environment")) } 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 const downloadPromises = assetUrls.map(async (asset) => { try { console.log(`Downloading ${asset.name}...`) const response = await fetch(asset.url) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const fileName = `${asset.name}.png` const filePath = path.join(publicDir, fileName) const arrayBuffer = await response.arrayBuffer() await fs.writeFile(filePath, Buffer.from(arrayBuffer)) console.log(`Successfully downloaded ${asset.name}`) } catch (error) { console.warn( chalk.yellow( `Failed to download ${asset.name}: ${ error instanceof Error ? error.message : String(error) }` ) ) } }) await Promise.all(downloadPromises) } catch (error) { console.warn( chalk.yellow( `Asset download failed: ${ error instanceof Error ? error.message : String(error) }` ) ) } }