UNPKG

create-ckb-js-vm-app

Version:

CLI tool to quickly bootstrap CKB on-chain script with ckb-js-vm.

347 lines (306 loc) • 9.33 kB
import { Command } from "commander"; import fs from "fs-extra"; import path from "path"; import updateCheck from "update-check"; import prompts from "prompts"; import { bold, cyan, green, red, yellow } from "picocolors"; import validateProjectName from "validate-npm-package-name"; import { spawn } from "child_process"; import { bindingVersion, cccCoreVersion, coreVersion, testtoolVersion, } from "./config"; const packageJson = require(path.join(__dirname, "../package.json")); let projectName = ""; const handleSigTerm = (): void => process.exit(0); process.on("SIGINT", handleSigTerm); process.on("SIGTERM", handleSigTerm); const onPromptState = (state: any): void => { if (state.aborted) { // If we don't re-enable the terminal cursor before exiting // the program, the cursor will remain hidden process.stdout.write("\x1B[?25h"); process.stdout.write("\n"); process.exit(1); } }; const program = new Command(packageJson.name) .version( packageJson.version, "-v, --version", `Output the current version of ${packageJson.name}.`, ) .argument("[directory]") .usage("[directory] [options]") .helpOption("-h, --help", "Display this help message.") .option( "--skip-install", "Explicitly tell the CLI to skip installing packages.", ) .action((name: string) => { // Commander does not implicitly support negated options. When they are used // by the user they will be interpreted as the positional argument (name) in // the action handler. See https://github.com/tj/commander.js/pull/1355 if (name && !name.startsWith("--no-")) { projectName = name; } }) .allowUnknownOption() .parse(process.argv); const opts = program.opts(); const packageManager = getPkgManager(); interface ValidationResult { valid: boolean; problems?: string[]; } function validateNpmName(name: string): ValidationResult { const nameValidation = validateProjectName(name); if (nameValidation.validForNewPackages) { return { valid: true }; } return { valid: false, problems: [ ...(nameValidation.errors || []), ...(nameValidation.warnings || []), ], }; } function getPkgManager(): string { return "pnpm"; } function isFolderEmpty(folderPath: string): boolean { const files = fs.readdirSync(folderPath); return files.length === 0; } export async function install(packageManager: string): Promise<void> { const args = ["install"]; return new Promise((resolve, reject) => { const child = spawn(packageManager, args, { stdio: "inherit", env: { ...process.env, ADBLOCK: "1", // we set NODE_ENV to development as pnpm skips dev // dependencies when production NODE_ENV: "development", DISABLE_OPENCOLLECTIVE: "1", }, }); child.on("close", (code) => { if (code !== 0) { reject({ command: `${packageManager} ${args.join(" ")}` }); return; } resolve(); }); }); } function updatePackageJson1(projectPath: string) { const packageJsonPath = path.join( projectPath, "packages/on-chain-script/package.json", ); let json: any = ""; if (fs.pathExistsSync(packageJsonPath)) { try { json = fs.readJsonSync(packageJsonPath); json.name = projectName; json.devDependencies["ckb-testtool"] = testtoolVersion; json.dependencies["@ckb-js-std/bindings"] = bindingVersion; json.dependencies["@ckb-js-std/core"] = coreVersion; fs.writeJsonSync(packageJsonPath, json, { spaces: 2 }); console.log(green(`Updated ${projectName}/package.json.`)); } catch (error: any) { console.error( red( `Failed to update packages/on-chain-script/package.json: ${error.message}`, ), ); process.exit(1); } } else { console.error( red( `Could not find packages/on-chain-script/package.json in the template. Make sure your template includes a package.json.`, ), ); process.exit(1); } } function updatePackageJson2(projectPath: string) { const packageJsonPath = path.join( projectPath, "packages/on-chain-script-tests/package.json", ); let json: any = ""; if (fs.pathExistsSync(packageJsonPath)) { try { json = fs.readJsonSync(packageJsonPath); json.name = projectName; json.devDependencies["ckb-testtool"] = testtoolVersion; json.devDependencies["@ckb-ccc/core"] = cccCoreVersion; fs.writeJsonSync(packageJsonPath, json, { spaces: 2 }); console.log(green(`Updated ${projectName}/package.json.`)); } catch (error: any) { console.error( red( `Failed to update packages/on-chain-script/package.json: ${error.message}`, ), ); process.exit(1); } } else { console.error( red( `Could not find packages/on-chain-script/package.json in the template. Make sure your template includes a package.json.`, ), ); process.exit(1); } } async function run(): Promise<void> { console.log(); if (projectName && typeof projectName === "string") { projectName = projectName.trim(); } if (!projectName) { const res = await prompts({ onState: onPromptState, type: "text", name: "path", message: "What is your project named?", initial: "my-ckb-script", validate: (name) => { const validation = validateNpmName(path.basename(path.resolve(name))); if (validation.valid) { return true; } return ( "Invalid project name: " + (validation.problems?.[0] || "Unknown validation error") ); }, }); if (typeof res.path === "string") { projectName = res.path.trim(); } } if (!projectName) { console.log( "\nPlease specify the project directory:\n" + ` ${cyan(program.name())} ${green("<project-directory>")}\n` + "For example:\n" + ` ${cyan(program.name())} ${green("my-ckb-script")}\n\n` + `Run ${cyan(`${program.name()} --help`)} to see all options.`, ); process.exit(1); } const appPath = path.resolve(projectName); const appName = path.basename(appPath); const validation = validateNpmName(appName); if (!validation.valid) { console.error( `Could not create a project called ${red( `"${appName}"`, )} because of npm naming restrictions:`, ); validation.problems?.forEach((p) => console.error(` ${red(bold("*"))} ${p}`), ); process.exit(1); } if (fs.pathExistsSync(appPath) && !isFolderEmpty(appPath)) { console.error( `Could not create a project called ${red( `"${appName}"`, )} because a project with the same name already exists.`, ); process.exit(1); } console.log(bold(`Using ${packageManager}.`)); const templatePath = path.join(__dirname, `../templates`); if (!fs.pathExistsSync(templatePath)) { console.error(`Could not find a template`); console.error(`\n šŸ˜®ā€šŸ’Ø Project ${projectName} created failed!\n`); process.exit(1); } const originalDirectory = process.cwd(); const projectPath = path.join(originalDirectory, projectName); fs.ensureDirSync(projectPath); fs.copySync(templatePath, projectPath); updatePackageJson1(projectName); updatePackageJson2(projectName); console.log(`\nšŸŽ‰ Project ${projectName} created!\n`); if (opts.skipInstall) { console.log( "Skip install the dependencies, we suggest that you begin by typing:", ); console.log(); console.log(cyan(" cd"), projectName); console.log(` ${cyan(`${packageManager} install`)}`); console.log(); } else { console.log("\nInstalling dependencies:"); console.log(); console.log("Installing packages. This might take a couple of minutes."); console.log(); process.chdir(appPath); await install(packageManager); console.log("Packages installed."); console.log(); } console.log(`${green("Success!")} Created ${projectName} at ${projectPath}`); console.log(); } const update = updateCheck(packageJson).catch(() => null); async function notifyUpdate(): Promise<void> { try { const updateInfo = await update; if (updateInfo?.latest) { const global: Record<string, string> = { pnpm: "pnpm add -g", }; const updateMessage = `${global[packageManager]} ${packageJson.name}`; console.log( yellow(bold(`A new version of \`${packageJson.name}\` is available!`)) + "\n" + "You can update by running: " + cyan(updateMessage) + "\n", ); } process.exit(0); } catch { // ignore error } } interface ExitReason { command?: string; message?: string; } async function exit(reason: ExitReason): Promise<void> { console.log(); console.log("Aborting installation."); if (reason.command) { console.log(` ${cyan(reason.command)} has failed.`); } else { console.log( red("Unexpected error. Please report it as a bug:") + "\n", reason, ); } console.log(); await notifyUpdate(); process.exit(1); } (async () => { try { await run(); await notifyUpdate(); } catch (error) { await exit(error as ExitReason); } })();