UNPKG

yrui-init

Version:

Init yrui app helper tools.

407 lines (373 loc) 12 kB
'use strict'; const os = require('os'); const fs = require('fs'); const path = require('path'); const rimraf = require('rimraf'); const zlib = require('zlib'); const tar = require('tar'); const unzip = require("unzip"); const urllib = require('urllib'); const updater = require('npm-updater'); const mkdirp = require('mkdirp'); const inquirer = require('inquirer'); const yargs = require('yargs'); const memFs = require('mem-fs'); const editor = require('mem-fs-editor'); const glob = require('glob'); const is = require('is-type-of'); const homedir = require('node-homedir'); const groupBy = require('group-object'); require('colors'); module.exports = class Command { constructor() { this.name = 'yrui-init'; this.pkgUrl = 'https://github.com/ahyiru/yrui-demo/archive/master.zip'; this.pkgInfo = require('../package.json'); this.inquirer = inquirer; this.fileMapping = { gitignore: '.gitignore', _gitignore: '.gitignore', '_.gitignore': '.gitignore', '_package.json': 'package.json', '_.eslintignore': '.eslintignore', }; }; * run(cwd, args) { const argv = this.argv = this.getParser().parse(args || []); this.cwd = cwd; // console.log('%j', argv); // detect registry url /*this.registryUrl = this.getRegistryByType(argv.registry); this.log(`use registry: ${this.registryUrl}`);*/ // check update /*yield updater({ package: this.pkgInfo, level: 'major', });*/ // ask for target dir this.targetDir = yield this.getTargetDirectory(); // use local template let templateDir = yield this.getTemplateDir(); /*if (!templateDir) { // list boilerplate const boilerplateMapping = yield this.fetchBoilerplateMapping(); // ask for boilerplate let boilerplate; if (argv.type && boilerplateMapping.hasOwnProperty(argv.type)) { boilerplate = boilerplateMapping[argv.type]; } else { boilerplate = yield this.askForBoilerplateType(boilerplateMapping); } this.log(`use boilerplate: ${boilerplate.name}(${boilerplate.package})`); // download boilerplate templateDir = yield this.downloadBoilerplate(boilerplate.package); }*/ templateDir = yield this.downloadBoilerplate(this.pkgUrl); // copy template yield this.processFiles(this.targetDir, templateDir); // done this.printUsage(this.targetDir); }; getParser() { return yargs .usage('init yrui project from boilerplate.\nUsage: $0 [dir] --type=simple') .options(this.getParserOptions()) .alias('h', 'help') .version() .help(); }; getParserOptions() { return { type: { type: 'string', description: 'boilerplate type', }, dir: { type: 'string', description: 'target directory', }, force: { type: 'boolean', description: 'force to override directory', alias: 'f', }, template: { type: 'string', description: 'local path to boilerplate', }, registry: { type: 'string', description: 'npm registry, support china/npm/custom, default to auto detect', alias: 'r', }, silent: { type: 'boolean', description: 'don\'t ask, just use default value', }, }; }; * getTargetDirectory() { const dir = this.argv._[0] || this.argv.dir || ''; let targetDir = path.resolve(this.cwd, dir); const force = this.argv.force; const validate = dir => { // create dir if not exist if (!fs.existsSync(dir)) { mkdirp.sync(dir); return true; } // not a directory if (!fs.statSync(dir).isDirectory()) { return `${dir} already exists as a file`.red; } // check if directory empty const files = fs.readdirSync(dir).filter(name => name[0] !== '.'); if (files.length > 0) { if (force) { this.log(`${dir} already exists and will be override due to --force`.red); return true; } return `${dir} already exists and not empty: ${JSON.stringify(files)}`.red; } return true; }; // if argv dir is invalid, then ask user const isValid = validate(targetDir); if (isValid !== true) { this.log(isValid); const answer = yield this.inquirer.prompt({ name: 'dir', message: 'Please enter target dir: ', default: dir || '.', filter: dir => path.resolve(this.cwd, dir), validate, }); targetDir = answer.dir; } this.log(`target dir is ${targetDir}`); return targetDir; }; * getTemplateDir() { let templateDir; // when use `egg-init --template=PATH` if (this.argv.template) { templateDir = path.resolve(this.cwd, this.argv.template); if (!fs.existsSync(templateDir)) { this.log(`${templateDir} is not exists`.red); } else if (!fs.existsSync(path.join(templateDir, 'boilerplate'))) { this.log(`${templateDir} should contain boilerplate folder`.red); } else { this.log(`local template dir is ${templateDir.green}`); return templateDir; } } }; * downloadBoilerplate(pkgUrl) { // const result = yield this.getPackageInfo(pkgName, false); // const tgzUrl = result.dist.tarball; const tgzUrl = pkgUrl; const saveDir = path.join(os.tmpdir(), 'yrui-init-tmp'); this.log(`downloading ${tgzUrl}`); yield this.downloadAndUnzip(tgzUrl, saveDir); this.log(`extract to ${saveDir}`); return saveDir; }; downloadAndUnzip(url, saveDir) { return function(callback) { rimraf.sync(saveDir); function handleError(err) { rimraf.sync(saveDir); callback(err); } urllib.request(url, { followRedirect: true, streaming: true, }, (err, _, res) => { if (err) { return callback(err); } const gunzip = zlib.createGunzip(); gunzip.on('error', handleError); /*const extracter = tar.Extract({ path: saveDir, strip: 1 }); extracter.on('error', handleError); extracter.on('end', callback); res.pipe(gunzip).pipe(extracter);*/ const extracter = unzip.Extract({ path: saveDir }); extracter.on('error', handleError); extracter.on('close', callback); res.pipe(extracter); }); }; }; * processFiles(targetDir, templateDir) { const src = path.join(templateDir, 'yrui-demo-master'); const locals = yield this.askForVariable(targetDir, templateDir); const fsEditor = editor.create(memFs.create()); const files = glob.sync('**/*', { cwd: src, dot: true, nodir: true }); files.forEach(file => { const from = path.join(src, file); const to = path.join(targetDir, this.replaceTemplate(this.fileMapping[file] || file, locals)); fsEditor.copy(from, to, { process: content => { this.log('write to %s', to); return this.replaceTemplate(content, locals); }, }); }); // write boilerplate base info to dist pkg info // const tplPkgInfo = require(path.join(templateDir, 'package.json')); const tplPkgInfo = require(path.join(src, 'package.json')); fsEditor.extendJSON(path.join(targetDir, 'package.json'), { boilerplate: { name: tplPkgInfo.name, version: tplPkgInfo.version, description: tplPkgInfo.description, repository: tplPkgInfo.repository, homepage: tplPkgInfo.homepage, }, }); // write file to disk yield new Promise(resolve => fsEditor.commit(resolve)); return files; }; * askForVariable(targetDir, templateDir) { let questions; try { questions = require(templateDir); // support function if (is.function(questions)) { questions = questions(this); } // use target dir name as `name` default if (questions.name && !questions.name.default) { questions.name.default = path.basename(targetDir).replace(/^egg-/, ''); } } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { this.log(`load boilerplate config got trouble, skip and use defaults, ${err.message}`.yellow); } return {}; } this.log('collecting boilerplate config...'); const keys = Object.keys(questions); if (this.argv.silent) { const result = keys.reduce((result, key) => { result[key] = questions[key].default || ''; return result; }, {}); this.log('use default due to --silent, %j', result); return result; } else { return yield inquirer.prompt(keys.map(key => { const question = questions[key]; return { type: 'input', name: key, message: question.description || question.desc, default: question.default, }; })); } }; replaceTemplate(content, scope) { return content.toString().replace(/(\\)?{{ *(\w+) *}}/g, (block, skip, key) => { if (skip) { return block.substring(skip.length); } return scope.hasOwnProperty(key) ? scope[key] : block; }); }; printUsage() { this.log(`usage: - cd ${this.targetDir} - npm install - npm start `); }; log() { const args = Array.prototype.slice.call(arguments); args[0] = `[${this.name}] `.blue + args[0]; console.log.apply(console, args); }; /** askForBoilerplateType(mapping) { // group by category const groupMapping = groupBy(mapping, (acc, value) => value.category || 'other'); const groupNames = Object.keys(groupMapping); let group; if (groupNames.length > 1) { const answers = yield inquirer.prompt({ name: 'group', type: 'list', message: 'Please select a boilerplate category', choices: groupNames, }); group = groupMapping[answers.group]; } else { group = groupMapping[groupNames[0]]; } // ask for boilerplate const choices = Object.keys(group).map(key => { const item = group[key]; return { name: `${key} - ${item.description}`, value: item, }; }); choices.unshift(new inquirer.Separator()); const answers = yield inquirer.prompt({ name: 'type', type: 'list', message: 'Please select a boilerplate type', choices, }); return answers.type; }; getRegistryByType(key) { switch (key) { case 'china': return 'https://registry.npm.taobao.org'; case 'npm': return 'https://registry.npmjs.org'; default: { if (/^https?:/.test(key)) { return key; } else { // support .npmrc const home = homedir(); let url = process.env.npm_registry || process.env.npm_config_registry || 'https://registry.cnpmjs.org'; if (fs.existsSync(path.join(home, '.cnpmrc')) || fs.existsSync(path.join(home, '.tnpmrc'))) { url = 'https://registry.npm.taobao.org'; } url = url.replace(/\/$/, ''); return url; } } } }; * fetchBoilerplateMapping(pkgName) { const pkgInfo = yield this.getPackageInfo(pkgName || this.configName, true); const mapping = pkgInfo.config.boilerplate; Object.keys(mapping).forEach(key => { const item = mapping[key]; item.name = item.name || key; item.from = pkgInfo; }); return mapping; }; * getPackageInfo(pkgName, withFallback) { try { const result = yield urllib.request(`${this.registryUrl}/${pkgName}/latest`, { dataType: 'json', followRedirect: true, }); return result.data; } catch (err) { if (withFallback) { this.log('use fallback from ${pkgName}'); return require(`${pkgName}/package.json`); } else { throw err; } } };*/ };