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