Shuunen/repo-checker

View on GitHub
src/files/readme.ts

Summary

Maintainability
D
1 day
Test Coverage
/* eslint-disable regexp/no-unused-capturing-group */
/* eslint-disable regexp/prefer-named-capture-group */
/* eslint-disable prefer-named-capture-group */
/* eslint-disable max-classes-per-file */
import { dataDefaults } from '../constants'
import { FileBase } from '../file'
import { log } from '../logger'
import { fileExists, join, readFile } from '../utils'

// eslint-disable-next-line no-restricted-syntax
class Thanks {
  public markdown = ''
  public label = ''
  public link = ''
  public description = ''
  public isExpected = true
  public isFixable = true

  // eslint-disable-next-line @typescript-eslint/max-params
  public constructor(label = '', link = '', description = '', isExpected = false, isFixable = true) {
    this.label = label
    this.link = link
    this.description = description
    this.isExpected = isExpected
    this.isFixable = isFixable
    this.markdown = `- [${label}](${link}) : ${description}`
  }
}

// eslint-disable-next-line no-restricted-syntax
class Badge {
  public markdown = ''
  public label = ''
  public link = ''
  public image = ''
  public isExpected = true
  public isFixable = true

  // eslint-disable-next-line @typescript-eslint/max-params
  public constructor(label = '', link = '', image = '', isExpected = true, isFixable = true) {
    this.label = label
    this.link = link
    this.image = image
    this.isExpected = isExpected
    this.isFixable = isFixable
    this.markdown = `[![${label}](${image})](${link})`
  }
}

const deprecatedBadges = [
  'bettercodehub.com',
  'david-dm.org',
  'lgtm.com',
  'libraries.io', // shows deprecated informations
]

