UNPKG

create-eth

Version:
947 lines (919 loc) 41.3 kB
import { execa, execaCommand } from 'execa'; import fs, { lstatSync, readdirSync, existsSync, promises } from 'fs'; import path, { basename, resolve } from 'path'; import mergeJsonStr from 'merge-packages'; import { fileURLToPath, pathToFileURL } from 'url'; import ncp from 'ncp'; import { promisify } from 'util'; import * as https from 'https'; import chalk from 'chalk'; import { Listr } from 'listr2'; import arg from 'arg'; import validateProjectName from 'validate-npm-package-name'; import inquirer from 'inquirer'; const findFilesRecursiveSync = (baseDir, criteriaFn = () => true) => { const subPaths = fs.readdirSync(baseDir); const files = subPaths.map(relativePath => { const fullPath = path.resolve(baseDir, relativePath); return fs.lstatSync(fullPath).isDirectory() ? [...findFilesRecursiveSync(fullPath, criteriaFn)] : criteriaFn(fullPath) ? [fullPath] : []; }); return files.flat(); }; function mergePackageJson(targetPackageJsonPath, secondPackageJsonPath, isDev) { const existsTarget = fs.existsSync(targetPackageJsonPath); const existsSecond = fs.existsSync(secondPackageJsonPath); if (!existsTarget && !existsSecond) { return; } const targetPackageJson = existsTarget ? fs.readFileSync(targetPackageJsonPath, "utf8") : "{}"; const secondPackageJson = existsSecond ? fs.readFileSync(secondPackageJsonPath, "utf8") : "{}"; const mergedPkgStr = mergeJsonStr.default(targetPackageJson, secondPackageJson); const formattedPkgStr = JSON.stringify(JSON.parse(mergedPkgStr), null, 2); fs.writeFileSync(targetPackageJsonPath, formattedPkgStr, "utf8"); if (isDev) { const devStr = `TODO: write relevant information for the contributor`; fs.writeFileSync(`${targetPackageJsonPath}.dev`, devStr, "utf8"); } } const { mkdir, link } = promises; const passesFilter = (source, options) => { const isDSStore = /\.DS_Store$/.test(source); if (isDSStore) { return false; // Exclude .DS_Store files } return options?.filter === undefined ? true // no filter : typeof options.filter === "function" ? options.filter(source) // filter is function : options.filter.test(source); // filter is regex }; /** * The goal is that this function has the same API as ncp, so they can be used * interchangeably. * * - clobber not implemented */ const linkRecursive = async (source, destination, options) => { if (!passesFilter(source, options)) { return; } if (lstatSync(source).isDirectory()) { const subPaths = readdirSync(source); await Promise.all(subPaths.map(async (subPath) => { const sourceSubpath = path.join(source, subPath); const isSubPathAFolder = lstatSync(sourceSubpath).isDirectory(); const destSubPath = path.join(destination, subPath); if (!passesFilter(destSubPath, options)) { return; } const existsDestSubPath = existsSync(destSubPath); if (isSubPathAFolder && !existsDestSubPath) { await mkdir(destSubPath); } await linkRecursive(sourceSubpath, destSubPath, options); })); return; } return link(source, destination); }; var curatedExtension = [ { extensionFlagValue: "subgraph", description: "This Scaffold-ETH 2 extension helps you build and test subgraphs locally for your contracts. It also enables interaction with the front-end and facilitates easy deployment to Subgraph Studio.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "subgraph" }, { extensionFlagValue: "eip-712", description: "An implementation of EIP-712, allowing you to send, sign, and verify typed messages in a user-friendly manner.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "eip-712" }, { extensionFlagValue: "ponder", description: "This Scaffold-ETH 2 extension comes pre-configured with ponder.sh, providing an example to help you get started quickly.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "ponder" }, { extensionFlagValue: "onchainkit", description: "This Scaffold-ETH 2 extension comes pre-configured with onchainkit, providing an example to help you get started quickly.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "onchainkit" }, { extensionFlagValue: "erc-20", description: "This extension introduces an ERC-20 token contract and demonstrates how to interact with it, including getting a holder balance and transferring tokens.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "erc-20" }, { extensionFlagValue: "eip-5792", description: "This extension demonstrates on how to use EIP-5792 wallet capabilities. This EIP introduces new JSON-RPC methods for sending multiple calls from the user wallet, and checking their status", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "eip-5792" }, { extensionFlagValue: "randao", description: "This extension shows how to use on-chain randomness using RANDAO for truly on-chain unpredictable random sources.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "randao" }, { extensionFlagValue: "erc-721", description: "This extension introduces an ERC-721 token contract and demonstrates how to use it, including getting the total supply and holder balance, listing all NFTs from the collection and NFTs from the connected address, and how to transfer NFTs.", repository: "https://github.com/scaffold-eth/create-eth-extensions", branch: "erc-721" }, { extensionFlagValue: "challenge-0-simple-nft", description: "SpeedRunEthereum Challenge 0: Simple NFT Example.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-0-simple-nft" }, { extensionFlagValue: "challenge-1-decentralized-staking", description: "SpeedRunEthereum Challenge 1: Decentralized Staking App.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-1-decentralized-staking" }, { extensionFlagValue: "challenge-2-token-vendor", description: "SpeedRunEthereum Challenge 2: Token Vendor.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-2-token-vendor" }, { extensionFlagValue: "challenge-3-dice-game", description: "SpeedRunEthereum Challenge 3: Dice Game.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-3-dice-game" }, { extensionFlagValue: "challenge-4-dex", description: "SpeedRunEthereum Challenge 4: Build a DEX.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-4-dex" }, { extensionFlagValue: "challenge-5-state-channels", description: "SpeedRunEthereum Challenge 5: A State Channel Application.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-5-state-channels" }, { extensionFlagValue: "challenge-6-multisig", description: "SpeedRunEthereum Challenge 6: Multisig Wallet.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-6-multisig" }, { extensionFlagValue: "challenge-7-svg-nft", description: "SpeedRunEthereum Challenge 7: SVG NFT.", repository: "https://github.com/scaffold-eth/se-2-challenges", branch: "challenge-7-svg-nft" } ]; const BASE_DIR = "base"; const SOLIDITY_FRAMEWORKS_DIR = "solidity-frameworks"; const EXAMPLE_CONTRACTS_DIR = "example-contracts"; const SOLIDITY_FRAMEWORKS = { HARDHAT: "hardhat", FOUNDRY: "foundry", }; const TRUSTED_GITHUB_ORGANIZATIONS = ["scaffold-eth", "buidlguidl"]; const extensions = curatedExtension; const CURATED_EXTENSIONS = extensions.reduce((acc, ext) => { if (!ext.repository) { throw new Error(`Extension must have 'repository': ${JSON.stringify(ext)}`); } if (!ext.extensionFlagValue) { throw new Error(`Extension must have 'extensionFlagValue': ${JSON.stringify(ext)}`); } acc[ext.extensionFlagValue] = { repository: ext.repository, branch: ext.branch, }; return acc; }, {}); function deconstructGithubUrl(url) { const urlParts = url.split("/"); const ownerName = urlParts[3]; const repoName = urlParts[4]; const branch = urlParts[5] === "tree" ? urlParts[6] : undefined; return { ownerName, repoName, branch }; } const validateExternalExtension = async (extensionName, dev) => { if (dev) { // Check externalExtensions/${extensionName} exists try { const currentFileUrl = import.meta.url; const externalExtensionsDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../externalExtensions"); await fs.promises.access(`${externalExtensionsDirectory}/${extensionName}`); } catch { throw new Error(`Extension not found in "externalExtensions/${extensionName}"`); } return extensionName; } const { githubUrl, githubBranchUrl, branch, owner } = getDataFromExternalExtensionArgument(extensionName); const isTrusted = TRUSTED_GITHUB_ORGANIZATIONS.includes(owner.toLowerCase()) || !!CURATED_EXTENSIONS[extensionName]; // Check if repository exists await new Promise((resolve, reject) => { https .get(githubBranchUrl, res => { if (res.statusCode !== 200) { reject(new Error(`Extension not found: ${githubUrl}`)); } else { resolve(null); } }) .on("error", err => { reject(err); }); }); return { repository: githubUrl, branch, isTrusted }; }; // Gets the data from the argument passed to the `--extension` option. const getDataFromExternalExtensionArgument = (externalExtension) => { if (CURATED_EXTENSIONS[externalExtension]) { externalExtension = getArgumentFromExternalExtensionOption(CURATED_EXTENSIONS[externalExtension]); } const isGithubUrl = externalExtension.startsWith("https://github.com/"); // Check format: owner/project:branch (branch is optional) const regex = /^[^/]+\/[^/]+(:[^/]+)?$/; if (!regex.test(externalExtension) && !isGithubUrl) { throw new Error(`Invalid extension format. Use "owner/project", "owner/project:branch" or github url.`); } let owner; let project; let branch; if (isGithubUrl) { const { ownerName, repoName, branch: urlBranch } = deconstructGithubUrl(externalExtension); owner = ownerName; project = repoName; branch = urlBranch; } else { // Extract owner, project and branch if format passed is owner/project:branch owner = externalExtension.split("/")[0]; project = externalExtension.split(":")[0].split("/")[1]; branch = externalExtension.split(":")[1]; } const githubUrl = `https://github.com/${owner}/${project}`; let githubBranchUrl; if (branch) { githubBranchUrl = `https://github.com/${owner}/${project}/tree/${branch}`; } return { githubBranchUrl: githubBranchUrl ?? githubUrl, githubUrl, branch, owner, project, }; }; // Parse the externalExtensionOption object into a argument string. // e.g. { repository: "owner/project", branch: "branch" } => "owner/project:branch" const getArgumentFromExternalExtensionOption = (externalExtensionOption) => { const { repository, branch } = externalExtensionOption || {}; const owner = repository?.split("/")[3]; const project = repository?.split("/")[4]; return `${owner}/${project}${branch ? `:${branch}` : ""}`; }; // Gets the solidity framework directories from the external extension repository const getSolidityFrameworkDirsFromExternalExtension = async (externalExtension) => { const solidityFrameworks = Object.values(SOLIDITY_FRAMEWORKS); const filterSolidityFrameworkDirs = (dirs) => { return dirs.filter(dir => solidityFrameworks.includes(dir)).reverse(); }; if (typeof externalExtension === "string") { const currentFileUrl = import.meta.url; const externalExtensionsDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../externalExtensions"); const externalExtensionSolidityFrameworkDirs = await fs.promises.readdir(`${externalExtensionsDirectory}/${externalExtension}/extension/packages`); return filterSolidityFrameworkDirs(externalExtensionSolidityFrameworkDirs); } const { branch, repository } = externalExtension; const { ownerName, repoName } = deconstructGithubUrl(repository); const githubApiUrl = `https://api.github.com/repos/${ownerName}/${repoName}/contents/extension/packages${branch ? `?ref=${branch}` : ""}`; const res = await fetch(githubApiUrl); if (!res.ok) { throw new Error(`Failed to fetch the githubApiUrl ${githubApiUrl}`); } const listOfContents = (await res.json()); const directories = listOfContents.filter(item => item.type === "dir").map(dir => dir.name); return filterSolidityFrameworkDirs(directories); }; const EXTERNAL_EXTENSION_TMP_DIR = "tmp-external-extension"; const copy = promisify(ncp); let copyOrLink = copy; const isTemplateRegex = /([^/\\]*?)\.template\./; const isPackageJsonRegex = /package\.json/; const isYarnLockRegex = /yarn\.lock/; const isConfigRegex = /([^/\\]*?)\\config\.json/; const isArgsRegex = /([^/\\]*?)\.args\./; const isSolidityFrameworkFolderRegex = /solidity-frameworks$/; const isPackagesFolderRegex = /packages$/; const isDeployedContractsRegex = /packages\/nextjs\/contracts\/deployedContracts\.ts/; const getSolidityFrameworkPath = (solidityFramework, templatesDirectory) => path.resolve(templatesDirectory, SOLIDITY_FRAMEWORKS_DIR, solidityFramework); const copyBaseFiles = async (basePath, targetDir, { dev: isDev }) => { await copyOrLink(basePath, targetDir, { clobber: false, filter: fileName => { const isTemplate = isTemplateRegex.test(fileName); const isYarnLock = isYarnLockRegex.test(fileName); const isDeployedContracts = isDeployedContractsRegex.test(fileName); const isPackageJson = isPackageJsonRegex.test(fileName); const skipDevOnly = isDev && (isYarnLock || isDeployedContracts || isPackageJson); return !isTemplate && !skipDevOnly; }, }); if (isDev) { // We don't want symlink below files in dev mode const baseYarnLockPaths = findFilesRecursiveSync(basePath, path => isYarnLockRegex.test(path)); baseYarnLockPaths.forEach(yarnLockPath => { const partialPath = yarnLockPath.split(basePath)[1]; void copy(path.join(basePath, partialPath), path.join(targetDir, partialPath)); }); const basePackageJsonPaths = findFilesRecursiveSync(basePath, path => isPackageJsonRegex.test(path)); basePackageJsonPaths.forEach(packageJsonPath => { const partialPath = packageJsonPath.split(basePath)[1]; mergePackageJson(path.join(targetDir, partialPath), path.join(basePath, partialPath), isDev); }); const baseDeployedContractsPaths = findFilesRecursiveSync(basePath, path => isDeployedContractsRegex.test(path)); baseDeployedContractsPaths.forEach(deployedContractsPath => { const partialPath = deployedContractsPath.split(basePath)[1]; void copy(path.join(basePath, partialPath), path.join(targetDir, partialPath)); }); } }; const isUnselectedSolidityFrameworkFile = ({ path, solidityFramework, }) => { const unselectedSolidityFrameworks = [SOLIDITY_FRAMEWORKS.FOUNDRY, SOLIDITY_FRAMEWORKS.HARDHAT].filter(sf => sf !== solidityFramework); return unselectedSolidityFrameworks.map(sf => new RegExp(`${sf}`)).some(sfregex => sfregex.test(path)); }; const copyExtensionFiles = async ({ dev: isDev, solidityFramework }, extensionPath, targetDir) => { // copy (or link if dev) root files await copyOrLink(extensionPath, path.join(targetDir), { clobber: false, filter: path => { const isConfig = isConfigRegex.test(path); const isArgs = isArgsRegex.test(path); const isSolidityFrameworkFolder = isSolidityFrameworkFolderRegex.test(path) && fs.lstatSync(path).isDirectory(); const isPackagesFolder = isPackagesFolderRegex.test(path) && fs.lstatSync(path).isDirectory(); const isTemplate = isTemplateRegex.test(path); // PR NOTE: this wasn't needed before because ncp had the clobber: false const isPackageJson = isPackageJsonRegex.test(path); const shouldSkip = isConfig || isArgs || isTemplate || isPackageJson || isSolidityFrameworkFolder || isPackagesFolder; return !shouldSkip; }, }); // merge root package.json mergePackageJson(path.join(targetDir, "package.json"), path.join(extensionPath, "package.json"), isDev); const extensionPackagesPath = path.join(extensionPath, "packages"); const hasPackages = fs.existsSync(extensionPackagesPath); if (hasPackages) { // copy extension packages files await copyOrLink(extensionPackagesPath, path.join(targetDir, "packages"), { clobber: false, filter: path => { const isArgs = isArgsRegex.test(path); const isTemplate = isTemplateRegex.test(path); const isPackageJson = isPackageJsonRegex.test(path); const isUnselectedSolidityFramework = isUnselectedSolidityFrameworkFile({ path, solidityFramework }); const shouldSkip = isArgs || isTemplate || isPackageJson || isUnselectedSolidityFramework; return !shouldSkip; }, }); // copy each package's package.json const extensionPackages = fs.readdirSync(extensionPackagesPath); extensionPackages.forEach(packageName => { const isUnselectedSolidityFramework = isUnselectedSolidityFrameworkFile({ path: path.join(targetDir, "packages", packageName, "package.json"), solidityFramework, }); if (isUnselectedSolidityFramework) { return; } mergePackageJson(path.join(targetDir, "packages", packageName, "package.json"), path.join(extensionPath, "packages", packageName, "package.json"), isDev); }); } }; const processTemplatedFiles = async ({ solidityFramework, externalExtension, dev: isDev }, basePath, solidityFrameworkPath, exampleContractsPath, targetDir) => { const baseTemplatedFileDescriptors = findFilesRecursiveSync(basePath, path => isTemplateRegex.test(path)).map(baseTemplatePath => ({ path: baseTemplatePath, fileUrl: pathToFileURL(baseTemplatePath).href, relativePath: baseTemplatePath.split(basePath)[1], source: "base", })); const solidityFrameworkTemplatedFileDescriptors = solidityFrameworkPath ? findFilesRecursiveSync(solidityFrameworkPath, filePath => isTemplateRegex.test(filePath)) .map(solidityFrameworkTemplatePath => ({ path: solidityFrameworkTemplatePath, fileUrl: pathToFileURL(solidityFrameworkTemplatePath).href, relativePath: solidityFrameworkTemplatePath.split(solidityFrameworkPath)[1], source: `extension ${solidityFramework}`, })) .flat() : []; const starterContractsTemplateFileDescriptors = exampleContractsPath ? findFilesRecursiveSync(exampleContractsPath, filePath => isTemplateRegex.test(filePath)) .map(exampleContractTemplatePath => ({ path: exampleContractTemplatePath, fileUrl: pathToFileURL(exampleContractTemplatePath).href, relativePath: exampleContractTemplatePath.split(exampleContractsPath)[1], source: `example-contracts ${solidityFramework}`, })) .flat() : []; const externalExtensionFolder = isDev ? typeof externalExtension === "string" ? path.join(basePath, "../../externalExtensions", externalExtension, "extension") : undefined : path.join(targetDir, EXTERNAL_EXTENSION_TMP_DIR, "extension"); const externalExtensionTemplatedFileDescriptors = externalExtension && externalExtensionFolder ? findFilesRecursiveSync(externalExtensionFolder, filePath => isTemplateRegex.test(filePath)).map(extensionTemplatePath => ({ path: extensionTemplatePath, fileUrl: pathToFileURL(extensionTemplatePath).href, relativePath: extensionTemplatePath.split(externalExtensionFolder)[1], source: `external extension ${isDev ? externalExtension : getArgumentFromExternalExtensionOption(externalExtension)}`, })) : []; await Promise.all([ ...baseTemplatedFileDescriptors, ...solidityFrameworkTemplatedFileDescriptors, ...externalExtensionTemplatedFileDescriptors, ...starterContractsTemplateFileDescriptors, ].map(async (templateFileDescriptor) => { const templateTargetName = templateFileDescriptor.path.match(isTemplateRegex)?.[1]; const argsPath = templateFileDescriptor.relativePath.replace(isTemplateRegex, `${templateTargetName}.args.`); const argsFileUrls = []; if (solidityFrameworkPath) { const argsFilePath = path.join(solidityFrameworkPath, argsPath); const fileExists = fs.existsSync(argsFilePath); if (fileExists) { argsFileUrls.push(pathToFileURL(argsFilePath).href); } } if (exampleContractsPath) { const argsFilePath = path.join(exampleContractsPath, argsPath); const fileExists = fs.existsSync(argsFilePath); if (fileExists) { argsFileUrls.push(pathToFileURL(argsFilePath).href); } } if (externalExtension) { const argsFilePath = isDev ? path.join(basePath, "../../externalExtensions", externalExtension, "extension", argsPath) : path.join(targetDir, EXTERNAL_EXTENSION_TMP_DIR, "extension", argsPath); const fileExists = fs.existsSync(argsFilePath); if (fileExists) { argsFileUrls?.push(pathToFileURL(argsFilePath).href); } } const args = await Promise.all(argsFileUrls.map(async (argsFileUrl) => (await import(argsFileUrl)))); const fileTemplate = (await import(templateFileDescriptor.fileUrl)).default; if (!fileTemplate) { throw new Error(`Template ${templateTargetName} from ${templateFileDescriptor.source} doesn't have a default export`); } if (typeof fileTemplate !== "function") { throw new Error(`Template ${templateTargetName} from ${templateFileDescriptor.source} is not exporting a function by default`); } const allKeys = [...new Set(args.flatMap(Object.keys))]; const freshArgs = Object.fromEntries(allKeys.map(key => [ key, // INFO: key for the freshArgs object [], // INFO: initial value for the freshArgs object ])); const combinedArgs = args.reduce((accumulated, arg) => { Object.entries(arg).map(([key, value]) => { accumulated[key]?.push(value); }); return accumulated; }, freshArgs); const output = fileTemplate(combinedArgs); const targetPath = path.join(targetDir, templateFileDescriptor.relativePath.split(templateTargetName)[0], templateTargetName); fs.writeFileSync(targetPath, output); if (isDev) { const hasCombinedArgs = Object.keys(combinedArgs).length > 0; const hasArgsPaths = argsFileUrls.length > 0; const devOutput = `--- TEMPLATE FILE templates/${templateFileDescriptor.source}${templateFileDescriptor.relativePath} --- ARGS FILES ${hasArgsPaths ? argsFileUrls.map(url => `\t- ${url.split("templates")[1] || url.split("externalExtensions")[1]}`).join("\n") : "(no args files writing to the template)"} --- RESULTING ARGS ${hasCombinedArgs ? Object.entries(combinedArgs) .map(([argName, argValue]) => `\t- ${argName}:\t[${argValue.join(",")}]`) // TODO improvement: figure out how to add the values added by each args file .join("\n") : "(no args sent for the template)"} `; fs.writeFileSync(`${targetPath}.dev`, devOutput); } })); }; const setUpExternalExtensionFiles = async (options, tmpDir) => { // 1. Create tmp directory to clone external extension await fs.promises.mkdir(tmpDir); const { repository, branch } = options.externalExtension; // 2. Clone external extension if (branch) { await execa("git", ["clone", "--branch", branch, repository, tmpDir], { cwd: tmpDir, }); } else { await execa("git", ["clone", repository, tmpDir], { cwd: tmpDir }); } }; async function copyTemplateFiles(options, templateDir, targetDir) { copyOrLink = options.dev ? linkRecursive : copy; const basePath = path.join(templateDir, BASE_DIR); const tmpDir = path.join(targetDir, EXTERNAL_EXTENSION_TMP_DIR); // 1. Copy base template to target directory await copyBaseFiles(basePath, targetDir, options); // 2. Copy solidity framework folder const solidityFrameworkPath = options.solidityFramework && getSolidityFrameworkPath(options.solidityFramework, templateDir); if (solidityFrameworkPath) { await copyExtensionFiles(options, solidityFrameworkPath, targetDir); } const exampleContractsPath = options.solidityFramework && path.resolve(templateDir, EXAMPLE_CONTRACTS_DIR, options.solidityFramework); // 3. Set up external extension if needed if (options.externalExtension) { let externalExtensionPath = path.join(tmpDir, "extension"); if (options.dev) { externalExtensionPath = path.join(templateDir, "../externalExtensions", options.externalExtension, "extension"); } else { await setUpExternalExtensionFiles(options, tmpDir); } if (options.solidityFramework) { const externalExtensionSolidityPath = path.join(externalExtensionPath, "packages", options.solidityFramework, "contracts"); // if external extension does not have solidity framework, we copy the example contracts if (!fs.existsSync(externalExtensionSolidityPath) && exampleContractsPath) { await copyExtensionFiles(options, exampleContractsPath, targetDir); } } await copyExtensionFiles(options, externalExtensionPath, targetDir); } const shouldCopyExampleContracts = !options.externalExtension && options.solidityFramework && exampleContractsPath; if (shouldCopyExampleContracts) { await copyExtensionFiles(options, exampleContractsPath, targetDir); } // 4. Process templated files and generate output await processTemplatedFiles(options, basePath, solidityFrameworkPath, shouldCopyExampleContracts ? exampleContractsPath : null, targetDir); // 5. Delete tmp directory if (options.externalExtension && !options.dev) { await fs.promises.rm(tmpDir, { recursive: true }); } // 6. Initialize git repo to avoid husky error await execa("git", ["init"], { cwd: targetDir }); await execa("git", ["checkout", "-b", "main"], { cwd: targetDir }); } async function createProjectDirectory(projectName) { try { const result = await execa("mkdir", [projectName]); if (result.failed) { throw new Error("There was a problem running the mkdir command"); } } catch (error) { throw new Error("Failed to create directory", { cause: error }); } return true; } async function installPackages(targetDir, task) { const execute = execaCommand("yarn install", { cwd: targetDir }); let outputBuffer = ""; const chunkSize = 1024; execute?.stdout?.on("data", (data) => { outputBuffer += data.toString(); if (outputBuffer.length > chunkSize) { outputBuffer = outputBuffer.slice(-1 * chunkSize); } const visibleOutput = outputBuffer .match(new RegExp(`.{1,${chunkSize}}`, "g")) ?.slice(-1) .map(chunk => chunk.trimEnd() + "\n") .join("") ?? outputBuffer; task.output = visibleOutput; if (visibleOutput.includes("Link step")) { task.output = chalk.yellow(`starting link step, this might take a little time...`); } }); execute?.stderr?.on("data", (data) => { outputBuffer += data.toString(); if (outputBuffer.length > chunkSize) { outputBuffer = outputBuffer.slice(-1 * chunkSize); } const visibleOutput = outputBuffer .match(new RegExp(`.{1,${chunkSize}}`, "g")) ?.slice(-1) .map(chunk => chunk.trimEnd() + "\n") .join("") ?? outputBuffer; task.output = visibleOutput; }); await execute; } const foundryLibraries = ["foundry-rs/forge-std", "OpenZeppelin/openzeppelin-contracts", "gnsps/solidity-bytes-utils"]; async function createFirstGitCommit(targetDir, options) { try { await execa("git", ["add", "-A"], { cwd: targetDir }); await execa("git", ["commit", "-m", "Initial commit with 🏗️ Scaffold-ETH 2", "--no-verify"], { cwd: targetDir }); if (options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) { const foundryWorkSpacePath = path.resolve(targetDir, "packages", SOLIDITY_FRAMEWORKS.FOUNDRY); // forge install foundry libraries await execa("forge", ["install", ...foundryLibraries], { cwd: foundryWorkSpacePath }); await execa("git", ["add", "-A"], { cwd: targetDir }); await execa("git", ["commit", "--amend", "--no-edit"], { cwd: targetDir }); } } catch (e) { // cast error as ExecaError to get stderr throw new Error("Failed to initialize git repository", { cause: e?.stderr ?? e, }); } } // TODO: Instead of using execa, use prettier package from cli to format targetDir async function prettierFormat(targetDir) { try { const result = await execa("yarn", ["format"], { cwd: targetDir }); if (result.failed) { throw new Error("There was a problem running the format command"); } } catch (error) { throw new Error("Failed to create directory", { cause: error }); } return true; } function renderOutroMessage(options) { let message = ` \n ${chalk.bold.green("Congratulations!")} Your project has been scaffolded! 🎉 ${chalk.bold("Next steps:")} ${chalk.dim("cd")} ${options.project} `; if (!options.install) { message += ` \t${chalk.bold("Install dependencies & format files")} \t${chalk.dim("yarn")} install && ${chalk.dim("yarn")} format `; } if (options.solidityFramework === SOLIDITY_FRAMEWORKS.HARDHAT || options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) { message += ` \t${chalk.bold("Start the local development node")} \t${chalk.dim("yarn")} chain `; message += ` \t${chalk.bold("In a new terminal window, deploy your contracts")} \t${chalk.dim("yarn")} deploy `; } message += ` \t${chalk.bold("In a new terminal window, start the frontend")} \t${chalk.dim("yarn")} start `; message += ` ${chalk.bold.green("Thanks for using Scaffold-ETH 2 🙏, Happy Building!")} `; console.log(message); } async function createProject(options) { console.log(`\n`); const currentFileUrl = import.meta.url; const templateDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../templates"); const targetDirectory = path.resolve(process.cwd(), options.project); const tasks = new Listr([ { title: `📁 Create project directory ${targetDirectory}`, task: () => createProjectDirectory(options.project), }, { title: `🚀 Creating a new Scaffold-ETH 2 app in ${chalk.green.bold(options.project)}${options.externalExtension ? ` with the ${chalk.green.bold(options.dev ? options.externalExtension : getArgumentFromExternalExtensionOption(options.externalExtension))} extension` : ""}`, task: () => copyTemplateFiles(options, templateDirectory, targetDirectory), }, { title: "📦 Installing dependencies with yarn, this could take a while", task: (_, task) => installPackages(targetDirectory, task), skip: () => { if (!options.install) { return "Manually skipped, since `--skip-install` flag was passed"; } return false; }, rendererOptions: { outputBar: 8, persistentOutput: false, }, }, { title: "🪄 Formatting files", task: () => prettierFormat(targetDirectory), skip: () => { if (!options.install) { return "Can't use source prettier, since `yarn install` was skipped"; } return false; }, }, { title: `📡 Initializing Git repository${options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY ? " and submodules" : ""}`, task: () => createFirstGitCommit(targetDirectory, options), }, ], { rendererOptions: { collapseSkips: false, suffixSkips: true } }); try { await tasks.run(); renderOutroMessage(options); } catch (error) { console.log("%s Error occurred", chalk.red.bold("ERROR"), error); console.log("%s Exiting...", chalk.red.bold("Uh oh! 😕 Sorry about that!")); } } const validateFoundryUp = async () => { try { await execa("foundryup", ["-h"]); } catch { const message = ` ${chalk.bold.yellow("Attention: Foundryup is not installed in your system.")} ${chalk.bold.yellow("To use foundry, please install foundryup")} ${chalk.bold.yellow("Checkout: https://getfoundry.sh")} `; throw new Error(message); } }; function validateNpmName(name) { const nameValidation = validateProjectName(basename(resolve(name))); if (nameValidation.validForNewPackages) { return { valid: true }; } return { valid: false, problems: [...(nameValidation.errors || []), ...(nameValidation.warnings || [])], }; } // TODO update smartContractFramework code with general extensions async function parseArgumentsIntoOptions(rawArgs) { const args = arg({ "--skip-install": Boolean, "--skip": "--skip-install", "--dev": Boolean, "--solidity-framework": solidityFrameworkHandler, "-s": "--solidity-framework", "--extension": String, "-e": "--extension", "--help": Boolean, "-h": "--help", }, { argv: rawArgs.slice(2), }); const skipInstall = args["--skip-install"] ?? null; const dev = args["--dev"] ?? false; // info: use false avoid asking user const help = args["--help"] ?? false; let project = args._[0] ?? null; // use the original extension arg const extensionName = args["--extension"]; // ToDo. Allow multiple const extension = extensionName ? await validateExternalExtension(extensionName, dev) : null; // if dev mode, extension would be a string if (extension && typeof extension === "object" && !extension.isTrusted) { console.log(chalk.yellow(` You are using a third-party extension. Make sure you trust the source of ${chalk.yellow.bold(extension.repository)}\n`)); } if (project) { const validation = validateNpmName(project); if (!validation.valid) { console.error(`Could not create a project called ${chalk.yellow(`"${project}"`)} because of naming restrictions:`); validation.problems.forEach(p => console.error(`${chalk.red(">>")} Project ${p}`)); project = null; } } let solidityFrameworkChoices = [ SOLIDITY_FRAMEWORKS.HARDHAT, SOLIDITY_FRAMEWORKS.FOUNDRY, { value: null, name: "none" }, ]; if (extension) { const externalExtensionSolidityFrameworkDirs = await getSolidityFrameworkDirsFromExternalExtension(extension); if (externalExtensionSolidityFrameworkDirs.length !== 0) { solidityFrameworkChoices = externalExtensionSolidityFrameworkDirs; } } // if lengh is 1, we don't give user a choice and set it ourselves. const solidityFramework = solidityFrameworkChoices.length === 1 ? solidityFrameworkChoices[0] : (args["--solidity-framework"] ?? null); if (solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) { await validateFoundryUp(); } return { rawOptions: { project, install: !skipInstall, dev, externalExtension: extension, help, solidityFramework: solidityFramework, }, solidityFrameworkChoices, }; } const SOLIDITY_FRAMEWORK_OPTIONS = [...Object.values(SOLIDITY_FRAMEWORKS), "none"]; function solidityFrameworkHandler(value) { const lowercasedValue = value.toLowerCase(); if (SOLIDITY_FRAMEWORK_OPTIONS.includes(lowercasedValue)) { return lowercasedValue; } // choose from cli prompts return null; } // default values for unspecified args const defaultOptions = { project: "my-dapp-example", solidityFramework: null, install: true, dev: false, externalExtension: null, help: false, }; async function promptForMissingOptions(options, solidityFrameworkChoices) { const cliAnswers = Object.fromEntries(Object.entries(options).filter(([, value]) => value !== null)); const questions = [ { type: "input", name: "project", message: "Your project name:", default: defaultOptions.project, validate: (name) => { const validation = validateNpmName(name); if (validation.valid) { return true; } return "Project " + validation.problems[0]; }, }, { type: "list", name: "solidityFramework", message: "What solidity framework do you want to use?", choices: solidityFrameworkChoices, default: SOLIDITY_FRAMEWORKS.HARDHAT, }, ]; const answers = await inquirer.prompt(questions, cliAnswers); const solidityFramework = options.solidityFramework ?? answers.solidityFramework; const mergedOptions = { project: options.project ?? answers.project, install: options.install, dev: options.dev ?? defaultOptions.dev, solidityFramework: solidityFramework === "none" ? null : solidityFramework, externalExtension: options.externalExtension, }; return mergedOptions; } const TITLE_TEXT = ` ${chalk.bold.blue("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+")} ${chalk.bold.blue("| Create Scaffold-ETH 2 app |")} ${chalk.bold.blue("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+")} `; function renderIntroMessage() { console.log(TITLE_TEXT); } const showHelpMessage = () => { console.log(` ${chalk.bold.blue("Usage:")} ${chalk.bold.green("npx create-eth<@version>")} ${chalk.gray("[--skip | --skip-install] [-s <solidity-framework> | --solidity-framework <solidity-framework>] [-e <extension> | --extension <extension>] [-h | --help]")} `); console.log(` ${chalk.bold.blue("Options:")} ${chalk.gray("--skip, --skip-install")} Skip packages installation ${chalk.gray("-s, --solidity-framework")} Choose solidity framework ${chalk.gray("-e, --extension")} Add curated or third-party extension ${chalk.gray("-h, --help")} Help `); }; async function cli(args) { try { renderIntroMessage(); const { rawOptions, solidityFrameworkChoices } = await parseArgumentsIntoOptions(args); if (rawOptions.help) { showHelpMessage(); return; } const options = await promptForMissingOptions(rawOptions, solidityFrameworkChoices); if (options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) { await validateFoundryUp(); } await createProject(options); } catch (error) { console.error(chalk.red.bold(error.message || "An unknown error occurred.")); return; } } export { cli }; //# sourceMappingURL=cli.js.map