rollup-plugin-uniapp-cementing.js 6.4 KB
Newer Older
DCloud_JSON's avatar
DCloud_JSON 已提交
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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
const PLUGIN_NAME = 'rollup-plugin-uniapp-cementing';
const CEMENTING_MODULE = '_uni_cementing_component.vue'

import fs from 'fs';
import path from 'path';
const SEP = '****************************************************************'
const DEBUG_FILE = 'debug.log'

let logReset = function() {
  fs.writeFileSync(
    path.join(__dirname, DEBUG_FILE),
    `${SEP}\n*  ${new Date().toLocaleString()}\n${SEP}\n`
  )
}

let logTitle = function(title) {
  let msg = `${SEP}\n*  ${title}\n${SEP}`
  log(msg)
}

let log = function(...args) {
  let msgs = []
  for (let msg of args) {
    if (typeof msg === 'string') {
      msgs.push(msg)
    } else {
      msgs.push(JSON.stringify(msg, null, 2))
    }
  }
  fs.writeFileSync(
    path.join(__dirname, DEBUG_FILE),
    `${msgs.join(' ')}\n`, {
      flag: 'a'
    }
  )
}

import { createFilter } from '@rollup/pluginutils';
import MagicString from 'magic-string';
import { walk } from 'estree-walker';

function walkTo(node, matchers = []) {
  let cur = node
  let matcher = matchers.shift()
  while (matcher) {
    let found
    walk(cur, {
      enter: function(node, parent, prop, index) {
        if (found) return
        if (typeof matcher == 'function' && matcher(node) || matcher === node.type) {
          found = node
        }
      }
    })
    if (!found) return
    cur = found
    if (matchers.length == 0) return cur
    matcher = matchers.shift()
  }
}

function cementingPlugin(options = {}) {
  const {
    platforms = ['app', /^mp(\-.*)?$/],
    include = ['*.vue', '*.nvue'],
    exclude,
    resolve = __dirname,
    debug,
    components = {}
  } = options

  let enabled = false
  for (let platform of platforms) {
    if (platform instanceof RegExp) {
      if (platform.exec(process.env['UNI_PLATFORM'])) {
        enabled = true
      }
    } else if (platform === process.env['UNI_PLATFORM']) {
      enabled = true
    }
  }
  if (!enabled) return false
  if (!debug) {
    logReset = logTitle = log = () => {}
  }

  let filter = createFilter(include, exclude, { resolve })

  return {
    name: PLUGIN_NAME,
    enforce: 'pre',

    buildStart(options) {
      logReset()
      // log(options)
    },

    // 对使用了动态组件 <component> 的组件进行静态化处理。
    transform(code, id) {
      if (!filter(id)) return
      logTitle('transform: ' + id)

      // 检查组件代码是否使用了动态组件
      let re = /(<component)(.*?)(\/>|<\/component>)/sg
      let m = re.exec(code)
      if (!m) return
      // log('使用了动态组件的代码:', code)

      // 把 <template> 部分里面的 <component :is="xxx" ...> 替换成 <CementingXxx v-if="$isCementing(xxx, 'xxx')" ...>
      let usedCmps = {}
      code = code.replace(re, (match, p1, p2, p3) => {
        let cementingName = ''
        p2 = p2.replace(/cementing="(.*?)"/, (_, name) => {
          cementingName = name
          return ''
        })
        if (!cementingName) {
          log('<component> 组件缺少 cementing 属性,无法静态化:', id)
          return ''
        }
        let cmps = components[cementingName]
        if (!cmps) {
          log('cementing 属性无效:', id)
          return ''
        }

        let replace = Object.keys(cmps).map(cmpName => {
          usedCmps[`${cementingName}_${cmpName}`] = cmps[cmpName]
          let r1 = p1.replace('component', `${cementingName}_${cmpName}`)
          let r2 = p2.replace(/:is="(.*?)"/, (_, cond) => {
            return `v-if="$isCementing(${cond},'${cmpName}')"`
          })
          let r3 = p3.replace('component', `${cementingName}_${cmpName}`)
          return r1 + r2 + r3
        }).join('\n')

        // log('<<<<----')
        // log(match)
        // log('--------')
        // log(replace)
        // log('---->>>>')
        return replace
      })

      // 在 <script> 部分添加相关代码
      code = code.replace(/(<script.*?>)(.*?)(<\/script>)/sg, (match, p1, p2, p3) => {
        let magicString = new MagicString(p2)

        // 在代码顶部添加 import
        let imports = []
        for (let name of Object.keys(usedCmps)) {
          let source = usedCmps[name]
          imports.push(`import ${name} from '${source}';`)
        }
        magicString.appendLeft(0, `\n${imports.join('\n')}\n`)

        // 通过 AST 找到组件的 components 属性并在其中添加注册
        let ast = this.parse(p2)
        let exportDefaultObj = walkTo(ast, [
          'ExportDefaultDeclaration',
          'ObjectExpression',
        ])
        if (!exportDefaultObj) return match
        let componentsObj = walkTo(exportDefaultObj, [
          (node) => node.type === 'Property' && node.key.name === 'components',
          'ObjectExpression',
        ])

        let usedNames = Object.keys(usedCmps)
        if (componentsObj) {
          // 如果组件已经定义了 components 属性,则在其中添加
          magicString.appendRight(componentsObj.start + 1, `\n${usedNames.join(',\n')},\n`)
        } else {
          // 如果组件没有定义 components 属性,则添加新定义
          magicString.appendRight(exportDefaultObj.start + 1, `components:{\n${usedNames.join(',\n')}\n},`)
        }

        // 通过 AST 找到组件的 methods 属性并在其中添加 $isCementing() 方法
        let methodsObj = walkTo(exportDefaultObj, [
          (node) => node.type === 'Property' && node.key.name === 'methods',
          'ObjectExpression',
        ])

        let fn =
`$isCementing(is, name) {
  if (is.name) is = is.name
  return name == is?.replace?.(/-(.?)/g, (match, c) => c.toUpperCase()).replace('-', '')
}`
        if (methodsObj) {
          // 如果组件已经定义了 methods 属性,则在其中添加
          magicString.appendRight(methodsObj.start + 1, `\n${fn},\n`)
        } else {
          // 如果组件没有定义 methods 属性,则添加新定义
          magicString.appendRight(exportDefaultObj.start + 1, `methods:{\n${fn}\n},`)
        }
        p2 = magicString.toString()
        // let map = magicString.generateMap({ hires: true })

        let replace = p1 + p2 + p3
        // log('<<<<----')
        // log(match)
        // log('--------')
        // log(replace)
        // log('---->>>>')
        return replace
      })
      log('静态化之后的代码:', code)

      return { code }
    },

    // writeBundle(options, bundle) {
    //   logTitle('writeBundle:')
    //   log('bundle:', bundle)
    // },
  }
}

export default cementingPlugin