zkapp-cli
Version:
CLI to create zkApps (zero-knowledge apps) for Mina Protocol
183 lines (160 loc) • 5.35 kB
JavaScript
import chalk from 'chalk';
import enquirer from 'enquirer';
import fs from 'fs-extra';
import path from 'node:path';
import url from 'node:url';
import util from 'node:util';
import ora from 'ora';
import shell from 'shelljs';
import Constants from './constants.js';
import { isDirEmpty, setProjectName, setupProject, step } from './helpers.js';
// Module external API
export default example;
// Module internal API (exported for testing purposes)
export { addStartScript, findUniqueDir, updateExampleSources };
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const shellExec = util.promisify(shell.exec);
/**
* Create a new zkApp project with recommended dir structure, Prettier config,
* testing lib, etc. Warns if already exists and does NOT overwrite.
* @param {string} name Desired dir name or path. Will recursively create
* dirs without overwriting existing content, if needed.
* @return {Promise<void>}
*/
async function example(example) {
if (!shell.which('git')) {
console.error(chalk.red('Please ensure Git is installed, then try again.'));
shell.exit(1);
}
if (!example) {
const res = await enquirer.prompt({
type: 'select',
name: 'example',
choices: Constants.exampleTypes,
message: (state) => {
const style =
state.submitted && !state.cancelled
? state.styles.success
: chalk.reset;
return style('Choose an example');
},
prefix: (state) => {
// Shows a cyan question mark when not submitted.
// Shows a green check mark if submitted.
// Shows a red "x" if ctrl+C is pressed (default is a magenta).
if (!state.submitted) return state.symbols.question;
return !state.cancelled
? state.symbols.check
: chalk.red(state.symbols.cross);
},
});
example = res.example;
}
const dir = findUniqueDir(example);
const isWindows = process.platform === 'win32';
if (!(await setupProject(path.join(shell.pwd().toString(), dir)))) {
shell.exit(1);
}
if (!(await updateExampleSources(example, dir))) {
shell.exit(1);
}
// Set dir for shell commands. Doesn't change user's dir in their CLI.
shell.cd(dir);
await step('Initialize Git repo', async () => {
await shellExec('git init -q');
});
await step('Set project name', async () => {
setProjectName(process.cwd());
});
await step('Update scripts', async () => {
addStartScript(path.join(process.cwd(), 'package.json'));
});
await step('NPM install', async () => {
await shellExec(
`npm install --silent > ${isWindows ? 'NUL' : '"/dev/null" 2>&1'}`
);
});
await step('Git init commit', async () => {
await shellExec(
'git add . && git commit -m "Init commit" -q -n && git branch -m main'
);
});
const str =
`\nSuccess!\n` +
`\nNext steps:` +
`\n cd ${dir}` +
`\n git remote add origin <your-repo-url>` +
`\n git push -u origin main` +
`\n` +
`\nTo run the example:` +
`\n cd ${dir}` +
`\n npm run test` +
`\n npm run build` +
`\n npm run start`;
console.log(chalk.green(str));
process.exit(0);
}
/**
* Helper to add start script to package.json.
* @param {string} file Path to file
*/
function addStartScript(file) {
let packageJsonContent = fs.readJsonSync(file, 'utf8');
packageJsonContent['scripts']['start'] = 'node build/src/run.js';
fs.writeJsonSync(file, packageJsonContent, { spaces: 2 });
}
/**
* Updates the example sources.
* @param {string} example Name of the example.
* @param {string} name Destination dir name.
* @param {string} lang ts (default) or js
* @returns {Promise<boolean>} True if successful; false if not.
*/
async function updateExampleSources(example, name, lang = 'ts') {
const step = 'Update example sources';
const spin = ora(`${step}...`).start();
try {
const examplePath = path.resolve(
__dirname,
'..',
'..',
'examples',
example,
lang,
'src'
);
// Example not found. Delete the project template & temp dir to clean up.
if (isDirEmpty(examplePath)) {
spin.fail(step);
console.error(chalk.red('Example not found'));
return false;
}
// Delete the project template's `src` & use the example's `src` instead.
const srcPath = path.resolve(name, 'src');
shell.rm('-r', srcPath);
// `node:fs.cpSync` instead of the `shell.cp` because `ShellJS` does not implement `cp -a`
// https://github.com/shelljs/shelljs/issues/79#issuecomment-30821277
fs.cpSync(`${examplePath}/`, `${srcPath}/`, { recursive: true });
spin.succeed(chalk.green(step));
return true;
} catch (err) {
spin.fail(step);
console.error(err);
return false;
}
}
/**
* Given a specified directory name, will return that dir name if it is available,
* or the next next available dir name with a numeric suffix: <dirName><#>.
* @param {string} str Desired dir name.
* @param {number} i Counter for the recursive function.
* @return {string} An unused directory name.
*/
function findUniqueDir(str, i = 0) {
const dir = str + (i || '');
if (fs.existsSync(dir)) {
return findUniqueDir(str, ++i);
}
return dir;
}