All files / libs/fs inject.js

100% Statements 60/60
100% Branches 41/41
100% Functions 8/8
100% Lines 52/52
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152                1x 1x 1x   1x           1x           1x 1x                                                               1x 17x 17x 17x 17x   17x 15x 15x   12x 11x   11x       17x 17x 17x 5x 5x 14x       5x     17x 17x   15x       15x 19x 17x 17x   17x 1x       17x   15x 15x   1x 1x   1x     16x 16x 16x               16x 15x         19x 19x 19x   1x     18x   17x       15x                
/**
 *
 * @module      libs/fs/inject
 * @createdAt   2017-06-12
 *
 * @copyright   Copyright (c) 2017 Zhonglei Qiu
 * @license     Licensed under the MIT license.
 */
var fs = require('fs')
var escapeRegExp = require('../lang/escapeRegExp')
var EOL = '\n'
 
var TAGS_MAP = {
  hash: ['## ', ' ##', '## ', ' ##'],
  docs: ['/*# ', ' #*/', '/*# ', ' #*/'],
  html: ['<!--# ', ' #-->', '<!--# ', ' #-->'],
  loose: ['# ', ' #', '# ', ' #']   // 最宽松的模式
}
var TAGS_FILE_EXTENSIONS = {
  hash: ['gitignore', 'sh', 'bash', 'npmignore'],
  docs: ['js', 'jsx', 'css', 'sass', 'ts', 'tsx', 'json'],
  html: ['html', 'md']
}
 
var TAG_START_KEYWORD = 'INJECT_START'
var TAG_END_KEYWORD = 'INJECT_END'
 
/**
 * 根据文件 file 中的注释,注入 data 中对应的内容
 *
 * GROUP   |  EXTENSIONS                          |  TAGS
 * --------|--------------------------------------------------------------
 * hash    |  gitignore, sh, bash                 | ['## ', ' ##', '## ', ' ##']
 * docs    |  js, jsx, css, sass, ts, tsx, json   | (文档注释加 "#")
 * html    |  html, md                            | ['<!--# ', ' #-->', '<!--# ', ' #-->']
 * loose   |  支持上面所有的后缀                     | ['# ', ' #', '# ', ' #']
 *
 * @param  {string} file      要注入的文件的文件路径
 * @param  {Object} data      要注入的内容
 * @param  {Object} [options] 选项
 * @param  {string|Array<string>} [options.tags]     [tagStartLeft, tagStartRight, tagEndLeft, tagEndRight]
 * @param  {boolean} [options.autoPrefixSpaces = true]  自动根据最后一个注释前的空格给每一行都添加相同的空格
 * @param  {boolean} [options.returnContent = false]  返回注入的内容,而不是直接注入
 * @param  {boolean} [options.append = false]  是否将内容 append 到原区块之后,而不是整体替换
 * @example
 * bash 中可以这样写: (type 默认是 string,可以不写,另外支持 file,这时 key 对应的 value 是文件地址)
 *
 *  ## INJECT_START {"type": "string", "key": "ignores"} ##
 *  ## INJECT_END ##
 *
 * 或
 *
 *  ## INJECT_START ignores ##
 *  ## INJECT_END ##
 *
 * @return {number}         返回注入成功的数量
 */
module.exports = function inject(file, data, options) {
  options = options || {}
  if (!('autoPrefixSpaces' in options)) options.autoPrefixSpaces = true
  var content = fs.readFileSync(file).toString()
  var counter = {count: 0}
 
  var tags = getTags(file, options)
  var regexp = buildRegExp(tags, TAG_START_KEYWORD, TAG_END_KEYWORD)
  var newContent = content.replace(regexp, replaceContent(data, counter, options))
 
  if (options.returnContent) return newContent
  if (newContent !== content) fs.writeFileSync(file, newContent)
 
  return counter.count
}
 
function getTags(file, options) {
  var tagsKey = options.tags
  var tags = tagsKey
  if (!tagsKey) {
    var extension = file.split('.').pop()
    tagsKey = Object.keys(TAGS_FILE_EXTENSIONS).find(function(key) {
      return TAGS_FILE_EXTENSIONS[key].indexOf(extension) >= 0
    })
 
    // if (!tagsKey) throw new Error('Can not judge the tags from current file extension')
    if (!tagsKey) tagsKey = 'loose' // 无法判断出就使用 loose 模式
  }
 
  if (typeof tagsKey === 'string') tags = TAGS_MAP[tagsKey]
  if (!Array.isArray(tags) || tags.length !== 4) throw new Error('Tags options should be Array and contains 4 string items')
 
  return tags
}
 
function replaceContent(data, counter, options) {
  return function(raw, startLine, jsonString, oldValue, endLine, spaces) {
    var json = checkJsonString(jsonString)
    var type = json.type || 'string'
    var dataValue = data[json.key] || ''
 
    if (typeof dataValue === 'function') {
      dataValue = dataValue(oldValue)
    }
 
    var replaceValue
    switch (type) {
      case 'string':
        replaceValue = dataValue
        break
      case 'file':
        replaceValue = fs.readFileSync(dataValue).toString()
        break
      default:
        throw new Error('Not supported inject type<' + data.type + '>')
    }
 
    counter.count++
    var eol = options.eol == null ? EOL : options.eol
    return startLine.trim()
      + (options.append ? oldValue : eol)
      + prefixSpaces(replaceValue, spaces, options.autoPrefixSpaces) + (replaceValue && eol)
      + endLine
  }
}
 
function prefixSpaces(content, spaces, enabled) {
  if (!content || !enabled) return content
  return spaces + content.replace(/\r?\n/g, '$&' + spaces)
}
 
function checkJsonString(raw) {
  var json
  var str = raw.trim()
  try {
    json = str[0] === '{' ? JSON.parse(str) : {key: str}
  } catch (e) {
    throw new Error('Inject config<' + raw + '> is not a valid json string')
  }
 
  if (!json.hasOwnProperty('key')) throw new Error('Inject config<' + raw + '> should contains "key" field')
 
  return json
}
 
function buildRegExp(tags, startKeyword, endKeyword) {
  return new RegExp(
    '(\\S*' + escapeRegExp(tags[0] + startKeyword) + '\\s*(.*)\\s*' + escapeRegExp(tags[1]) + '\\S*)'
    + '([\\s\\S]*?)'
    // 保持前导的空格或 tab 一样多
    + '(([   ]*)\\S*' + escapeRegExp(tags[2] + endKeyword + tags[3]) + '\\S*)',
    'g'
  )
}