1 | 'use strict'
|
2 |
|
3 | const debug = require('debug')('gen')
|
4 | const Generator = require('yeoman-generator')
|
5 | const _ = require('lodash')
|
6 | const originUrl = require('git-remote-origin-url')
|
7 | const fs = require('fs')
|
8 | const exists = fs.existsSync
|
9 | const path = require('path')
|
10 | const fixpack = require('fixpack')
|
11 | const packageFilename = 'package.json'
|
12 | const usernameFromGithubUrl = require('./github-username')
|
13 | const getRepoDescripton = require('./github-description')
|
14 | const defaults = require('./defaults')
|
15 | const la = require('lazy-ass')
|
16 | const is = require('check-more-types')
|
17 | const withoutScope = require('./without-scope')
|
18 | const remoteGitUtils = require('./https-remote-git-url')
|
19 | const errors = require('./errors')
|
20 | const getBadges = require('./badges').getBadges
|
21 | const getBadgesUrls = require('./badges').getBadgesUrls
|
22 |
|
23 | function isEmpty (x) {
|
24 | return x
|
25 | }
|
26 |
|
27 | function _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 |
|
35 | const 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 |
|
101 |
|
102 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
517 | module.exports = g
|