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