UNPKG

7.96 kBJavaScriptView Raw
1const path = require('path')
2const fs = require('fs-extra')
3const chalk = require('chalk')
4const resolveFrom = require('resolve-from')
5const downloadGitRepo = require('./downloadGitRepo')
6const loadConfig = require('./loadConfig')
7const paths = require('./paths')
8const spinner = require('./spinner')
9const BaseGeneratorContext = require('./GeneratorContext')
10const installPackages = require('./installPackages')
11const logger = require('./logger')
12const isLocalPath = require('./utils/isLocalPath')
13const SherryError = require('./SherryError')
14const parseGenerator = require('./parseGenerator')
15const updateCheck = require('./updateCheck')
16const store = require('./store')
17const PluginAPI = require('./PluginAPI')
18
19module.exports = class Sherry {
20 /**
21 * Create an instance of Sherry
22 * @param {Object} opts
23 */
24 constructor(opts) {
25 this.logger = logger
26 logger.setOptions(
27 process.argv.includes('--debug') || process.argv.includes('--verbose') ?
28 4 :
29 1
30 )
31 if (opts) {
32 this.setOptions(opts)
33 }
34 }
35
36 setCLI(cli) {
37 this.cli = cli
38 this.pluginAPI = new PluginAPI(this)
39 }
40
41 setOptions(opts) {
42 this.opts = Object.assign({}, opts)
43 this.opts.outDir = path.resolve(this.opts.outDir)
44 this.opts.npmClient = installPackages.setNpmClient(this.opts.npmClient)
45 this.logger = logger
46 logger.setOptions({
47 logLevel:
48 typeof this.opts.logLevel === 'number' ?
49 this.opts.logLevel :
50 this.opts.debug || process.argv.includes('--debug') || process.argv.includes('--verbose') ?
51 4 :
52 this.opts.quiet ?
53 1 :
54 3
55 })
56
57 this.parsedGenerator = parseGenerator(this.opts.generator)
58 logger.debug('parsedGenerator', this.parsedGenerator)
59 // Sub generator can only be used in an existing
60 if (this.parsedGenerator.subGenerator) {
61 logger.debug(
62 `Setting out directory to process.cwd() since it's a sub generator`
63 )
64 this.opts.outDir = process.cwd()
65 }
66 return this
67 }
68
69 /**
70 * Run the generator.
71 */
72 async run(generator, parent) {
73 generator = generator || this.parsedGenerator
74
75 if (generator.type === 'repo') {
76 await ensureRepo(generator, this.opts)
77 } else if (generator.type === 'npm') {
78 await ensurePackage(generator, this.opts)
79 } else if (generator.type === 'local') {
80 await ensureLocal(generator)
81 }
82
83 const loaded = await loadConfig(generator.path)
84 const config = loaded.path ?
85 loaded.data :
86 require(path.join(__dirname, 'sherry-config.fallback.js'))
87
88 // Only run following code for root generator
89 if (!parent) {
90 if (this.opts.updateCheck) {
91 updateCheck({
92 generator,
93 checkGenerator:
94 config.updateCheck !== false && generator.type === 'npm',
95 // Don't show the notifier after updated the generator
96 // Since the notifier is for the older version
97 showNotifier: !this.opts.update
98 })
99 }
100 // Keep the generator info
101 store.set(`generators.${generator.hash}`, generator)
102 }
103
104 if (generator.subGenerator) {
105 // TODO: remove `config.generators` support, you should use `subGenerators` instead
106 const subGenerators = config.subGenerators || config.generators
107 const subGenerator =
108 subGenerators &&
109 subGenerators.find(g => g.name === generator.subGenerator)
110 if (subGenerator) {
111 // TODO: remove `subGenerator.from` support, you should use `subGenerator.generator` instead
112 let generatorPath = subGenerator.generator || subGenerator.from
113 generatorPath = isLocalPath(generatorPath) ?
114 path.resolve(generator.path, generatorPath) :
115 resolveFrom(generator.path, generatorPath)
116 return this.run(parseGenerator(generatorPath), generator)
117 }
118 throw new SherryError(
119 `No such sub generator in generator ${generator.path}`
120 )
121 }
122
123 await this.runGenerator(generator, config)
124 }
125
126 async runGenerator(generator, config) {
127 if (config.description) {
128 logger.status('green', 'Generator', config.description)
129 }
130
131 const GeneratorContext = this.opts.getContext ?
132 this.opts.getContext(BaseGeneratorContext) :
133 BaseGeneratorContext
134 const generatorContext = new GeneratorContext(this, generator)
135 this.generatorContext = generatorContext
136
137 if (typeof config.prepare === 'function') {
138 await config.prepare.call(generatorContext, generatorContext)
139 }
140
141 if (config.prompts) {
142 await require('./runPrompts')(config, generatorContext)
143 }
144
145 generatorContext.data = typeof config.data === 'function' ?
146 config.data.call(generatorContext, generatorContext) :
147 (config.data || {})
148
149 if (config.actions) {
150 await require('./runActions')(config, generatorContext)
151 }
152
153 if (!this.opts.mock && config.completed) {
154 await config.completed.call(generatorContext, generatorContext)
155 }
156 }
157
158 applyPlugins(plugins) {
159 for (const { name, path } of plugins) {
160 logger.debug(`Apply plugin: ${name}`)
161 const pluginFn = require(path)
162 pluginFn(this.pluginAPI)
163 }
164 }
165}
166
167/**
168 * Download git repo
169 * @param {string} repo
170 * @param {string} target
171 * @param {Object=} opts
172 */
173function downloadRepo(repo, target, opts) {
174 return fs.remove(target).then(
175 () =>
176 new Promise((resolve, reject) => {
177 downloadGitRepo(repo, target, opts, err => {
178 if (err) return reject(err)
179 resolve()
180 })
181 })
182 )
183}
184
185/**
186 * Ensure packages are installed in a generator
187 * In most cases this is used for `repo` generators
188 * @param {Object} generator
189 * @param {Object} options
190 */
191async function ensureRepo(generator, { update, gitClone, registry, gitServer }) {
192 if (!update && (await fs.pathExists(generator.path))) {
193 return
194 }
195
196 // Download repo
197 spinner.start('Downloading repo')
198 try {
199 await downloadRepo(generator.slug, generator.path, { clone: gitClone, gitServer })
200 spinner.stop()
201 logger.success('Downloaded repo')
202 } catch (err) {
203 let message = err.message
204 if (err.host && err.path) {
205 message += '\n' + err.host + err.path
206 }
207 throw new SherryError(message)
208 }
209 // Only try to install dependencies for real generator
210 const [hasConfig, hasPackageJson] = await Promise.all([
211 loadConfig.hasConfig(generator.path),
212 fs.pathExists(path.join(generator.path, 'package.json'))
213 ])
214
215 if (hasConfig && hasPackageJson) {
216 await installPackages({
217 cwd: generator.path,
218 registry,
219 installArgs: ['--production']
220 })
221 }
222}
223
224async function ensureLocal(generator) {
225 const exists = await fs.pathExists(generator.path)
226
227 if (!exists) {
228 throw new SherryError(
229 `Directory ${chalk.underline(generator.path)} does not exist`
230 )
231 }
232}
233
234async function ensurePackage(generator, { update, registry }) {
235 const installPath = path.join(paths.packagePath, generator.hash)
236
237 if (update || !(await fs.pathExists(generator.path))) {
238 await fs.ensureDir(installPath)
239 await fs.writeFile(
240 path.join(installPath, 'package.json'),
241 JSON.stringify({
242 private: true
243 }),
244 'utf8'
245 )
246 logger.debug('Installing generator at', installPath)
247 await installPackages({
248 cwd: installPath,
249 registry,
250 packages: [`${generator.name}@${generator.version || 'latest'}`]
251 })
252 }
253}
254
255// async function ensureOutDir(outDir, force) {
256// if (force) return
257// if (!(await fs.pathExists(outDir))) return
258
259// const files = await fs.readdir(outDir)
260// if (files.length === 0) return
261
262// const answers = await require('inquirer').prompt([{
263// name: 'force',
264// message: `Directory ${chalk.underline(outDir)} already exists, do you want to continue`,
265// type: 'confirm',
266// default: false
267// }])
268// if (!answers.force) {
269// throw new SherryError(`Aborted`)
270// }
271// }