From 6d5c8848c476fc2770204f215ddd6f48d539b4e0 Mon Sep 17 00:00:00 2001 From: Franck Abgrall Date: Thu, 27 Jun 2019 13:22:56 +0200 Subject: [PATCH] :sparkles: Allow user to choose a non HTML README template (#80) --- src/__snapshots__/readme.spec.js.snap | 69 ++++++++++++++++- src/ask-questions.js | 24 ++---- src/ask-questions.spec.js | 4 +- src/choose-template.js | 33 ++++++++ src/choose-template.spec.js | 51 +++++++++++++ src/cli.js | 21 ++++-- src/cli.spec.js | 29 +++---- src/index.js | 19 ++--- src/readme.js | 30 +++++--- src/readme.spec.js | 76 +++++++++++-------- templates/default-no-html.md | 104 ++++++++++++++++++++++++++ 11 files changed, 358 insertions(+), 102 deletions(-) create mode 100644 src/choose-template.js create mode 100644 src/choose-template.spec.js create mode 100644 templates/default-no-html.md diff --git a/src/__snapshots__/readme.spec.js.snap b/src/__snapshots__/readme.spec.js.snap index 1b73c86..56a6492 100644 --- a/src/__snapshots__/readme.spec.js.snap +++ b/src/__snapshots__/readme.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`readme buildReadmeContent should return readme template content 1`] = ` +exports[`readme buildReadmeContent should return readme default template content 1`] = ` "

Welcome to readme-md-generator 👋

@@ -74,3 +74,70 @@ This project is [MIT](https://github.com/kefranabg/readme-md-generator/blob/mast *** _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_" `; + +exports[`readme buildReadmeContent should return readme default template no html content 1`] = ` +"# Welcome to readme-md-generator 👋 +![Version](https://img.shields.io/badge/version-0.1.3-blue.svg?cacheSeconds=2592000) +![Prerequisite](https://img.shields.io/badge/npm-%3E%3D5.5.0-blue.svg) +![Prerequisite](https://img.shields.io/badge/node-%3E%3D%209.3.0-blue.svg) +[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://github.com/kefranabg/readme-md-generator#readme) +[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/kefranabg/readme-md-generator/graphs/commit-activity) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/kefranabg/readme-md-generator/blob/master/LICENSE) +[![Twitter: FranckAbgrall](https://img.shields.io/twitter/follow/FranckAbgrall.svg?style=social)](https://twitter.com/FranckAbgrall) + +> Generates beautiful README files from git config & package.json infos + +### 🏠 [Homepage](https://github.com/kefranabg/readme-md-generator#readme) + +## Prerequisites + +- npm >=5.5.0 +- node >= 9.3.0 + +## Install + +\`\`\`sh +npm install +\`\`\` + +## Usage + +\`\`\`sh +npm start +\`\`\` + +## Run tests + +\`\`\`sh +npm run test +\`\`\` + +## Author + +👤 **Franck Abgrall** + +* Twitter: [@FranckAbgrall](https://twitter.com/FranckAbgrall) +* Github: [@kefranabg](https://github.com/kefranabg) + +## 🤝 Contributing + +Contributions, issues and feature requests are welcome! + +Feel free to check [issues page](https://github.com/kefranabg/readme-md-generator/issues). + +## Show your support + +Give a ⭐️ if this project helped you! + +[![support us](https://img.shields.io/badge/become-a patreon%20us-orange.svg?cacheSeconds=2592000)](https://www.patreon.com/FranckAbgrall) + + +## 📝 License + +Copyright © 2019 [Franck Abgrall](https://github.com/kefranabg). + +This project is [MIT](https://github.com/kefranabg/readme-md-generator/blob/master/LICENSE) licensed. + +*** +_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_" +`; diff --git a/src/ask-questions.js b/src/ask-questions.js index 34ac114..0594064 100644 --- a/src/ask-questions.js +++ b/src/ask-questions.js @@ -1,31 +1,21 @@ const inquirer = require('inquirer') +const { flatMap } = require('lodash') const questionsBuilders = require('./questions') const utils = require('./utils') -/** - * Get questions - * - * @param {Object} projectInfos - */ -const getQuestions = projectInfos => - Object.values(questionsBuilders).reduce( - (questions, questionBuilder) => [ - ...questions, - questionBuilder(projectInfos) - ], - [] - ) - /** * Ask user questions and return context to generate a README * * @param {Object} projectInfos + * @param {Boolean} useDefaultAnswers */ -module.exports = async (projectInfos, skipQuestions) => { - const questions = getQuestions(projectInfos) +module.exports = async (projectInfos, useDefaultAnswers) => { + const questions = flatMap(Object.values(questionsBuilders), questionBuilder => + questionBuilder(projectInfos) + ) - const answersContext = skipQuestions + const answersContext = useDefaultAnswers ? utils.getDefaultAnswers(questions) : await inquirer.prompt(questions) diff --git a/src/ask-questions.spec.js b/src/ask-questions.spec.js index 8c92796..c0a8999 100644 --- a/src/ask-questions.spec.js +++ b/src/ask-questions.spec.js @@ -40,7 +40,7 @@ describe('ask-questions', () => { it('should call all builder functions exported by questions', async () => { const projectInfos = { name: 'readme-md-generator' } - await askQuestions(projectInfos) + await askQuestions(projectInfos, false) expect(questions.askProjectName).toHaveBeenCalledTimes(1) expect(questions.askProjectVersion).toHaveBeenCalledTimes(1) @@ -66,7 +66,7 @@ describe('ask-questions', () => { it('should return merged contexts', async () => { const projectInfos = { name: 'readme-md-generator' } - const context = await askQuestions(projectInfos) + const context = await askQuestions(projectInfos, false) expect(context).toEqual({ projectName: 'value', diff --git a/src/choose-template.js b/src/choose-template.js new file mode 100644 index 0000000..064eb4d --- /dev/null +++ b/src/choose-template.js @@ -0,0 +1,33 @@ +const inquirer = require('inquirer') +const path = require('path') + +module.exports = async useDefaultAnswers => { + const defaultTemplate = path.resolve(__dirname, '../templates/default.md') + const defaultNoHtmlTemplate = path.resolve( + __dirname, + '../templates/default-no-html.md' + ) + + if (useDefaultAnswers) return defaultTemplate + + const question = { + type: 'list', + message: + '🎨 Use HTML in your README.md for a nicer rendering? (not supported everywhere. ex: Bitbucket)', + name: 'templatePath', + choices: [ + { + name: 'Yes ', + value: defaultTemplate + }, + { + name: 'No', + value: defaultNoHtmlTemplate + } + ] + } + + const { templatePath } = await inquirer.prompt([question]) + + return templatePath +} diff --git a/src/choose-template.spec.js b/src/choose-template.spec.js new file mode 100644 index 0000000..4a1bb80 --- /dev/null +++ b/src/choose-template.spec.js @@ -0,0 +1,51 @@ +const inquirer = require('inquirer') +const path = require('path') + +const chooseTemplate = require('./choose-template') + +const defaultTemplatePath = path.resolve(__dirname, '../templates/default.md') +const defaultNoHtmlTemplatePath = path.resolve( + __dirname, + '../templates/default-no-html.md' +) + +inquirer.prompt = jest.fn(() => + Promise.resolve({ templatePath: defaultTemplatePath }) +) + +describe('choose-template', () => { + it('should return user choice', async () => { + const result = await chooseTemplate(false) + + expect(result).toEqual(defaultTemplatePath) + }) + + it('should return default template', async () => { + const result = await chooseTemplate(true) + + expect(result).toEqual(defaultTemplatePath) + }) + + it('should call prompt with correct parameters', async () => { + await chooseTemplate(false) + + expect(inquirer.prompt).toHaveBeenNthCalledWith(1, [ + { + type: 'list', + message: + '🎨 Use HTML in your README.md for a nicer rendering? (not supported everywhere. ex: Bitbucket)', + name: 'templatePath', + choices: [ + { + name: 'Yes ', + value: defaultTemplatePath + }, + { + name: 'No', + value: defaultNoHtmlTemplatePath + } + ] + } + ]) + }) +}) diff --git a/src/cli.js b/src/cli.js index cab5ab6..2cec288 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,21 +1,28 @@ const readme = require('./readme') const infos = require('./project-infos') - const utils = require('./utils') const askQuestions = require('./ask-questions') /** * Main process: - * 1) Gather project infos - * 2) Ask user questions - * 3) Build README content - * 4) Create README.md file + * 1) Get README template path + * 2) Gather project infos + * 3) Ask user questions + * 4) Build README content + * 5) Create README.md file * * @param {Object} args */ -module.exports = async ({ templatePath, yes }) => { +module.exports = async ({ customTemplatePath, useDefaultAnswers }) => { + const templatePath = await readme.getReadmeTemplatePath( + customTemplatePath, + useDefaultAnswers + ) const projectInformations = await infos.getProjectInfos() - const answersContext = await askQuestions(projectInformations, yes) + const answersContext = await askQuestions( + projectInformations, + useDefaultAnswers + ) const readmeContent = await readme.buildReadmeContent( answersContext, templatePath diff --git a/src/cli.spec.js b/src/cli.spec.js index dcd532f..d6a5a4a 100644 --- a/src/cli.spec.js +++ b/src/cli.spec.js @@ -44,21 +44,29 @@ describe('mainProcess', () => { }) it('should call main functions with correct args', async () => { - const templatePath = 'default' + const customTemplatePath = undefined + const useDefaultAnswers = true const projectInformations = { name: 'readme-md-generator' } const readmeContent = 'content' + const templatePath = 'path/to/template' infos.getProjectInfos = jest.fn(() => Promise.resolve(projectInformations)) readme.buildReadmeContent = jest.fn(() => Promise.resolve(readmeContent)) + readme.getReadmeTemplatePath = jest.fn(() => Promise.resolve(templatePath)) readme.writeReadme = jest.fn() utils.showEndMessage = jest.fn() - await mainProcess({ templatePath }) + await mainProcess({ customTemplatePath, useDefaultAnswers }) + expect(readme.getReadmeTemplatePath).toHaveBeenNthCalledWith( + 1, + customTemplatePath, + useDefaultAnswers + ) expect(infos.getProjectInfos).toHaveBeenCalledTimes(1) expect(askQuestions).toHaveBeenNthCalledWith( 1, projectInformations, - undefined + useDefaultAnswers ) expect(readme.buildReadmeContent).toHaveBeenNthCalledWith( 1, @@ -68,19 +76,4 @@ describe('mainProcess', () => { expect(readme.writeReadme).toHaveBeenNthCalledWith(1, readmeContent) expect(utils.showEndMessage).toHaveBeenCalledTimes(1) }) - - it('should forward --yes option to askQuestions', async () => { - const template = 'default' - const projectInformations = { name: 'readme-md-generator' } - const skipQuestions = true - utils.showEndMessage = jest.fn() - - await mainProcess({ template, yes: skipQuestions }) - - expect(askQuestions).toHaveBeenNthCalledWith( - 1, - projectInformations, - skipQuestions - ) - }) }) diff --git a/src/index.js b/src/index.js index f7afa51..4ce4819 100755 --- a/src/index.js +++ b/src/index.js @@ -1,25 +1,16 @@ #!/usr/bin/env node const yargs = require('yargs') +const { noop } = require('lodash') const mainProcess = require('./cli') -const { getReadmeTemplatePath } = require('./readme') yargs .usage('Usage: $0 [options]') - .command( - '$0 [template]', - 'Generate README.md from a template', - command => - command.positional('template', { - desc: 'The name of template you want to use', - default: 'default' - }), - args => { - const templatePath = getReadmeTemplatePath(args) - mainProcess({ templatePath, yes: args.yes }) - } - ) + .command('$0', 'Generate README.md', noop, args => { + const { path: customTemplatePath, yes: useDefaultAnswers } = args + mainProcess({ customTemplatePath, useDefaultAnswers }) + }) .string('p') .alias('p', 'path') .describe('path', 'Path to your own template') diff --git a/src/readme.js b/src/readme.js index 1ad45cb..819753b 100644 --- a/src/readme.js +++ b/src/readme.js @@ -3,9 +3,10 @@ const ora = require('ora') const { promisify } = require('util') const getYear = require('date-fns/get_year') const fs = require('fs') -const path = require('path') const { isNil } = require('lodash') +const chooseTemplate = require('./choose-template') + const README_PATH = 'README.md' /** @@ -61,20 +62,13 @@ const buildReadmeContent = async (context, templatePath) => { } /** - * Get path to the readme template + * Validate template path * - * @param {string} availableTemplate - * @param {string} customTemplate + * @param {string} templatePath */ -const getReadmeTemplatePath = args => { +const validateReadmeTemplatePath = templatePath => { const spinner = ora('Resolving README template path').start() - const { template: availableTemplate, path: customTemplate } = args - - const templatePath = isNil(customTemplate) - ? path.resolve(__dirname, `../templates/${availableTemplate}.md`) - : customTemplate - try { fs.lstatSync(templatePath).isFile() } catch (err) { @@ -83,6 +77,20 @@ const getReadmeTemplatePath = args => { } spinner.succeed('README template path resolved') +} + +/** + * Get readme template path + * (either a custom template, or a template that user will choose from prompt) + * + * @param {String} customTemplate + */ +const getReadmeTemplatePath = async (customTemplate, useDefaultAnswers) => { + const templatePath = isNil(customTemplate) + ? await chooseTemplate(useDefaultAnswers) + : customTemplate + + validateReadmeTemplatePath(templatePath) return templatePath } diff --git a/src/readme.spec.js b/src/readme.spec.js index 871bbf5..471b404 100644 --- a/src/readme.spec.js +++ b/src/readme.spec.js @@ -1,8 +1,14 @@ const fs = require('fs') const ora = require('ora') const path = require('path') +const chooseTemplate = require('./choose-template') -jest.mock('ora') +const defaultTemplatePath = path.resolve(__dirname, '../templates/default.md') +const defaultNoHtmlTemplatePath = path.resolve( + __dirname, + '../templates/default-no-html.md' +) +chooseTemplate.mockReturnValue(defaultTemplatePath) const { writeReadme, @@ -69,7 +75,6 @@ describe('readme', () => { }) describe('buildReadmeContent', () => { - const templatePath = path.resolve(__dirname, '../templates/default.md') const context = { isGithubRepos: true, repositoryUrl: 'https://github.com/kefranabg/readme-md-generator', @@ -104,7 +109,7 @@ describe('readme', () => { }) it('should call ora with correct parameters in success case', async () => { - await buildReadmeContent(context, templatePath) + await buildReadmeContent(context, defaultTemplatePath) expect(ora).toHaveBeenCalledTimes(1) expect(ora).toHaveBeenCalledWith('Loading README template') @@ -112,8 +117,17 @@ describe('readme', () => { expect(succeed).toHaveBeenCalledWith('README template loaded') }) - it('should return readme template content', async () => { - const result = await buildReadmeContent(context, templatePath) + it('should return readme default template content', async () => { + const result = await buildReadmeContent(context, defaultTemplatePath) + + expect(result).toMatchSnapshot() + }) + + it('should return readme default template no html content', async () => { + const result = await buildReadmeContent( + context, + defaultNoHtmlTemplatePath + ) expect(result).toMatchSnapshot() }) @@ -124,7 +138,7 @@ describe('readme', () => { }) try { - await buildReadmeContent(context, templatePath) + await buildReadmeContent(context, defaultTemplatePath) // eslint-disable-next-line no-empty } catch (err) {} @@ -136,37 +150,40 @@ describe('readme', () => { }) describe('getReadmeTemplatePath', () => { - it('should return default template path if customTemplate is undefined', () => { - const args = { template: 'default' } - const actualResult = getReadmeTemplatePath(args) - const expectedResult = path.resolve(__dirname, '../templates/default.md') + it('should return template that user has selected', async () => { + const useDefaultAnswers = false + const actualResult = await getReadmeTemplatePath( + undefined, + useDefaultAnswers + ) - expect(actualResult).toEqual(expectedResult) + expect(actualResult).toEqual(defaultTemplatePath) + expect(chooseTemplate).toHaveBeenNthCalledWith(1, useDefaultAnswers) }) - it('should return custom template path if customTemplate is defined', () => { - const customTemplatePath = path.resolve( - __dirname, - '../templates/default.md' + it('should return custom template path if customTemplatePath is defined', async () => { + const customTemplatePath = defaultTemplatePath + + const actualResult = await getReadmeTemplatePath( + customTemplatePath, + false ) - const args = { template: 'default', path: customTemplatePath } - const expectedResult = customTemplatePath - const actualResult = getReadmeTemplatePath(args) - expect(actualResult).toEqual(expectedResult) + expect(actualResult).toEqual(customTemplatePath) + expect(chooseTemplate).not.toHaveBeenCalled() }) it('should throw an error if customTemplate is defined but invalid', () => { - const args = { template: 'default', path: 'wrong path' } + const wrongPath = 'wrong path' - expect(() => getReadmeTemplatePath(args)).toThrow() + expect(getReadmeTemplatePath(wrongPath, false)).rejects.toThrow() }) it('should call ora with correct parameters in fail case', async () => { - const args = { template: 'default', path: 'wrong path' } + const wrongPath = 'wrong path' try { - getReadmeTemplatePath(args) + await getReadmeTemplatePath(wrongPath, false) // eslint-disable-next-line no-empty } catch (err) {} @@ -178,15 +195,7 @@ describe('readme', () => { }) it('should call ora with correct parameters in success case', async () => { - const args = { - template: 'default', - path: path.resolve(__dirname, '../templates/default.md') - } - - try { - getReadmeTemplatePath(args) - // eslint-disable-next-line no-empty - } catch (err) {} + await getReadmeTemplatePath(defaultTemplatePath, false) expect(ora).toHaveBeenNthCalledWith(1, 'Resolving README template path') expect(succeed).toHaveBeenNthCalledWith( @@ -196,3 +205,6 @@ describe('readme', () => { }) }) }) + +jest.mock('ora') +jest.mock('./choose-template') diff --git a/templates/default-no-html.md b/templates/default-no-html.md new file mode 100644 index 0000000..f83cebc --- /dev/null +++ b/templates/default-no-html.md @@ -0,0 +1,104 @@ +# Welcome to <%= projectName %> 👋 +<% if (projectVersion) { -%> +![Version](https://img.shields.io/badge/version-<%= projectVersion %>-blue.svg?cacheSeconds=2592000) +<% } -%> +<% if (projectPrerequisites) { -%> +<% projectPrerequisites.map(({ name, value }) => { -%> +![Prerequisite](https://img.shields.io/badge/<%= name %>-<%= encodeURIComponent(value) %>-blue.svg) +<% }) -%> +<% } -%> +<% if (projectDocumentationUrl) { -%> +[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](<%= projectDocumentationUrl %>) +<% } -%> +<% if (isGithubRepos) { -%> +[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](<%= repositoryUrl %>/graphs/commit-activity) +<% } -%> +<% if (licenseName && licenseUrl) { -%> +[![License: <%= licenseName %>](https://img.shields.io/badge/License-<%= licenseName %>-yellow.svg)](<%= licenseUrl %>) +<% } -%> +<% if (authorTwitterUsername) { -%> +[![Twitter: <%= authorTwitterUsername %>](https://img.shields.io/twitter/follow/<%= authorTwitterUsername %>.svg?style=social)](https://twitter.com/<%= authorTwitterUsername %>) +<% } -%> +<% if (projectDescription) { -%> + +> <%= projectDescription %> +<% } -%> +<% if (projectHomepage) { -%> + +### 🏠 [Homepage](<%= projectHomepage %>) +<% } -%> +<% if (projectPrerequisites && projectPrerequisites.length) { -%> + +## Prerequisites + +<% projectPrerequisites.map(({ name, value }) => { -%> +- <%= name %> <%= value %> +<% }) -%> +<% } -%> +<% if (installCommand) { -%> + +## Install + +```sh +<%= installCommand %> +``` +<% } -%> +<% if (usage) { -%> + +## Usage + +```sh +<%= usage %> +``` +<% } -%> +<% if (testCommand) { -%> + +## Run tests + +```sh +<%= testCommand %> +``` +<% } -%> +<% if (authorName || authorTwitterUsername || authorGithubUsername) { -%> + +## Author +<% if (authorName) { %> +👤 **<%= authorName %>** +<% } %> +<% if (authorTwitterUsername) { -%> +* Twitter: [@<%= authorTwitterUsername %>](https://twitter.com/<%= authorTwitterUsername %>) +<% } -%> +<% if (authorGithubUsername) { -%> +* Github: [@<%= authorGithubUsername %>](https://github.com/<%= authorGithubUsername %>) +<% } -%> +<% } -%> +<% if (contributingUrl) { -%> + +## 🤝 Contributing + +Contributions, issues and feature requests are welcome! + +Feel free to check [issues page](<%= contributingUrl %>). +<% } -%> + +## Show your support + +Give a ⭐️ if this project helped you! +<% if (authorPatreonUsername) { -%> + +[![support us](https://img.shields.io/badge/become-a patreon%20us-orange.svg?cacheSeconds=2592000)](https://www.patreon.com/<%= authorPatreonUsername %>) +<% } -%> + +<% if (licenseName && licenseUrl) { -%> + +## 📝 License + +<% if (authorName && authorGithubUsername) { -%> +Copyright © <%= currentYear %> [<%= authorName %>](https://github.com/<%= authorGithubUsername %>). + +<% } -%> +This project is [<%= licenseName %>](<%= licenseUrl %>) licensed. +<% } -%> + +*** +<%- include('footer.md'); -%>