UNPKG

14.3 kBJavaScriptView Raw
1'use strict'
2
3const debug = require('debug')('gen')
4const Generator = require('yeoman-generator')
5const _ = require('lodash')
6const originUrl = require('git-remote-origin-url')
7const fs = require('fs')
8const exists = fs.existsSync
9const path = require('path')
10const fixpack = require('fixpack')
11const packageFilename = 'package.json'
12const usernameFromGithubUrl = require('./github-username')
13const getRepoDescripton = require('./github-description')
14const defaults = require('./defaults')
15const la = require('lazy-ass')
16const is = require('check-more-types')
17const withoutScope = require('./without-scope')
18const remoteGitUtils = require('./https-remote-git-url')
19const errors = require('./errors')
20const getBadges = require('./badges').getBadges
21const getBadgesUrls = require('./badges').getBadgesUrls
22
23function isEmpty (x) {
24 return x
25}
26
27function _printVersion () {
28 const rootFolder = path.join(__dirname, '..')
29 const myPackageFilename = path.join(rootFolder, 'package.json')
30 const myPackage = require(myPackageFilename)
31 console.log('using %s@%s', myPackage.name, myPackage.version)
32 console.log('installed in %s', rootFolder)
33}
34
35const g = Generator.extend({
36 printVersion () {
37 _printVersion()
38 },
39
40 setDefaults () {
41 this.answers = defaults
42 },
43
44 copyNpmrc () {
45 debug('Copying .npmrc file')
46 this.fs.copy(this.templatePath('npmrc'), this.destinationPath('.npmrc'))
47 },
48
49 copyGitignore () {
50 debug('Copying .gitignore file')
51 this.fs.copy(
52 this.templatePath('gitignore'),
53 this.destinationPath('.gitignore')
54 )
55 },
56
57 copyIssueTemplate () {
58 debug('Copying issue template')
59 this.fs.copy(
60 this.templatePath('issue_template.md'),
61 this.destinationPath('issue_template.md')
62 )
63 },
64
65 git () {
66 debug('Looking for .git folder')
67 if (!exists('.git')) {
68 console.error(
69 'Cannot find .git folder, please initialize the Git repo first'
70 )
71 console.error('git init')
72 console.error('git remote add origin ...')
73 process.exit(-1)
74 }
75 },
76
77 gitOrigin () {
78 debug('Getting Git origin url')
79 const done = this.async()
80 originUrl()
81 .then(url => {
82 la(is.unemptyString(url), 'could not get github origin url')
83 debug('got remote origin url', url)
84 this.repoDomain = remoteGitUtils.getDomain(url)
85 debug('repo domain', this.repoDomain)
86 this.originUrl = remoteGitUtils.gitRemoteToHttps(url)
87 debug('git origin HTTPS url', this.originUrl)
88 done()
89 })
90 .catch(errors.onGitOriginError)
91 },
92
93 author () {
94 this.answers = _.extend(this.answers, {
95 author: this.user.git.name() + ' <' + this.user.git.email() + '>'
96 })
97 },
98
99 githubUsername () {
100 // HACK, cannot get github username reliably from email
101 // hitting api rate limits
102 // parse github url instead
103 this.githubUsername = usernameFromGithubUrl(this.originUrl)
104 debug('got github username', this.githubUsername)
105 console.assert(
106 this.githubUsername,
107 'Could not get github username from url ' + this.originUrl
108 )
109 },
110
111 _recordAnswers (answers) {
112 la(is.unemptyString(answers.name), 'missing name', answers)
113
114 if (is.not.unemptyString(answers.description)) {
115 errors.onMissingDescription()
116 }
117 if (is.not.unemptyString(answers.keywords)) {
118 errors.onMissingKeywords()
119 }
120
121 answers.keywords = answers.keywords.split(',').filter(isEmpty)
122 this.answers = _.extend(defaults, answers)
123 la(
124 is.unemptyString(this.answers.name),
125 'missing full name',
126 this.answers.name
127 )
128 this.answers.noScopeName = withoutScope(this.answers.name)
129 la(
130 is.unemptyString(this.answers.noScopeName),
131 'could not compute name without scope from',
132 this.answers.name
133 )
134 debug('got answers to my questions')
135 debug('answers to main questions')
136 debug('- name', this.answers.name)
137 debug('- description', this.answers.description)
138 debug('- keywords', this.answers.keywords)
139 debug('- typescript', this.answers.typescript)
140 debug('- immutable', this.answers.immutable)
141 debug('- renovateApp', this.answers.renovateApp)
142
143 la(
144 is.bool(this.answers.typescript),
145 'expected boolean typescript',
146 this.answers.typescript
147 )
148 la(
149 is.bool(this.answers.immutable),
150 'expected boolean immutable',
151 this.answers.immutable
152 )
153 if (this.answers.typescript) {
154 console.log('⚠️ Cannot lint TypeScript with immutable yet')
155 this.answers.immutable = false
156 }
157
158 if (this.answers.immutable) {
159 this.answers.scripts.postlint = 'eslint --fix src/*.js'
160 this.answers.eslintConfig = {
161 env: {
162 es6: true
163 },
164 plugins: ['immutable'],
165 rules: {
166 'no-var': 2,
167 'immutable/no-let': 2,
168 'immutable/no-this': 2,
169 'immutable/no-mutation': 2
170 }
171 }
172 }
173 },
174
175 _readAnswersFromFile (filename) {
176 la(is.unemptyString(filename), 'missing answers filename', filename)
177 la(exists(filename), 'cannot find file', filename)
178 la(is.isJson(filename), 'answers file should be JSON', filename)
179 const answers = require(filename)
180 return Promise.resolve(answers)
181 },
182
183 _fillDefaultAnswers () {
184 return getRepoDescripton(this.originUrl).then(description => ({
185 description
186 }))
187 },
188
189 projectInformation () {
190 debug('getting project name and other details')
191 const recordAnswers = this._recordAnswers.bind(this)
192
193 const answersFilename = path.join(process.cwd(), 'answers.json')
194 if (exists(answersFilename)) {
195 debug('reading answers from file', answersFilename)
196 return this._readAnswersFromFile(answersFilename).then(recordAnswers)
197 }
198
199 return this._fillDefaultAnswers().then(answers => {
200 const questions = [
201 {
202 type: 'input',
203 name: 'name',
204 message: 'Your project name',
205 default: _.kebabCase(this.appname),
206 store: false
207 },
208 {
209 type: 'input',
210 name: 'description',
211 message: 'Project description',
212 default: answers.description || '',
213 store: false
214 },
215 {
216 type: 'input',
217 name: 'keywords',
218 message: 'Comma separated keywords',
219 store: false
220 },
221 {
222 type: 'confirm',
223 name: 'typescript',
224 message: 'Do you want to use TypeScript? (alpha)',
225 default: false,
226 store: false
227 },
228 {
229 type: 'confirm',
230 name: 'immutable',
231 message: 'Do you want to prevent data mutations? (alpha)',
232 default: false,
233 store: false
234 },
235 {
236 type: 'confirm',
237 name: 'renovateApp',
238 message: 'Do you want to use RenovateApp?',
239 default: true,
240 store: false
241 }
242 ]
243 return this.prompt(questions).then(recordAnswers)
244 })
245 },
246
247 repo () {
248 debug('getting repo details')
249 this.answers = _.extend(this.answers, {
250 repository: {
251 type: 'git',
252 url: this.originUrl
253 }
254 })
255 },
256
257 homepage () {
258 const domain = this.repoDomain
259 const user = this.githubUsername
260 const name = this.answers.noScopeName
261 this.answers.homepage = `https://${domain}/${user}/${name}#readme`
262 la(
263 is.strings([domain, user, name]),
264 'missing information to construct homepage url',
265 this.answers.homepage
266 )
267 debug('home is', this.answers.homepage)
268 },
269
270 bugs () {
271 const domain = this.repoDomain
272 const user = this.githubUsername
273 const name = this.answers.noScopeName
274 this.answers.bugs = `https://${domain}/${user}/${name}/issues`
275 la(
276 is.strings([domain, user, name]),
277 'missing information to construct bugs url',
278 this.answers.bugs
279 )
280 debug('bugs url is', this.answers.bugs)
281 },
282
283 copyRenovateConfig () {
284 if (!this.answers.renovateApp) {
285 debug('no need to set up renovate')
286 return
287 }
288 debug('copying renovate config')
289 this.fs.copyTpl(
290 this.templatePath('renovate.json'),
291 this.destinationPath('renovate.json')
292 )
293 },
294
295 copyReadme () {
296 debug('copying readme')
297 // need bunch of badges
298 const options = {
299 name: this.answers.name,
300 repoName: this.answers.noScopeName,
301 username: this.githubUsername,
302 npmBadge: true,
303 travisBadge: true,
304 semanticReleaseBadge: true,
305 standardBadge: true,
306 renovateBadge: this.answers.renovateApp
307 }
308
309 const badges = getBadges(options)
310 la(is.unemptyString(badges), 'expected badges markdown', options)
311
312 const badgesUrls = getBadgesUrls(options)
313 la(is.unemptyString(badgesUrls), 'expected badges urls', options)
314
315 const readmeContext = {
316 name: this.answers.name,
317 repoName: this.answers.noScopeName,
318 description: this.answers.description,
319 author: this.answers.author,
320 year: new Date().getFullYear(),
321 username: this.githubUsername,
322 badges: badges,
323 badgesUrls: badgesUrls
324 }
325 debug('Copying readme template with values', readmeContext)
326
327 this.fs.copyTpl(
328 this.templatePath('README.md'),
329 this.destinationPath('README.md'),
330 readmeContext
331 )
332 },
333
334 copySourceFiles () {
335 debug('copying source files')
336
337 // entry file
338 const index = this.answers.typescript ? 'src/index.ts' : 'src/index.js'
339 this.fs.copyTpl(
340 this.templatePath('index.js'),
341 this.destinationPath(index),
342 _.pick(this.answers, ['typescript', 'immutable'])
343 )
344
345 // default spec file
346 const name = _.kebabCase(this.answers.noScopeName)
347 const specName = this.answers.typescript
348 ? name + '-spec.ts'
349 : name + '-spec.js'
350 const specFilename = path.join('src', specName)
351 const info = {
352 name: this.answers.name,
353 nameVar: _.camelCase(this.answers.noScopeName),
354 typescript: this.answers.typescript
355 }
356 this.fs.copyTpl(
357 this.templatePath('spec.js'),
358 this.destinationPath(specFilename),
359 info
360 )
361 debug('copied index and spec files')
362 },
363
364 copyTypeScriptFiles () {
365 if (!this.answers.typescript) {
366 debug('skipping TypeScript files')
367 return
368 }
369
370 this.fs.copy(
371 this.templatePath('tsconfig.json'),
372 this.destinationPath('tsconfig.json')
373 )
374
375 this.fs.copy(
376 this.templatePath('tslint.json'),
377 this.destinationPath('tslint.json')
378 )
379 debug('copied TypeScript config files')
380 },
381
382 report () {
383 debug('all values')
384 debug(JSON.stringify(this.answers, null, 2))
385 },
386
387 writePackage () {
388 debug('writing package.json file')
389 // remove all keywords used to pass options via answers
390 const clean = _.omit(this.answers, [
391 'noScopeName',
392 'repoDomain',
393 'typescript',
394 'immutable',
395 'renovateApp'
396 ])
397
398 if (this.answers.typescript) {
399 debug('setting TypeScript build step')
400 clean.scripts.build = 'tsc'
401 clean.scripts.pretest = 'npm run build'
402 clean.scripts.lint = 'tslint --fix --format stylish src/**/*.ts'
403 clean.scripts.unit = 'mocha build/*-spec.js'
404 clean.files = [
405 'src/*.ts',
406 'build/*.js',
407 '!src/*-spec.ts',
408 '!build/*-spec.js'
409 ]
410 clean.main = 'build/'
411 }
412
413 const str = JSON.stringify(clean, null, 2) + '\n'
414 fs.writeFileSync(packageFilename, str, 'utf8')
415 },
416
417 fixpack () {
418 debug('fixing package.json')
419 fixpack(packageFilename)
420 },
421
422 install () {
423 debug('installing dependencies')
424 const devDependencies = [
425 'ban-sensitive-files',
426 'dependency-check',
427 'deps-ok',
428 'git-issues',
429 'license-checker',
430 'mocha',
431 'nsp',
432 'pre-git',
433 'prettier-standard'
434 ]
435 if (this.answers.immutable) {
436 devDependencies.push('eslint', 'eslint-plugin-immutable')
437 }
438 if (this.answers.typescript) {
439 devDependencies.push(
440 'tslint',
441 'tslint-config-standard',
442 'typescript',
443 '@types/mocha'
444 )
445 } else {
446 // linting JavaScript
447 devDependencies.push('standard')
448 }
449 const installOptions = {
450 saveDev: true,
451 depth: 0
452 }
453 return this.npmInstall(devDependencies, installOptions)
454 },
455
456 end: {
457 lintTypeScript () {
458 if (!this.answers.typescript) {
459 return
460 }
461 debug('linting typescript')
462 const done = this.async()
463 const child = this.spawnCommand('npm', ['run', 'lint'])
464 child.on('close', exitCode => {
465 if (exitCode) {
466 const msg = 'Could not lint TypeScript code'
467 console.error(msg)
468 console.error('exit code', exitCode)
469 return done(new Error(msg))
470 }
471 done()
472 })
473 },
474
475 endAndBuildTypeScript () {
476 if (!this.answers.typescript) {
477 return
478 }
479 debug('building from typescript')
480 const done = this.async()
481 const child = this.spawnCommand('npm', ['run', 'build'])
482 child.on('close', exitCode => {
483 if (exitCode) {
484 const msg = 'Could not build from TypeScript code'
485 console.error(msg)
486 console.error('exit code', exitCode)
487 return done(new Error(msg))
488 }
489 done()
490 })
491 },
492
493 printSemanticReleaseAdvice () {
494 console.log('Solid Node project has been setup for you 🎉🎉🎉')
495
496 if (this.answers.typescript) {
497 console.log('TypeScript source code in src/ folder')
498 console.log('run "npm run build" to build JS code')
499 console.log('generated JavaScript code in build/ folder')
500 }
501
502 if (remoteGitUtils.isGithub(this.originUrl)) {
503 console.log('Please consider using semantic release to publish to NPM')
504 console.log(' npm i -g semantic-release-cli')
505 console.log('and then run this generator again')
506 console.log(' yo node-bahmutov:release')
507 } else if (remoteGitUtils.isGitlab(this.originUrl)) {
508 console.log('Please consider using semantic release to publish to NPM')
509 console.log(
510 'See https://gitlab.com/hyper-expanse/semantic-release-gitlab'
511 )
512 }
513 }
514 }
515})
516
517module.exports = g