UNPKG

create-mettle-app

Version:

A set of fast building mettle.js project command line tool.

259 lines (228 loc) 6.74 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const argv = require('minimist')(process.argv.slice(2), { string: ['_'] }); const prompts = require('prompts'); const { yellow, red, lightYellow, cyan, blue } = require('kolorist'); const cwd = process.cwd(); const FRAMEWORKS = [ { name: 'create-mettle-app', color: yellow, variants: [ { name: 'mettle', display: 'JavaScript', color: cyan, }, { name: 'mettle-apps', display: 'JavaScript', color: cyan, }, { name: 'mettle-ts', display: 'TypeScript', color: blue, }, { name: 'mettle-apps-ts', display: 'TypeScript', color: blue, }, ], }, ]; const TEMPLATES = FRAMEWORKS.map( (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name] ).reduce((a, b) => a.concat(b), []); const renameFiles = { _gitignore: '.gitignore', }; async function init() { let targetDir = argv._[0]; let template = argv.template || argv.t; const defaultProjectName = !targetDir ? 'mettle-project' : targetDir; let result = {}; try { result = await prompts( [ { type: targetDir ? null : 'text', name: 'projectName', message: 'Project name:', initial: defaultProjectName, onState: (state) => (targetDir = state.value.trim() || defaultProjectName), }, { type: () => (!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm'), name: 'overwrite', message: () => (targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`) + ` is not empty. Remove existing files and continue?`, }, { // @ts-ignore type: (_, { overwrite } = {}) => { if (overwrite === false) { throw new Error(red('✖') + ' Operation cancelled'); } return null; }, name: 'overwriteChecker', }, { type: () => (isValidPackageName(targetDir) ? null : 'text'), name: 'packageName', message: 'Package name:', initial: () => toValidPackageName(targetDir), validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name', }, { type: template && TEMPLATES.includes(template) ? null : 'select', name: 'framework', message: typeof template === 'string' && !TEMPLATES.includes(template) ? `"${template}" isn't a valid template. Please choose from below: ` : 'Select a framework:', initial: 0, choices: FRAMEWORKS.map((framework) => { const frameworkColor = framework.color; return { title: frameworkColor(framework.name), value: framework, }; }), }, { type: (framework) => (framework && framework.variants ? 'select' : null), name: 'variant', message: 'Select a variant:', // @ts-ignore choices: (framework) => framework.variants.map((variant) => { const variantColor = variant.color; return { title: variantColor(variant.name), value: variant.name, }; }), }, ], { onCancel: () => { throw new Error(red('✖') + ' Operation cancelled'); }, } ); } catch (cancelled) { console.log(cancelled.message); return; } // user choice associated with prompts const { framework, overwrite, packageName, variant } = result; const root = path.join(cwd, targetDir); if (overwrite) { emptyDir(root); } else if (!fs.existsSync(root)) { fs.mkdirSync(root); } // determine template template = variant || framework || template; console.log(`\nScaffolding project in ${root}...`); const templateDir = path.join(__dirname, `template-${template}`); const write = (file, content) => { const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file); if (content) { fs.writeFileSync(targetPath, content); } else { copy(path.join(templateDir, file), targetPath); } }; const files = fs.readdirSync(templateDir); for (const file of files.filter((f) => f !== 'package.json')) { write(file); } const pkg = require(path.join(templateDir, `package.json`)); pkg.name = packageName || targetDir; write('package.json', JSON.stringify(pkg, null, 2)); const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent); const pkgManager = pkgInfo ? pkgInfo.name : 'npm'; console.log(`\nDone. Now run:\n`); if (root !== cwd) { console.log(` cd ${path.relative(cwd, root)}`); } switch (pkgManager) { case 'yarn': console.log(' yarn'); console.log(' yarn dev'); break; default: console.log(` ${pkgManager} install`); console.log(` ${pkgManager} run dev`); break; } } function copy(src, dest) { const stat = fs.statSync(src); if (stat.isDirectory()) { copyDir(src, dest); } else { fs.copyFileSync(src, dest); } } function isValidPackageName(projectName) { return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName); } function toValidPackageName(projectName) { return projectName .trim() .toLowerCase() .replace(/\s+/g, '-') .replace(/^[._]/, '') .replace(/[^a-z0-9-~]+/g, '-'); } function copyDir(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }); for (const file of fs.readdirSync(srcDir)) { const srcFile = path.resolve(srcDir, file); const destFile = path.resolve(destDir, file); copy(srcFile, destFile); } } function isEmpty(path) { return fs.readdirSync(path).length === 0; } function emptyDir(dir) { if (!fs.existsSync(dir)) { return; } for (const file of fs.readdirSync(dir)) { const abs = path.resolve(dir, file); // baseline is Node 12 so can't use rmSync :( if (fs.lstatSync(abs).isDirectory()) { emptyDir(abs); fs.rmdirSync(abs); } else { fs.unlinkSync(abs); } } } /** * @param {string | undefined} userAgent process.env.npm_config_user_agent * @returns object | undefined */ function pkgFromUserAgent(userAgent) { if (!userAgent) return undefined; const pkgSpec = userAgent.split(' ')[0]; const pkgSpecArr = pkgSpec.split('/'); return { name: pkgSpecArr[0], version: pkgSpecArr[1], }; } init().catch((e) => { console.error(e); });