/* c8 ignore start */
// eslint-disable-next-line no-restricted-syntax
export class ReadmeFile extends FileBase {
  private checkMarkdown() {
    let hasNoCrLf = this.shouldContains('no CRLF Windows carriage return', /\r/u, 0, false, 'prefer Unix LF', true)
    if (!hasNoCrLf && this.canFix) this.fileContent = this.fileContent.replace(/\r\n/gu, '\n')
    const starLists = /\n\*\s[\w[]/gu
    hasNoCrLf = this.couldContains('no star flavored list', starLists, 0, 'should use dash flavor', true)
    if (!hasNoCrLf && this.canFix) this.fileContent = this.fileContent.replace(/\n\*\s(?=[\w[])/gu, '\n- ')
  }

  private addBadge(line = '') {
    // just after project title
    this.fileContent = this.fileContent.replace(/^(# [\s\w-]+)/u, `$1${line}\n`)
  }

  private checkBadgesDeprecated() {
    for (const badge of deprecatedBadges) {
      // eslint-disable-next-line security/detect-non-literal-regexp
      const isOk = this.shouldContains(`no deprecated ${badge} badge`, new RegExp(badge, 'u'), 0, false, `${badge} does not exist anymore`, true)
      // eslint-disable-next-line security/detect-non-literal-regexp
      if (!isOk && this.canFix) this.fileContent = this.fileContent.replace(new RegExp(`.*${badge}.*\n`, 'giu'), '')
    }
  }

  private checkBadgesRecommended() {
    const badges = this.getBadgesRecommended()
    for (const badge of badges) {
      const message = `${badge.isExpected ? 'a' : 'no'} "${badge.label}" badge`
      // eslint-disable-next-line security/detect-non-literal-regexp, sonarjs/no-nested-template-literals
      const regex = new RegExp(`\\(${badge.link.replace('?', String.raw`\?`)}\\)`, 'u')
      const isOk = this.couldContains(message, regex, badge.isExpected ? 1 : 0, badge.markdown, badge.isExpected)
      if (!isOk && badge.isExpected && badge.isFixable && this.canFix) this.addBadge(badge.markdown)
    }
  }

  private checkBadges() {
    this.checkBadgesRecommended()
    this.checkBadgesDeprecated()
  }

  private getBadgesRecommended() {
    const userRepo = `${this.data.userId}/${this.data.repoId}`
    const list = [
      new Badge('Project license', `https://github.com/${userRepo}/blob/master/LICENSE`, `https://img.shields.io/github/license/${userRepo}.svg?color=informational`),
      new Badge('Code Climate maintainability', `https://codeclimate.com/github/${userRepo}`, `https://img.shields.io/codeclimate/maintainability/${userRepo}`),
    ]
    if (this.data.isWebPublished && !this.fileContent.includes('shields.io/website/')) list.push(new Badge('Website up', this.data.webUrl, `https://img.shields.io/website/https/${this.data.webUrl.replace('https://', '')}.svg`, true, this.data.webUrl !== dataDefaults.webUrl))
    if (this.data.isPublishedPackage)
      list.push(
        new Badge('Npm monthly downloads', `https://www.npmjs.com/package/${this.data.packageName}`, `https://img.shields.io/npm/dm/${this.data.packageName}.svg?color=informational`),
        new Badge('Npm version', `https://www.npmjs.com/package/${this.data.packageName}`, `https://img.shields.io/npm/v/${this.data.packageName}.svg?color=informational`),
        new Badge('Publish size', `https://bundlephobia.com/package/${this.data.packageName}`, `https://img.shields.io/bundlephobia/min/${this.data.packageName}?label=publish%20size`),
        new Badge('Install size', `https://packagephobia.com/result?p=${this.data.packageName}`, `https://badgen.net/packagephobia/install/${this.data.packageName}`),
      )
    return list
  }

  private addThanks(line = '') {
    // just after Thank title
    this.fileContent = this.fileContent.replace(/(## Thank.*\n{2})/u, `$1${line}\n`)
    log.debug('added line', line)
  }

  private checkTodos() {
    const matches = this.fileContent.match(/- \[ \] (.*)/gu)
    if (matches === null) return
    for (const line of matches) {
      // a todo line in markdown is like "- [ ] add some fancy gifs"
      const todo = line.replace('- [ ] ', '')
      log.info(`TODO : ${todo}`)
    }
  }

  // eslint-disable-next-line max-statements
  public async start() {
    const hasUpReadmeFile = await this.fileExists('README.md')
    const hasReadmeFile = await this.fileExists('readme.md')
    if (!(hasUpReadmeFile || hasReadmeFile)) {
      this.test(false, 'no README.md or readme.md file found')
      return
    }
    await this.inspectFile(hasUpReadmeFile ? 'README.md' : 'readme.md')
    this.shouldContains('a title', /^#\s\w+/u)
    this.couldContains('a logo image', /!\[logo\]\(.*\.\w{3,4}\)/u, 1, '![logo](folder/any-file.ext)')
    this.couldContains('a demo image or gif', /!\[demo\]\(.*\.\w{3,4}\)/u, 1, '![demo](folder/any-file.ext)')
    this.shouldContains('no link to deprecated *.netlify.com', /\.netlify\.com/u, 0)
    this.shouldContains('no links without https scheme', /[^:]\/\/[\w-]+\.\w+/u, 0) // https://stackoverflow.com/questions/9161769/url-without-httphttps
    this.checkMarkdown()
    this.checkTodos()
    this.checkBadges()
    await this.checkThanks()
  }

  private async checkThanks() {
    const hasSection = this.couldContains('a thanks section', /## Thanks/u)
    if (!hasSection) return
    const thanks = await this.getThanks()
    for (const thank of thanks) {
      const message = `${thank.isExpected ? 'a' : 'no remaining'} thanks to ${thank.label}`
      // eslint-disable-next-line security/detect-non-literal-regexp
      const regex = new RegExp(`\\[${thank.label}\\]`, 'iu')
      const hasThanks = this.couldContains(message, regex, thank.isExpected ? 1 : 0, thank.markdown, thank.isExpected)
      const shouldAdd = !hasThanks && thank.isExpected && thank.isFixable && this.canFix
      if (shouldAdd) this.addThanks(thank.markdown)
    }
  }

  private async getThanks() {
    const list = [
      new Thanks('Shields.io', 'https://shields.io', 'for the nice badges on top of this readme', this.fileContent.includes('shields')),
      new Thanks('Travis-ci.com', 'https://travis-ci.com', 'for providing free continuous deployments', this.fileContent.includes('travis-ci')),
      new Thanks('Github', 'https://github.com', 'for all their great work year after year, pushing OSS forward', this.fileContent.includes('github')),
      new Thanks('Netlify', 'https://netlify.com', 'awesome company that offers free CI & hosting for OSS projects', this.fileContent.includes('netlify')),
    ]
    const filePath = join(this.folderPath, 'package.json')
    if (!(await fileExists(filePath))) return list
    const json = await readFile(filePath)
    if (json === '') return list
    list.push(
      new Thanks('Arg', 'https://github.com/vercel/arg', 'un-opinionated, no-frills CLI argument parser', json.includes('"arg":')),
      new Thanks('Ava', 'https://github.com/avajs/ava', 'great test runner easy to setup & use', json.includes('ava"')),
      new Thanks('C8', 'https://github.com/bcoe/c8', 'simple & effective cli for code coverage', this.data.isUsingC8),
      new Thanks('Chokidar', 'https://github.com/paulmillr/chokidar', 'minimal and efficient cross-platform file watching library', json.includes('"chokidar"')),
      new Thanks('Cypress.io', 'https://www.cypress.io', 'cool E2E testing framework', json.includes('cypress')),
      new Thanks('Dependency-cruiser', 'https://github.com/sverweij/dependency-cruiser', 'handy tool to validate and visualize dependencies', this.data.isUsingDependencyCruiser),
      new Thanks('Esbuild', 'https://github.com/evanw/esbuild', 'an extremely fast JavaScript bundler and minifier', json.includes('esbuild')),
      new Thanks('Eslint', 'https://eslint.org', 'super tool to find & fix problems', this.data.isUsingEslint),
      new Thanks('Mocha', 'https://github.com/mochajs/mocha', 'great test runner easy to setup & use', json.includes('mocha"')),
      new Thanks('Npm-run-all', 'https://github.com/mysticatea/npm-run-all', 'to keep my npm scripts clean & readable', json.includes('npm-run-all"')),
      new Thanks('Npm-parallel', 'https://github.com/spion/npm-parallel', 'to keep my npm scripts clean & readable', json.includes('npm-parallel"')),
      new Thanks('Nuxt', 'https://github.com/nuxt/nuxt.js', 'minimalist framework for server-rendered Vue.js applications', json.includes('"nuxt"')),
      new Thanks('Nyc', 'https://github.com/istanbuljs/nyc', 'simple & effective cli for code coverage', this.data.isUsingNyc),
      new Thanks('Reef', 'https://github.com/cferdinandi/reef', 'a lightweight library for creating reactive, state-based components and UI', json.includes('reefjs"')),
      new Thanks('Repo-checker', 'https://github.com/Shuunen/repo-checker', 'eslint cover /src code and this tool the rest ^^', json.includes('repo-check"')),
      new Thanks('Rollup', 'https://rollupjs.org', 'a fast & efficient js module bundler', json.includes('rollup"')),
      new Thanks('Servor', 'https://github.com/lukejacksonn/servor', 'dependency free dev server for single page app development', json.includes('"servor"')),
      new Thanks('Showdown', 'https://github.com/showdownjs/showdown', 'a Markdown to HTML converter written in Javascript', json.includes('"showdown"')),
      new Thanks('Shuutils', 'https://github.com/Shuunen/shuutils', 'collection of pure JS utils', this.data.isUsingShuutils),
      new Thanks('TailwindCss', 'https://tailwindcss.com', 'awesome lib to produce maintainable style', this.data.isUsingTailwind),
      new Thanks('Tsup', 'https://github.com/egoist/tsup', 'super fast js/ts bundler with no config, powered by esbuild <3', json.includes('tsup"')),
      new Thanks('UvU', 'https://github.com/lukeed/uvu', 'extremely fast and lightweight test runner for Node.js and the browser', json.includes('"uvu":')),
      new Thanks('V8', 'https://github.com/demurgos/v8-coverage', 'simple & effective cli for code coverage', this.data.isUsingV8),
      new Thanks('Vitest', 'https://github.com/vitest-dev/vitest', 'super fast vite-native testing framework', json.includes('"vitest"')),
      new Thanks('Vite', 'https://github.com/vitejs/vite', 'super fast frontend tooling', json.includes('vitejs') || json.includes('"vite"')),
      new Thanks('Vue', 'https://vuejs.org', 'when I need a front framework, this is the one I choose <3', this.data.isUsingVue),
      new Thanks('Watchlist', 'https://github.com/lukeed/watchlist', 'recursively watch a list of directories & run a command on any file system', json.includes('"watchlist"')),
      new Thanks('Xo', 'https://github.com/xojs/xo', 'super tool to find & fix problems', json.includes('"xo"')),
      // new Thanks('name', 'url', 'desc', json.includes('"xxx":')),
    )
    return list
  }
}
/* c8 ignore stop */