UNPKG

sao

Version:

Futuristic scaffolding tool ⚔

257 lines (228 loc) 7.52 kB
const path = require('path') const fs = require('fs-extra') const chalk = require('chalk') const downloadGitRepo = require('download-git-repo') const resolveFrom = require('resolve-from') const loadConfig = require('./loadConfig') const paths = require('./paths') const spinner = require('./spinner') const BaseGeneratorContext = require('./GeneratorContext') const installPackages = require('./installPackages') const logger = require('./logger') const isLocalPath = require('./utils/isLocalPath') const SAOError = require('./SAOError') const parseGenerator = require('./parseGenerator') const updateCheck = require('./updateCheck') const store = require('./store') class SAO { /** * Create an instance of SAO * @param {Object} opts */ constructor(opts) { this.opts = Object.assign({}, opts) this.opts.outDir = path.resolve(this.opts.outDir) this.opts.npmClient = installPackages.setNpmClient(this.opts.npmClient) this.logger = logger logger.setOptions({ logLevel: typeof this.opts.logLevel === 'number' ? this.opts.logLevel : this.opts.debug ? 4 : this.opts.quiet ? 1 : 3 }) this.parsedGenerator = parseGenerator(this.opts.generator) // Sub generator can only be used in an existing if (this.parsedGenerator.subGenerator) { logger.debug( `Setting out directory to process.cwd() since it's a sub generator` ) this.opts.outDir = process.cwd() } } /** * Run the generator. */ async run(generator, parent) { generator = generator || this.parsedGenerator if (generator.type === 'repo') { await ensureRepo(generator, this.opts) } else if (generator.type === 'npm') { await ensurePackage(generator, this.opts) } else if (generator.type === 'local') { await ensureLocal(generator) } const loaded = await loadConfig(generator.path) const config = loaded.path ? loaded.data : require(path.join(__dirname, 'saofile.fallback.js')) // Only run following code for root generator if (!parent) { if (this.opts.updateCheck) { updateCheck({ generator, checkGenerator: config.updateCheck !== false && generator.type === 'npm', // Don't show the notifier after updated the generator // Since the notifier is for the older version showNotifier: !this.opts.update }) } // Keep the generator info store.set(`generators.${generator.hash}`, generator) } if (generator.subGenerator) { // TODO: remove `config.generators` support, you should use `subGenerators` instead const subGenerators = config.subGenerators || config.generators const subGenerator = subGenerators && subGenerators.find(g => g.name === generator.subGenerator) if (subGenerator) { // TODO: remove `subGenerator.from` support, you should use `subGenerator.generator` instead let generatorPath = subGenerator.generator || subGenerator.from generatorPath = isLocalPath(generatorPath) ? path.resolve(generator.path, generatorPath) : resolveFrom(generator.path, generatorPath) return this.run(parseGenerator(generatorPath), generator) } throw new SAOError(`No such sub generator in generator ${generator.path}`) } await this.runGenerator(generator, config) } async runGenerator(generator, config) { if (config.description) { logger.status('green', 'Generator', config.description) } const GeneratorContext = this.opts.getContext ? this.opts.getContext(BaseGeneratorContext) : BaseGeneratorContext const generatorContext = new GeneratorContext(this, generator) this.generatorContext = generatorContext if (typeof config.prepare === 'function') { await config.prepare.call(generatorContext, generatorContext) } if (this.opts.answers) { try { generatorContext._answers = typeof this.opts.answers === 'string' ? JSON.parse(this.opts.answers) : this.opts.answers } catch (err) { throw new Error(`Answers JSON could not be parsed (${err.message})`) } } else if (config.prompts) { await require('./runPrompts')(config, generatorContext) } if (config.actions) { await require('./runActions')(config, generatorContext) } if (!this.opts.mock && config.completed) { await config.completed.call(generatorContext, generatorContext) } } } /** * Create an instance of SAO * @param {Object} opts */ module.exports = opts => new SAO(opts) module.exports.mock = require('./mock') module.exports.handleError = require('./handleError') /** * Download git repo * @param {string} repo * @param {string} target * @param {Object=} opts */ function downloadRepo(repo, target, opts) { return fs.remove(target).then( () => new Promise((resolve, reject) => { downloadGitRepo(repo, target, opts, err => { if (err) return reject(err) resolve() }) }) ) } /** * Ensure packages are installed in a generator * In most cases this is used for `repo` generators * @param {Object} generator * @param {Object} options */ async function ensureRepo(generator, { update, clone, registry }) { if (!update && (await fs.pathExists(generator.path))) { return } // Download repo spinner.start('Downloading repo') try { await downloadRepo(generator.slug, generator.path, { clone }) spinner.stop() logger.success('Downloaded repo') } catch (err) { let message = err.message if (err.host && err.path) { message += '\n' + err.host + err.path } throw new SAOError(message) } // Only try to install dependencies for real generator const [hasConfig, hasPackageJson] = await Promise.all([ loadConfig.hasConfig(generator.path), fs.pathExists(path.join(generator.path, 'package.json')) ]) if (hasConfig && hasPackageJson) { await installPackages({ cwd: generator.path, registry, installArgs: ['--production'] }) } } async function ensureLocal(generator) { const exists = await fs.pathExists(generator.path) if (!exists) { throw new SAOError( `Directory ${chalk.underline(generator.path)} does not exist` ) } } async function ensurePackage(generator, { update, registry }) { const installPath = path.join(paths.packagePath, generator.hash) if (update || !(await fs.pathExists(generator.path))) { await fs.ensureDir(installPath) await fs.writeFile( path.join(installPath, 'package.json'), JSON.stringify({ private: true }), 'utf8' ) logger.debug('Installing generator at', installPath) await installPackages({ cwd: installPath, registry, packages: [`${generator.name}@${generator.version || 'latest'}`] }) } } // async function ensureOutDir(outDir, force) { // if (force) return // if (!(await fs.pathExists(outDir))) return // const files = await fs.readdir(outDir) // if (files.length === 0) return // const answers = await require('inquirer').prompt([{ // name: 'force', // message: `Directory ${chalk.underline(outDir)} already exists, do you want to continue`, // type: 'confirm', // default: false // }]) // if (!answers.force) { // throw new SAOError(`Aborted`) // } // }