diff --git a/.eslintrc.js b/.eslintrc.js
index 9b3f6a0218..71ffd9f02e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -23,6 +23,7 @@ module.exports = {
App: 'readonly',
__mpx_mode__: 'readonly',
__mpx_env__: 'readonly',
+ __mpx_dynamic_runtime__: 'readonly',
getRegExp: 'readonly',
getCurrentPages: 'readonly'
},
@@ -33,6 +34,6 @@ module.exports = {
},
env: {
'jest/globals': true,
- 'browser': true
+ browser: true
}
}
diff --git a/docs-vuepress/articles/mpx2.md b/docs-vuepress/articles/mpx2.md
index c533b31f11..9db576d5e1 100644
--- a/docs-vuepress/articles/mpx2.md
+++ b/docs-vuepress/articles/mpx2.md
@@ -776,7 +776,7 @@ class MPXProxy {
this.miniRenderData = {}
for (let key in renderData) { // 遍历数据访问路径
if (renderData.hasOwnProperty(key)) {
- let item = renderData[key]
+ let item = renderData[key]
let data = item[0]
let firstKey = item[1] // 某个字段 path 的第一个 key 值
if (this.localKeys.indexOf(firstKey) > -1) {
@@ -817,7 +817,7 @@ class MPXProxy {
* @param {Object} renderData
* @return {Object} processedRenderData
*/
-export function preprocessRenderData (renderData) {
+export function preprocessRenderData (renderData) {
// method for get key path array
const processKeyPathMap = (keyPathMap) => {
let keyPath = Object.keys(keyPathMap)
diff --git a/packages/core/src/core/proxy.js b/packages/core/src/core/proxy.js
index 76cc346c2f..7f6a407a32 100644
--- a/packages/core/src/core/proxy.js
+++ b/packages/core/src/core/proxy.js
@@ -44,6 +44,8 @@ import {
ONHIDE,
ONRESIZE
} from './innerLifecycle'
+import contextMap from '../dynamic/vnode/context'
+import { getAst } from '../dynamic/astCache'
let uid = 0
@@ -131,6 +133,8 @@ export default class MpxProxy {
}
created () {
+ // 缓存上下文,在 destoryed 阶段删除
+ contextMap.set(this.uid, this.target)
if (__mpx_mode__ !== 'web') {
// web中BEFORECREATE钩子通过vue的beforeCreate钩子单独驱动
this.callHook(BEFORECREATE)
@@ -190,6 +194,8 @@ export default class MpxProxy {
}
unmounted () {
+ // 页面/组件销毁清除上下文的缓存
+ contextMap.remove(this.uid)
this.callHook(BEFOREUNMOUNT)
this.scope?.stop()
if (this.update) this.update.active = false
@@ -379,7 +385,10 @@ export default class MpxProxy {
this.doRender(this.processRenderDataWithStrictDiff(renderData))
}
- renderWithData (skipPre) {
+ renderWithData (skipPre, vnode) {
+ if (vnode) {
+ return this.doRenderWithVNode(vnode)
+ }
const renderData = skipPre ? this.renderData : preProcessRenderData(this.renderData)
this.doRender(this.processRenderDataWithStrictDiff(renderData))
// 重置renderData准备下次收集
@@ -478,6 +487,25 @@ export default class MpxProxy {
return result
}
+ doRenderWithVNode (vnode) {
+ if (!this._vnode) {
+ this.target.__render({ r: vnode })
+ } else {
+ let diffPath = diffAndCloneA(vnode, this._vnode).diffData
+ if (!isEmptyObject(diffPath)) {
+ // 构造 diffPath 数据
+ diffPath = Object.keys(diffPath).reduce((preVal, curVal) => {
+ const key = 'r' + curVal
+ preVal[key] = diffPath[curVal]
+ return preVal
+ }, {})
+ this.target.__render(diffPath)
+ }
+ }
+ // 缓存本地的 vnode 用以下一次 diff
+ this._vnode = diffAndCloneA(vnode).clone
+ }
+
doRender (data, cb) {
if (typeof this.target.__render !== 'function') {
error('Please specify a [__render] function to render view.', this.options.mpxFileResource)
@@ -545,12 +573,30 @@ export default class MpxProxy {
const _c = this.target._c.bind(this.target)
const _r = this.target._r.bind(this.target)
const _sc = this.target._sc.bind(this.target)
+ const _g = this.target._g?.bind(this.target)
+ const __getAst = this.target.__getAst?.bind(this.target)
+ const moduleId = this.target.__moduleId
+ const dynamicTarget = this.target.__dynamic
+
const effect = this.effect = new ReactiveEffect(() => {
// pre render for props update
if (this.propsUpdatedFlag) {
this.updatePreRender()
}
-
+ if (dynamicTarget || __getAst) {
+ try {
+ const ast = getAst(__getAst, moduleId)
+ return _r(false, _g(ast, moduleId))
+ } catch (e) {
+ e.errType = 'mpx-dynamic-render'
+ e.errmsg = e.message
+ if (!__mpx_dynamic_runtime__) {
+ return error('Please make sure you have set dynamicRuntime true in mpx webpack plugin config because you have use the dynamic runtime feature.', this.options.mpxFileResource, e)
+ } else {
+ return error('Dynamic rendering error', this.options.mpxFileResource, e)
+ }
+ }
+ }
if (this.target.__injectedRender) {
try {
return this.target.__injectedRender(_i, _c, _r, _sc)
diff --git a/packages/core/src/dynamic/astCache.js b/packages/core/src/dynamic/astCache.js
new file mode 100644
index 0000000000..9f4fb32cd3
--- /dev/null
+++ b/packages/core/src/dynamic/astCache.js
@@ -0,0 +1,25 @@
+import { isFunction, isObject, error } from '@mpxjs/utils'
+
+class DynamicAstCache {
+ #astCache = {}
+
+ getAst (id) {
+ return this.#astCache[id]
+ }
+
+ setAst (id, ast) {
+ this.#astCache[id] = ast
+ }
+}
+
+export const dynamic = new DynamicAstCache()
+
+export const getAst = (__getAst, moduleId) => {
+ if ((__getAst && isFunction(__getAst))) {
+ const ast = __getAst()
+ if (!isObject(ast)) return error('__getAst returned data is not of type object')
+ return Object.values(ast)[0]
+ } else {
+ return dynamic.getAst(moduleId)
+ }
+}
diff --git a/packages/core/src/dynamic/dynamicRenderMixin.js b/packages/core/src/dynamic/dynamicRenderMixin.js
new file mode 100644
index 0000000000..13eeba645d
--- /dev/null
+++ b/packages/core/src/dynamic/dynamicRenderMixin.js
@@ -0,0 +1,77 @@
+import { hasOwn, isObject, error } from '@mpxjs/utils'
+import genVnodeTree from './vnode/render'
+import contextMap from './vnode/context'
+import { CREATED } from '../core/innerLifecycle'
+
+function dynamicRefsMixin () {
+ return {
+ [CREATED] () {
+ // 处理ref场景,如果是在容器组件的上下文渲染
+ if (this.mpxCustomElement) {
+ this._getRuntimeRefs()
+ }
+ },
+ methods: {
+ _getRuntimeRefs () {
+ const vnodeContext = contextMap.get(this.uid)
+ if (vnodeContext) {
+ const refsArr = vnodeContext.__getRefsData && vnodeContext.__getRefsData()
+ if (Array.isArray(refsArr)) {
+ refsArr.forEach((ref) => {
+ const all = ref.all
+ if (!vnodeContext.$refs[ref.key] || (all && !vnodeContext.$refs[ref.key].length)) {
+ const refNode = this.__getRefNode(ref)
+ if ((all && refNode.length) || refNode) {
+ Object.defineProperty(vnodeContext.$refs, ref.key, {
+ enumerable: true,
+ configurable: true,
+ get: () => {
+ return refNode
+ }
+ })
+ }
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+}
+
+function dynamicSlotMixin () {
+ if (__mpx_mode__ === 'ali') {
+ return {
+ props: { slots: {} }
+ }
+ } else {
+ return {
+ properties: { slots: { type: Object } }
+ }
+ }
+}
+
+function dynamicRenderHelperMixin () {
+ return {
+ methods: {
+ _g (astData, moduleId) {
+ const location = this.__mpxProxy && this.__mpxProxy.options.mpxFileResource
+ if (astData && isObject(astData) && hasOwn(astData, 'template')) {
+ const vnodeTree = genVnodeTree(astData, [this], { moduleId, location })
+ return vnodeTree
+ } else {
+ error('Dynamic component get the wrong json ast data, please check.', location, {
+ errType: 'mpx-dynamic-render',
+ errmsg: 'invalid json ast data'
+ })
+ }
+ }
+ }
+ }
+}
+
+export {
+ dynamicRefsMixin,
+ dynamicSlotMixin,
+ dynamicRenderHelperMixin
+}
diff --git a/packages/core/src/dynamic/vnode/context.js b/packages/core/src/dynamic/vnode/context.js
new file mode 100644
index 0000000000..0600c3e41e
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/context.js
@@ -0,0 +1,17 @@
+const cache = {}
+
+const contextMap = {
+ set (id, context) {
+ cache[id] = context
+ },
+ get (id) {
+ return cache[id]
+ },
+ remove (id) {
+ if (cache[id]) {
+ delete cache[id]
+ }
+ }
+}
+
+export default contextMap
diff --git a/packages/core/src/dynamic/vnode/css-select/cssauron.js b/packages/core/src/dynamic/vnode/css-select/cssauron.js
new file mode 100644
index 0000000000..65a3aaa12f
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/css-select/cssauron.js
@@ -0,0 +1,445 @@
+import tokenizer from './tokenizer'
+
+export default function language (lookups, matchComparison) {
+ return function (selector, moduleId) {
+ return parse(
+ selector,
+ remap(lookups),
+ moduleId,
+ matchComparison || caseSensitiveComparison
+ )
+ }
+}
+
+function remap (opts) {
+ // 对于字符串类型的 value 转化为函数
+ for (const key in opts) {
+ if (opt_okay(opts, key)) {
+ /* eslint-disable-next-line */
+ opts[key] = Function(
+ 'return function(node, attr) { return node.' + opts[key] + ' }'
+ )
+ opts[key] = opts[key]()
+ }
+ }
+
+ return opts
+}
+
+function opt_okay (opts, key) {
+ return Object.prototype.hasOwnProperty.call(opts, key) && typeof opts[key] === 'string'
+}
+
+function parse (selector, options, moduleId, matchComparison) {
+ const stream = tokenizer()
+ // const default_subj = true
+ const selectors = [[]]
+ let bits = selectors[0]
+
+ // 逆向关系
+ const traversal = {
+ '': any_parents,
+ '>': direct_parent,
+ '+': direct_sibling,
+ '~': any_sibling
+ }
+
+ stream.on('data', group)
+ stream.end(selector)
+
+ function group (token) {
+ if (token.type === 'comma') {
+ selectors.unshift((bits = []))
+
+ return
+ }
+
+ // 获取节点之间的关系路径,匹配的规则从右往左依次进行,因此在后面的匹配规则需要存储在栈结构的前面
+ if (token.type === 'op' || token.type === 'any-child') {
+ bits.unshift(traversal[token.data]) // 获取节点之间关系的操作数
+ bits.unshift(check()) // 添加空的匹配操作数
+
+ return
+ }
+
+ bits[0] = bits[0] || check()
+ const crnt = bits[0]
+
+ if (token.type === '!') {
+ crnt.subject = selectors[0].subject = true
+
+ return
+ }
+
+ crnt.push(
+ token.type === 'class'
+ ? listContains(token.type, token.data)
+ : token.type === 'attr'
+ ? attr(token)
+ : token.type === ':' || token.type === '::'
+ ? pseudo(token)
+ : token.type === '*'
+ ? Boolean
+ : matches(token.type, token.data, matchComparison)
+ )
+ }
+
+ return selector_fn
+
+ // 单节点对比
+ function selector_fn (node, as_boolean) {
+ if (node.data?.moduleId !== moduleId) {
+ return
+ }
+ let current, length, subj
+
+ const orig = node
+ const set = []
+
+ for (let i = 0, len = selectors.length; i < len; ++i) {
+ bits = selectors[i]
+ current = entry // 当前 bits 检测规则
+ length = bits.length
+ node = orig // 引用赋值
+ subj = []
+
+ let j = 0
+ // 步长为2,因为2个节点之间的关系中间会有一个 OP 操作符
+ for (j = 0; j < length; j += 2) {
+ node = current(node, bits[j], subj)
+
+ if (!node) {
+ break
+ }
+
+ // todo 这里的规则和步长设计的很巧妙
+ current = bits[j + 1] // 改变当前的 bits 检测规则
+ }
+
+ if (j >= length) {
+ if (as_boolean) {
+ return true
+ }
+
+ add(!bits.subject ? [orig] : subj)
+ }
+ }
+
+ if (as_boolean) {
+ return false
+ }
+
+ return !set.length ? false : set.length === 1 ? set[0] : set
+
+ function add (items) {
+ let next
+
+ while (items.length) {
+ next = items.shift()
+
+ if (set.indexOf(next) === -1) {
+ set.push(next)
+ }
+ }
+ }
+ }
+
+ // 匹配操作数
+ function check () {
+ _check.bits = []
+ _check.subject = false
+ _check.push = function (token) {
+ _check.bits.push(token)
+ }
+
+ return _check
+
+ function _check (node, subj) {
+ for (let i = 0, len = _check.bits.length; i < len; ++i) {
+ if (!_check.bits[i](node)) {
+ return false
+ }
+ }
+
+ if (_check.subject) {
+ subj.push(node)
+ }
+
+ return true
+ }
+ }
+
+ function listContains (type, data) {
+ return function (node) {
+ let val = options[type](node)
+ val = Array.isArray(val) ? val : val ? val.toString().split(/\s+/) : []
+ return val.indexOf(data) >= 0
+ }
+ }
+
+ function attr (token) {
+ return token.data.lhs
+ ? valid_attr(options.attr, token.data.lhs, token.data.cmp, token.data.rhs)
+ : valid_attr(options.attr, token.data)
+ }
+
+ function matches (type, data, matchComparison) {
+ return function (node) {
+ return matchComparison(type, options[type](node), data)
+ }
+ }
+
+ function any_parents (node, next, subj) {
+ do {
+ node = options.parent(node)
+ } while (node && !next(node, subj))
+
+ return node
+ }
+
+ function direct_parent (node, next, subj) {
+ node = options.parent(node)
+
+ return node && next(node, subj) ? node : null
+ }
+
+ function direct_sibling (node, next, subj) {
+ const parent = options.parent(node)
+ let idx = 0
+
+ const children = options.children(parent)
+
+ for (let i = 0, len = children.length; i < len; ++i) {
+ if (children[i] === node) {
+ idx = i
+
+ break
+ }
+ }
+
+ return children[idx - 1] && next(children[idx - 1], subj)
+ ? children[idx - 1]
+ : null
+ }
+
+ function any_sibling (node, next, subj) {
+ const parent = options.parent(node)
+
+ const children = options.children(parent)
+
+ for (let i = 0, len = children.length; i < len; ++i) {
+ if (children[i] === node) {
+ return null
+ }
+
+ if (next(children[i], subj)) {
+ return children[i]
+ }
+ }
+
+ return null
+ }
+
+ function pseudo (token) {
+ return valid_pseudo(options, token.data, matchComparison)
+ }
+}
+
+function entry (node, next, subj) {
+ return next(node, subj) ? node : null
+}
+
+function valid_pseudo (options, match, matchComparison) {
+ switch (match) {
+ case 'empty':
+ return valid_empty(options)
+ case 'first-child':
+ return valid_first_child(options)
+ case 'last-child':
+ return valid_last_child(options)
+ case 'root':
+ return valid_root(options)
+ }
+
+ if (match.indexOf('contains') === 0) {
+ return valid_contains(options, match.slice(9, -1))
+ }
+
+ if (match.indexOf('any') === 0) {
+ return valid_any_match(options, match.slice(4, -1), matchComparison)
+ }
+
+ if (match.indexOf('not') === 0) {
+ return valid_not_match(options, match.slice(4, -1), matchComparison)
+ }
+
+ if (match.indexOf('nth-child') === 0) {
+ return valid_nth_child(options, match.slice(10, -1))
+ }
+
+ return function () {
+ return false
+ }
+}
+
+function valid_not_match (options, selector, matchComparison) {
+ const fn = parse(selector, options, matchComparison)
+
+ return not_function
+
+ function not_function (node) {
+ return !fn(node, true)
+ }
+}
+
+function valid_any_match (options, selector, matchComparison) {
+ const fn = parse(selector, options, matchComparison)
+
+ return fn
+}
+
+function valid_attr (fn, lhs, cmp, rhs) {
+ return function (node) {
+ const attr = fn(node, lhs)
+
+ if (!cmp) {
+ return !!attr
+ }
+
+ if (cmp.length === 1) {
+ return attr === rhs
+ }
+
+ if (attr === undefined || attr === null) {
+ return false
+ }
+
+ return checkattr[cmp.charAt(0)](attr, rhs)
+ }
+}
+
+function valid_first_child (options) {
+ return function (node) {
+ return options.children(options.parent(node))[0] === node
+ }
+}
+
+function valid_last_child (options) {
+ return function (node) {
+ const children = options.children(options.parent(node))
+
+ return children[children.length - 1] === node
+ }
+}
+
+function valid_empty (options) {
+ return function (node) {
+ return options.children(node).length === 0
+ }
+}
+
+function valid_root (options) {
+ return function (node) {
+ return !options.parent(node)
+ }
+}
+
+function valid_contains (options, contents) {
+ return function (node) {
+ return options.contents(node).indexOf(contents) !== -1
+ }
+}
+
+function valid_nth_child (options, nth) {
+ let test = function () {
+ return false
+ }
+ if (nth === 'odd') {
+ nth = '2n+1'
+ } else if (nth === 'even') {
+ nth = '2n'
+ }
+ const regexp = /( ?([-|+])?(\d*)n)? ?((\+|-)? ?(\d+))? ?/
+ const matches = nth.match(regexp)
+ if (matches) {
+ let growth = 0
+ if (matches[1]) {
+ const positiveGrowth = matches[2] !== '-'
+ growth = parseInt(matches[3] === '' ? 1 : matches[3])
+ growth = growth * (positiveGrowth ? 1 : -1)
+ }
+ let offset = 0
+ if (matches[4]) {
+ offset = parseInt(matches[6])
+ const positiveOffset = matches[5] !== '-'
+ offset = offset * (positiveOffset ? 1 : -1)
+ }
+ if (growth === 0) {
+ if (offset !== 0) {
+ test = function (children, node) {
+ return children[offset - 1] === node
+ }
+ }
+ } else {
+ test = function (children, node) {
+ const validPositions = []
+ const len = children.length
+ for (let position = 1; position <= len; position++) {
+ const divisible = (position - offset) % growth === 0
+ if (divisible) {
+ if (growth > 0) {
+ validPositions.push(position)
+ } else {
+ if ((position - offset) / growth >= 0) {
+ validPositions.push(position)
+ }
+ }
+ }
+ }
+ for (let i = 0; i < validPositions.length; i++) {
+ if (children[validPositions[i] - 1] === node) {
+ return true
+ }
+ }
+ return false
+ }
+ }
+ }
+ return function (node) {
+ const children = options.children(options.parent(node))
+
+ return test(children, node)
+ }
+}
+
+const checkattr = {
+ $: check_end,
+ '^': check_beg,
+ '*': check_any,
+ '~': check_spc,
+ '|': check_dsh
+}
+
+function check_end (l, r) {
+ return l.slice(l.length - r.length) === r
+}
+
+function check_beg (l, r) {
+ return l.slice(0, r.length) === r
+}
+
+function check_any (l, r) {
+ return l.indexOf(r) > -1
+}
+
+function check_spc (l, r) {
+ return l.split(/\s+/).indexOf(r) > -1
+}
+
+function check_dsh (l, r) {
+ return l.split('-').indexOf(r) > -1
+}
+
+function caseSensitiveComparison (type, pattern, data) {
+ return pattern === data
+}
diff --git a/packages/core/src/dynamic/vnode/css-select/index.js b/packages/core/src/dynamic/vnode/css-select/index.js
new file mode 100644
index 0000000000..1429f43602
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/css-select/index.js
@@ -0,0 +1,116 @@
+import cssauron from './cssauron'
+
+const language = cssauron({
+ tag: function (node) {
+ return node.nodeType
+ },
+ class: function (node) {
+ return node.data?.class
+ },
+ id: function (node) {
+ return node.data?.id
+ },
+ children: function (node) {
+ return node.children
+ },
+ parent: function (node) {
+ return node.parent
+ },
+ contents: function (node) {
+ return node.contents || ''
+ },
+ attr: function (node, attr) {
+ if (node.properties) {
+ const attrs = node.properties.attributes
+ if (attrs && attrs[attr]) {
+ return attrs[attr]
+ }
+ return node.properties[attr]
+ }
+ }
+})
+
+export default function cssSelect (sel, options) {
+ options = options || {}
+ const selector = language(sel, options.moduleId)
+ function match (vtree) {
+ const node = mapTree(vtree, null, options) || {}
+ const matched = []
+
+ // Traverse each node in the tree and see if it matches our selector
+ traverse(node, function (node) {
+ let result = selector(node)
+ if (result) {
+ if (!Array.isArray(result)) {
+ result = [result]
+ }
+ matched.push.apply(matched, result)
+ }
+ })
+
+ const results = mapResult(matched)
+ if (results.length === 0) {
+ return null
+ }
+ return results
+ }
+ match.matches = function (vtree) {
+ const node = mapTree(vtree, null, options)
+ return !!selector(node)
+ }
+ return match
+}
+
+function traverse (vtree, fn) {
+ fn(vtree)
+ if (vtree.children) {
+ vtree.children.forEach(function (vtree) {
+ traverse(vtree, fn)
+ })
+ }
+}
+
+function mapResult (result) {
+ return result
+ .filter(function (node) {
+ return !!node.vtree
+ })
+ .map(function (node) {
+ return node.vtree
+ })
+}
+
+function getNormalizeCaseFn (caseSensitive) {
+ return caseSensitive
+ ? function noop (str) {
+ return str
+ }
+ : function toLowerCase (str) {
+ return str.toLowerCase()
+ }
+}
+
+// Map a virtual-dom node tree into a data structure that cssauron can use to
+// traverse.
+function mapTree (vtree, parent, options) {
+ const normalizeTagCase = getNormalizeCaseFn(options.caseSensitiveTag)
+
+ if (vtree.nt != null) {
+ const node = {}
+ node.parent = parent
+ node.vtree = vtree
+ node.nodeType = normalizeTagCase(vtree.nt)
+ if (vtree.d) {
+ node.data = vtree.d
+ }
+
+ if (vtree.c) {
+ node.children = vtree.c
+ .map(function (child) {
+ return mapTree(child, node, options)
+ })
+ .filter(Boolean)
+ }
+ return node
+ }
+}
diff --git a/packages/core/src/dynamic/vnode/css-select/through.js b/packages/core/src/dynamic/vnode/css-select/through.js
new file mode 100644
index 0000000000..654a349c06
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/css-select/through.js
@@ -0,0 +1,19 @@
+export default function through (onData) {
+ let dataCb = null
+
+ return {
+ on: function (name, callback) {
+ if (name === 'data') {
+ dataCb = callback
+ }
+ },
+ end: function (data) {
+ onData(data)
+ },
+ queue: function (data) {
+ if (dataCb) {
+ dataCb(data)
+ }
+ }
+ }
+}
diff --git a/packages/core/src/dynamic/vnode/css-select/tokenizer.js b/packages/core/src/dynamic/vnode/css-select/tokenizer.js
new file mode 100644
index 0000000000..ba0094c53b
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/css-select/tokenizer.js
@@ -0,0 +1,371 @@
+import through from './through'
+
+const PSEUDOSTART = 'pseudo-start'
+const ATTR_START = 'attr-start'
+const ANY_CHILD = 'any-child'
+const ATTR_COMP = 'attr-comp'
+const ATTR_END = 'attr-end'
+const PSEUDOPSEUDO = '::'
+const PSEUDOCLASS = ':'
+const READY = '(ready)' // 重置标志位
+const OPERATION = 'op'
+const CLASS = 'class'
+const COMMA = 'comma'
+const ATTR = 'attr'
+const SUBJECT = '!'
+const TAG = 'tag'
+const STAR = '*'
+const ID = 'id'
+
+export default function tokenize () {
+ let escaped = false
+ let gathered = []
+ let state = READY
+ let data = []
+ let idx = 0
+ let stream
+ let length
+ let quote
+ let depth
+ let lhs
+ let rhs
+ let cmp
+ let c
+
+ return (stream = through(ondata, onend))
+
+ function ondata (chunk) {
+ data = data.concat(chunk.split(''))
+ length = data.length
+
+ while (idx < length && (c = data[idx++])) {
+ switch (state) {
+ case READY:
+ state_ready()
+ break
+ case ANY_CHILD:
+ state_any_child()
+ break
+ case OPERATION:
+ state_op()
+ break
+ case ATTR_START:
+ state_attr_start()
+ break
+ case ATTR_COMP:
+ state_attr_compare()
+ break
+ case ATTR_END:
+ state_attr_end()
+ break
+ case PSEUDOCLASS:
+ case PSEUDOPSEUDO:
+ state_pseudo()
+ break
+ case PSEUDOSTART:
+ state_pseudostart()
+ break
+ case ID:
+ case TAG:
+ case CLASS:
+ state_gather()
+ break
+ }
+ }
+
+ data = data.slice(idx)
+
+ if (gathered.length) {
+ stream.queue(token())
+ }
+ }
+
+ function onend (chunk) {
+ // if (arguments.length) {
+ // ondata(chunk)
+ // }
+
+ // if (gathered.length) {
+ // stream.queue(token())
+ // }
+ }
+
+ function state_ready () {
+ switch (true) {
+ case c === '#':
+ state = ID
+ break
+ case c === '.':
+ state = CLASS
+ break
+ case c === ':':
+ state = PSEUDOCLASS
+ break
+ case c === '[':
+ state = ATTR_START
+ break
+ case c === '!':
+ subject()
+ break
+ case c === '*':
+ star()
+ break
+ case c === ',':
+ comma()
+ break
+ case /[>+~]/.test(c):
+ state = OPERATION
+ break
+ case /\s/.test(c):
+ state = ANY_CHILD
+ break
+ case /[\w\d\-_]/.test(c):
+ state = TAG
+ --idx
+ break
+ }
+ }
+
+ function subject () {
+ state = SUBJECT
+ gathered = ['!']
+ stream.queue(token())
+ state = READY
+ }
+
+ function star () {
+ state = STAR
+ gathered = ['*']
+ stream.queue(token())
+ state = READY
+ }
+
+ function comma () {
+ state = COMMA
+ gathered = [',']
+ stream.queue(token())
+ state = READY
+ }
+
+ function state_op () {
+ if (/[>+~]/.test(c)) {
+ return gathered.push(c)
+ }
+
+ // chomp down the following whitespace.
+ if (/\s/.test(c)) {
+ return
+ }
+
+ stream.queue(token())
+ state = READY
+ --idx // 指针左移,归档,开始匹配下一个 token
+ }
+
+ function state_any_child () {
+ if (/\s/.test(c)) {
+ return
+ }
+
+ if (/[>+~]/.test(c)) {
+ --idx
+ state = OPERATION
+ return state
+ // return --idx, (state = OPERATION)
+ }
+
+ // 生成 any_child 节点,并重置状态
+ stream.queue(token())
+ state = READY
+ --idx
+ }
+
+ function state_pseudo () {
+ rhs = state
+ state_gather(true)
+
+ if (state !== READY) {
+ return
+ }
+
+ if (c === '(') {
+ lhs = gathered.join('')
+ state = PSEUDOSTART
+ gathered.length = 0
+ depth = 1
+ ++idx
+
+ return
+ }
+
+ state = PSEUDOCLASS
+ stream.queue(token())
+ state = READY
+ }
+
+ function state_pseudostart () {
+ if (gathered.length === 0 && !quote) {
+ quote = /['"]/.test(c) ? c : null
+
+ if (quote) {
+ return
+ }
+ }
+
+ if (quote) {
+ if (!escaped && c === quote) {
+ quote = null
+
+ return
+ }
+
+ if (c === '\\') {
+ escaped ? gathered.push(c) : (escaped = true)
+
+ return
+ }
+
+ escaped = false
+ gathered.push(c)
+
+ return
+ }
+
+ gathered.push(c)
+
+ if (c === '(') {
+ ++depth
+ } else if (c === ')') {
+ --depth
+ }
+
+ if (!depth) {
+ gathered.pop()
+ stream.queue({
+ type: rhs,
+ data: lhs + '(' + gathered.join('') + ')'
+ })
+
+ state = READY
+ lhs = rhs = cmp = null
+ gathered.length = 0
+ }
+ }
+
+ function state_attr_start () {
+ // 在收集字符的阶段,还会有 state 标志位的判断,因此会影响到下面的逻辑执行
+ state_gather(true)
+
+ if (state !== READY) {
+ return
+ }
+
+ if (c === ']') {
+ state = ATTR
+ stream.queue(token())
+ state = READY
+
+ return
+ }
+
+ lhs = gathered.join('')
+ gathered.length = 0
+ state = ATTR_COMP
+ }
+
+ // 属性选择器:https://www.w3school.com.cn/css/css_attribute_selectors.asp
+ function state_attr_compare () {
+ if (/[=~|$^*]/.test(c)) {
+ gathered.push(c)
+ }
+
+ // 操作符&=
+ if (gathered.length === 2 || c === '=') {
+ cmp = gathered.join('')
+ gathered.length = 0
+ state = ATTR_END
+ quote = null
+ }
+ }
+
+ function state_attr_end () {
+ if (!gathered.length && !quote) {
+ quote = /['"]/.test(c) ? c : null
+
+ if (quote) {
+ return
+ }
+ }
+
+ if (quote) {
+ if (!escaped && c === quote) {
+ quote = null
+
+ return
+ }
+
+ if (c === '\\') {
+ if (escaped) {
+ gathered.push(c)
+ }
+
+ escaped = !escaped
+
+ return
+ }
+
+ escaped = false
+ gathered.push(c)
+
+ return
+ }
+
+ state_gather(true)
+
+ if (state !== READY) {
+ return
+ }
+
+ stream.queue({
+ type: ATTR,
+ data: {
+ lhs: lhs,
+ rhs: gathered.join(''),
+ cmp: cmp
+ }
+ })
+
+ state = READY
+ lhs = rhs = cmp = null
+ gathered.length = 0
+ }
+
+ function state_gather (quietly) {
+ // 如果是非单词字符,例如 空格。会更新 state 的状态
+ if (/[^\d\w\-_]/.test(c) && !escaped) {
+ if (c === '\\') {
+ escaped = true
+ } else {
+ !quietly && stream.queue(token())
+ state = READY
+ --idx
+ }
+
+ return
+ }
+
+ escaped = false
+ gathered.push(c)
+ }
+
+ function token () {
+ const data = gathered.join('')
+
+ gathered.length = 0
+
+ return {
+ type: state,
+ data: data
+ }
+ }
+}
diff --git a/packages/core/src/dynamic/vnode/interpreter.js b/packages/core/src/dynamic/vnode/interpreter.js
new file mode 100644
index 0000000000..9775d000ad
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/interpreter.js
@@ -0,0 +1,449 @@
+class Interpreter {
+ constructor (contextScope = []) {
+ this.stateStack = []
+ this.value = undefined
+ contextScope.unshift(this.initGlobalContext())
+ this.contextScope = contextScope
+
+ this.TYPE_ERROR = 'TypeError'
+ this.REFERENCE_ERROR = 'ReferenceError'
+ }
+
+ eval (ast) {
+ const state = new State(ast, {})
+ this.stateStack = [state]
+ // eslint-disable-next-line
+ while (this.step()) { }
+ return this.value
+ }
+
+ step () {
+ const state = this.stateStack[this.stateStack.length - 1]
+ if (!state) {
+ return false
+ }
+ const node = state.node
+ const type = node[0]
+ // Program
+ if (type === 1 && state.done) {
+ return false
+ }
+
+ let nextState
+ try {
+ nextState = this[type](this.stateStack, state, node)
+ } catch (e) {
+ throw Error(e)
+ }
+
+ if (nextState) {
+ this.stateStack.push(nextState)
+ }
+ return true
+ }
+
+ getValue (ref) {
+ if (ref[0] === Interpreter.SCOPE_REFERENCE) {
+ // A null/varname variable lookup
+ return this.getValueFromScope(ref[1])
+ } else {
+ // An obj/prop components tuple(foo.bar)
+ return ref[0][ref[1]]
+ }
+ }
+
+ setValue (ref, value) {
+ if (ref[0] === Interpreter.SCOPE_REFERENCE) {
+ return this.setValueToScope(ref[1], value)
+ } else {
+ ref[0][ref[1]] = value
+ return value
+ }
+ }
+
+ setValueToScope (name, value) {
+ let index = this.contextScope.length
+ while (index--) {
+ const scope = this.contextScope[index]
+ if (name in scope) {
+ scope[name] = value
+ return undefined
+ }
+ }
+ this.throwException(this.REFERENCE_ERROR, name + ' is not defined')
+ }
+
+ getValueFromScope (name) {
+ let index = this.contextScope.length
+ while (index--) {
+ const scope = this.contextScope[index]
+ if (name in scope) {
+ return scope[name]
+ }
+ }
+ this.throwException(this.REFERENCE_ERROR, name + ' is not defined')
+ }
+
+ throwException (errorType, message) {
+ throw new Error('[JsInterpreter]: ' + errorType + ` ${message}.`)
+ }
+
+ initGlobalContext () {
+ const context = {
+ // eslint-disable-next-line
+ 'undefined': undefined
+ }
+ return context
+ }
+}
+
+Interpreter.SCOPE_REFERENCE = { SCOPE_REFERENCE: true }
+Interpreter.STEP_ERROR = { STEP_ERROR: true }
+
+class State {
+ constructor (node, scope) {
+ this.node = node
+ this.scope = scope
+ }
+}
+
+// Program
+Interpreter.prototype[1] = function stepProgram (stack, state, node) {
+ const bodyIndex = 1
+ const expression = node[bodyIndex].shift()
+ if (expression) {
+ state.done = false
+ return new State(expression)
+ }
+ state.done = true
+}
+
+// Identifier
+Interpreter.prototype[2] = function stepIdentifier (stack, state, node) {
+ const identifierIndex = 1
+ stack.pop()
+ // 引用场景: ++a
+ if (state.components) {
+ stack[stack.length - 1].value = [Interpreter.SCOPE_REFERENCE, node[identifierIndex]]
+ return
+ }
+ const value = this.getValueFromScope(node[identifierIndex])
+ stack[stack.length - 1].value = value
+}
+
+// Literal 暂未支持正则字面量,也不需要支持
+Interpreter.prototype[3] = function stepLiteral (stack, state, node) {
+ stack.pop()
+ stack[stack.length - 1].value = node[1]
+}
+
+// ArrayExpression
+Interpreter.prototype[28] = function stepArrayExpression (stack, state, node) {
+ const elementsIndex = 1
+ const elements = node[elementsIndex]
+ let n = state.n_ || 0
+ if (!state.array_) {
+ state.array_ = []
+ } else {
+ state.array_[n] = state.value
+ n++
+ }
+ while (n < elements.length) {
+ if (elements[n]) {
+ state.n_ = n
+ return new State(elements[n])
+ }
+ n++
+ }
+ stack.pop()
+ stack[stack.length - 1].value = state.array_
+}
+
+// ObjectExpression
+Interpreter.prototype[29] = function stepObjectExpression (stack, state, node) {
+ const propertyIndex = 1
+ const kindIndex = 3
+ let n = state.n_ || 0
+ let property = node[propertyIndex][n]
+ if (!state.object_) {
+ // first execution
+ state.object_ = {}
+ state.properties_ = {}
+ } else {
+ const propName = state.destinationName
+ if (!state.properties_[propName]) {
+ state.properties_[propName] = {}
+ }
+ state.properties_[propName][property[kindIndex]] = state.value
+ state.n_ = ++n
+ property = node[propertyIndex][n]
+ }
+
+ if (property) {
+ const keyIndex = 1
+ const valueIndex = 2
+ const key = property[keyIndex]
+ const identifierOrLiteralValueIndex = 1
+ let propName
+ if (key[0] === 2 || key[0] === 3) {
+ propName = key[identifierOrLiteralValueIndex]
+ } else {
+ throw SyntaxError('Unknown object structure: ' + key[0])
+ }
+ state.destinationName = propName
+ return new State(property[valueIndex])
+ }
+
+ for (const key in state.properties_) {
+ state.object_[key] = state.properties_[key].init
+ }
+ stack.pop()
+ stack[stack.length - 1].value = state.object_
+}
+
+// UnaryExpression
+Interpreter.prototype[33] = function stepUnaryExpression (stack, state, node) {
+ const operatorIndex = 1
+ const argumentIndex = 2
+ if (!state.done_) {
+ state.done_ = true
+ const nextState = new State(node[argumentIndex])
+ nextState.components = node[operatorIndex] === 'delete'
+ return nextState
+ }
+ stack.pop()
+ let value = state.value
+ switch (node[operatorIndex]) {
+ case '-':
+ value = -value
+ break
+ case '+':
+ value = +value
+ break
+ case '!':
+ value = !value
+ break
+ case '~':
+ value = ~value
+ break
+ case 'delete': {
+ let result = true
+ if (Array.isArray(value)) {
+ let context = value[0]
+ if (context === Interpreter.SCOPE_REFERENCE) {
+ context = this.contextScope
+ }
+ const name = String(value[1])
+ try {
+ delete context[0][name]
+ } catch (e) {
+ this.throwException(this.TYPE_ERROR, "Cannot delete property '" + name + "' of '" + context[0] + "'")
+ result = false
+ }
+ }
+ value = result
+ break
+ }
+ case 'typeof':
+ value = typeof value
+ break
+ case 'void':
+ value = undefined
+ break
+ default:
+ throw SyntaxError('Unknow unary operator:' + node[operatorIndex])
+ }
+ stack[stack.length - 1].value = value
+}
+
+// UpdateExpression
+Interpreter.prototype[34] = function stepUpdateExpression (stack, state, node) {
+ const argumentIndex = 2
+ if (!state.doneLeft_) {
+ state.doneLeft_ = true
+ const nextState = new State(node[argumentIndex])
+ nextState.components = true
+ return nextState
+ }
+
+ if (!state.leftSide_) {
+ state.leftSide_ = state.value
+ }
+ if (!state.doneGetter_) {
+ const leftValue = this.getValue(state.leftSide_)
+ state.leftValue_ = leftValue
+ }
+
+ const operatorIndex = 1
+ const leftValue = Number(state.leftValue_)
+ let changeValue
+ if (node[operatorIndex] === '++') {
+ changeValue = leftValue + 1
+ } else if (node[operatorIndex] === '--') {
+ changeValue = leftValue - 1
+ } else {
+ throw SyntaxError('Unknown update expression: ' + node[operatorIndex])
+ }
+ const prefixIndex = 3
+ const returnValue = node[prefixIndex] ? changeValue : leftValue
+ this.setValue(state.leftSide_, changeValue)
+
+ stack.pop()
+ stack[stack.length - 1].value = returnValue
+}
+
+// BinaryExpression
+Interpreter.prototype[35] = function stepBinaryExpression (stack, state, node) {
+ if (!state.doneLeft_) {
+ state.doneLeft_ = true
+ const leftNodeIndex = 2
+ return new State(node[leftNodeIndex])
+ }
+ if (!state.doneRight_) {
+ state.doneRight_ = true
+ state.leftValue_ = state.value
+ const rightNodeIndex = 3
+ return new State(node[rightNodeIndex])
+ }
+ stack.pop()
+ const leftValue = state.leftValue_
+ const rightValue = state.value
+ const operatorIndex = 1
+ let value
+ switch (node[operatorIndex]) {
+ // eslint-disable-next-line
+ case '==': value = leftValue == rightValue; break
+ // eslint-disable-next-line
+ case '!=': value = leftValue != rightValue; break
+ case '===': value = leftValue === rightValue; break
+ case '!==': value = leftValue !== rightValue; break
+ case '>': value = leftValue > rightValue; break
+ case '>=': value = leftValue >= rightValue; break
+ case '<': value = leftValue < rightValue; break
+ case '<=': value = leftValue <= rightValue; break
+ case '+': value = leftValue + rightValue; break
+ case '-': value = leftValue - rightValue; break
+ case '*': value = leftValue * rightValue; break
+ case '/': value = leftValue / rightValue; break
+ case '%': value = leftValue % rightValue; break
+ case '&': value = leftValue & rightValue; break
+ case '|': value = leftValue | rightValue; break
+ case '^': value = leftValue ^ rightValue; break
+ case '<<': value = leftValue << rightValue; break
+ case '>>': value = leftValue >> rightValue; break
+ case '>>>': value = leftValue >>> rightValue; break
+ case 'in':
+ if (!(rightValue instanceof Object)) {
+ this.throwException(this.TYPE_ERROR, "'in' expects an object, not '" + rightValue + "'")
+ }
+ value = leftValue in rightValue
+ break
+ case 'instanceof':
+ if (!(rightValue instanceof Object)) {
+ this.throwException(this.TYPE_ERROR, 'Right-hand side of instanceof is not an object')
+ }
+ value = leftValue instanceof rightValue
+ break
+ default:
+ throw SyntaxError('Unknown binary operator: ' + node[operatorIndex])
+ }
+ stack[stack.length - 1].value = value
+}
+
+// LogicalExpression
+Interpreter.prototype[37] = function stepLogicalExpression (stack, state, node) {
+ const operatorIndex = 1
+ const leftIndex = 2
+ const rightIndex = 3
+ if (node[operatorIndex] !== '&&' && node[operatorIndex] !== '||') {
+ throw SyntaxError('Unknown logical operator: ' + node[operatorIndex])
+ }
+ if (!state.doneLeft_) {
+ state.doneLeft_ = true
+ return new State(node[leftIndex])
+ }
+
+ if (!state.doneRight_) {
+ state.doneRight_ = true
+ // Shortcut evaluation
+ if ((node[operatorIndex] === '&&' && !state.value) || (node[operatorIndex] === '||' && state.value)) {
+ stack.pop()
+ stack[stack.length - 1].value = state.value
+ } else {
+ state.doneRight_ = true
+ return new State(node[rightIndex])
+ }
+ } else {
+ stack.pop()
+ stack[stack.length - 1].value = state.value
+ }
+}
+
+// MemberExpression
+Interpreter.prototype[38] = function stepMemberExperssion (stack, state, node) {
+ const objectIndex = 1
+ if (!state.doneObject_) {
+ state.doneObject_ = true
+ return new State(node[objectIndex])
+ }
+
+ const computedIndex = 3
+ const propertyIndex = 2
+ const propertyKeyIndex = 1
+ let propName
+ if (!node[computedIndex]) {
+ state.object_ = state.value
+ // obj.foo -- Just access `foo` directly.
+ propName = node[propertyIndex][propertyKeyIndex]
+ } else if (!state.doneProperty_) {
+ state.object_ = state.value
+ // obj[foo] -- Compute value of `foo`.
+ state.doneProperty_ = true
+ return new State(node[propertyIndex])
+ } else {
+ propName = state.value
+ }
+ // todo 取值的优化,错误提示等
+ const value = state.object_[propName]
+ stack.pop()
+ stack[stack.length - 1].value = value
+}
+
+// ConditionalExpression
+Interpreter.prototype[39] = function stepConditionalExpression (stack, state, node) {
+ const testIndex = 1
+ const mode = state.mode_ || 0
+ if (mode === 0) {
+ state.mode_ = 1
+ return new State(node[testIndex])
+ }
+ if (mode === 1) {
+ state.mode_ = 2
+ const value = Boolean(state.value)
+ const consequentIndex = 2
+ const alternateIndex = 3
+ if (value && node[consequentIndex]) {
+ return new State(node[consequentIndex])
+ } else if (!value && node[alternateIndex]) {
+ return new State(node[alternateIndex])
+ }
+ }
+ stack.pop()
+ if (node[0] === 39) {
+ stack[stack.length - 1].value = state.value
+ }
+}
+
+// ExpressionStatement
+Interpreter.prototype[40] = function stepExpressionStatement (stack, state, node) {
+ const expressionIndex = 1
+ if (!state.done_) {
+ state.done_ = true
+ return new State(node[expressionIndex])
+ }
+ stack.pop()
+ // Save this value to interpreter.value for use as a return value
+ this.value = state.value
+}
+
+export default Interpreter
diff --git a/packages/core/src/dynamic/vnode/render.js b/packages/core/src/dynamic/vnode/render.js
new file mode 100644
index 0000000000..20e3d5cca9
--- /dev/null
+++ b/packages/core/src/dynamic/vnode/render.js
@@ -0,0 +1,289 @@
+import cssSelect from './css-select'
+// todo: stringify wxs 模块只能放到逻辑层执行,主要还是因为生成 vdom tree 需要根据 class 去做匹配,需要看下这个代码从哪引入
+import stringify from '@mpxjs/webpack-plugin/lib/runtime/stringify.wxs'
+import Interpreter from './interpreter'
+import { dash2hump, isString, error } from '@mpxjs/utils'
+
+const deepCloneNode = function (val) {
+ return JSON.parse(JSON.stringify(val))
+}
+
+function simpleNormalizeChildren (children) {
+ for (let i = 0; i < children.length; i++) {
+ if (Array.isArray(children[i])) {
+ return Array.prototype.concat.apply([], children)
+ }
+ }
+ return children
+}
+
+export default function _genVnodeTree (astData, contextScope, options) {
+ const { template = {}, styles = [] } = astData || {}
+ const { moduleId, location } = options || {}
+ // 解除引用
+ const templateAst = deepCloneNode(template)
+ // 获取实例 uid
+ const uid = contextScope[0]?.__mpxProxy?.uid || contextScope[0]?.uid
+ // 动态化组件 slots 通过上下文传递,相当于 props
+ const slots = contextScope[0]?.slots || {}
+
+ function createEmptyNode () {
+ return createNode('block')
+ }
+
+ function genVnodeTree (node) {
+ if (node.type === 1) {
+ // wxs 模块不需要动态渲染
+ if (node.tag === 'wxs') {
+ return createEmptyNode()
+ } else if (node.for && !node.forProcessed) {
+ return genFor(node)
+ } else if (node.if && !node.ifProcessed) {
+ return genIf(node)
+ } else if (node.tag === 'slot') {
+ return genSlot(node)
+ } else {
+ const data = genData(node)
+ let children = genChildren(node)
+ // 运行时组件的子组件都通过 slots 属性传递,样式规则在当前组件内匹配后挂载
+ if (node.dynamic) {
+ data.slots = resolveSlot(children.map(item => genVnodeWithStaticCss(deepCloneNode(item))))
+ children = []
+ }
+ return createNode(node.tag, data, children)
+ }
+ } else if (node.type === 3) {
+ return genText(node)
+ }
+ }
+
+ function evalExps (exps) {
+ const interpreter = new Interpreter(contextScope)
+ // 消除引用关系
+ let value
+ try {
+ value = interpreter.eval(JSON.parse(JSON.stringify(exps)))
+ } catch (e) {
+ const errmsg = e.message
+ console.warn(errmsg)
+ error('interprete the expression wrong: ', location, {
+ errType: 'mpx-dynamic-interprete',
+ errmsg
+ })
+ }
+ return value
+ }
+
+ function createNode (tag, data = {}, children = []) {
+ if (Array.isArray(data)) {
+ children = data
+ data = {}
+ }
+ if (typeof tag === 'object') {
+ return tag
+ }
+
+ // 处理 for 循环产生的数组,同时清除空节点
+ children = simpleNormalizeChildren(children).filter(node => !!node?.nt)
+
+ return {
+ nt: tag,
+ d: data,
+ c: children
+ }
+ }
+
+ /**
+ *
+ * 样式隔离的匹配策略优化:
+ *
+ * 条件1: 子组件不能影响到父组件的样式
+ * 条件2: slot 的内容必须在父组件的上下文当中完成样式匹配
+ * 条件3: 匹配过程只能进行一次
+ *
+ * 方案一:根据 moduleId 即作用域来进行匹配
+ * 方案二:根据虚拟树来进行匹配
+ */
+ // function createDynamicNode (moduleId, data = {}, children = []) {
+ // const { template = {}, styles = [] } = staticMap[moduleId]
+ // data.$slots = resolveSlot(children) // 将 slot 通过上下文传递到子组件的渲染流程中
+ // const vnodeTree = _genVnodeTree(template, [data], styles, moduleId)
+ // return vnodeTree
+ // }
+
+ function resolveSlot (children) {
+ const slots = {}
+ if (children.length) {
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ const name = child.d?.slot
+ if (name) {
+ const slot = (slots[name] || (slots[name] = []))
+ if (child.tag === 'template') {
+ slot.push.apply(slot, child.children || [])
+ } else {
+ slot.push(child)
+ }
+ } else {
+ (slots.default || (slots.default = [])).push(child)
+ }
+ }
+ }
+ return slots
+ }
+
+ function genData (node) {
+ const res = {
+ uid,
+ moduleId
+ }
+ if (!node.attrsList) {
+ return res
+ }
+
+ node.attrsList.forEach((attr) => {
+ if (attr.name === 'class' || attr.name === 'style') {
+ // class/style 的表达式为数组形式,class/style的计算过程需要放到逻辑层,主要是因为有逻辑匹配的过程去生成 vnodeTree
+ const helper = attr.name === 'class' ? stringify.stringifyClass : stringify.stringifyStyle
+ let value = ''
+ if (attr.__exp) {
+ let valueArr = evalExps(attr.__exp)
+ valueArr = Array.isArray(valueArr) ? valueArr : [valueArr]
+ value = helper(...valueArr)
+ // dynamic style + wx:show
+ const showStyle = valueArr[2]
+ if (showStyle) {
+ value = value + ';' + showStyle
+ }
+ } else {
+ value = attr.value
+ }
+ res[attr.name] = value
+ } else {
+ res[dash2hump(attr.name)] = attr.__exp
+ ? evalExps(attr.__exp)
+ : attr.value
+ }
+ })
+
+ return res
+ }
+
+ function genChildren (node) {
+ const res = []
+ const children = node.children || []
+ if (children.length) {
+ children.forEach((item) => {
+ res.push(genNode(item))
+ })
+ }
+ return res
+ }
+
+ function genNode (node) {
+ if (node.type === 1) {
+ return genVnodeTree(node)
+ } else if (node.type === 3 && node.isComment) {
+ return ''
+ // TODO: 注释暂不处理
+ // return _genComment(node)
+ } else {
+ return genText(node) // 文本节点统一通过 _genText 来生成,type = 2(带有表达式的文本,在 mpx 统一处理为了3) || type = 3(纯文本,非注释)
+ }
+ }
+
+ function genText (node) {
+ return {
+ nt: '#text',
+ ct: node.__exp ? evalExps(node.__exp) : node.text
+ }
+ }
+
+ function genFor (node) {
+ node.forProcessed = true
+
+ const itemKey = node.for.item || 'item'
+ const indexKey = node.for.index || 'index'
+ const scope = {
+ [itemKey]: null,
+ [indexKey]: null
+ }
+ const forExp = node.for
+ const res = []
+ let forValue = evalExps(forExp.__exp)
+
+ // 和微信的模版渲染策略保持一致:当 wx:for 的值为字符串时,会将字符串解析成字符串数组
+ if (isString(forValue)) {
+ forValue = forValue.split('')
+ }
+
+ if (Array.isArray(forValue)) {
+ forValue.forEach((item, index) => {
+ // item、index 模板当中如果没申明,需要给到默认值
+ scope[itemKey] = item
+ scope[indexKey] = index
+
+ contextScope.push(scope)
+
+ // 针对 for 循环避免每次都操作的同一个 node 导致数据的污染的问题
+ res.push(deepCloneNode(genVnodeTree(deepCloneNode(node))))
+
+ contextScope.pop()
+ })
+ }
+
+ return res
+ }
+
+ // 对于 if 而言最终生成 <= 1 节点
+ function genIf (node) {
+ if (!node.ifConditions) {
+ node.ifConditions = []
+ return {} // 一个空节点
+ }
+ node.ifProcessed = true
+
+ const ifConditions = node.ifConditions.slice()
+
+ let res = {} // 空节点
+ for (let i = 0; i < ifConditions.length; i++) {
+ const condition = ifConditions[i]
+ // 非 else 节点
+ if (condition.ifExp) {
+ const identifierValue = evalExps(condition.__exp)
+ if (identifierValue) {
+ res = genVnodeTree(condition.block === 'self' ? node : condition.block)
+ break
+ }
+ } else { // else 节点
+ res = genVnodeTree(condition.block)
+ break
+ }
+ }
+ return res
+ }
+
+ // 暂时不支持作用域插槽
+ function genSlot (node) {
+ const data = genData(node) // 计算属性值
+ const slotName = data.name || 'default'
+ return slots[slotName] || null
+ }
+
+ function genVnodeWithStaticCss (vnodeTree) {
+ styles.forEach((item) => {
+ const [selector, style] = item
+ const nodes = cssSelect(selector, { moduleId })(vnodeTree)
+ nodes?.forEach((node) => {
+ // todo style 合并策略问题:合并过程中缺少了权重关系 style, class 的判断,需要优化
+ node.d.style = node.d.style ? style + node.d.style : style
+ })
+ })
+
+ return vnodeTree
+ }
+
+ const interpreteredVnodeTree = genVnodeTree(templateAst)
+
+ return genVnodeWithStaticCss(interpreteredVnodeTree)
+}
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index 908ae6d7eb..efac8c8992 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -60,6 +60,8 @@ export {
export { getMixin } from './core/mergeOptions'
+export { dynamic } from './dynamic/astCache'
+
export function toPureObject (obj) {
return diffAndCloneA(obj).clone
}
diff --git a/packages/core/src/platform/builtInMixins/index.js b/packages/core/src/platform/builtInMixins/index.js
index c7c54e63f5..d9d903c32e 100644
--- a/packages/core/src/platform/builtInMixins/index.js
+++ b/packages/core/src/platform/builtInMixins/index.js
@@ -10,6 +10,7 @@ import pageScrollMixin from './pageScrollMixin'
import componentGenericsMixin from './componentGenericsMixin'
import getTabBarMixin from './getTabBarMixin'
import pageRouteMixin from './pageRouteMixin'
+import { dynamicRefsMixin, dynamicRenderHelperMixin, dynamicSlotMixin } from '../../dynamic/dynamicRenderMixin'
export default function getBuiltInMixins (options, type) {
let bulitInMixins = []
@@ -41,6 +42,13 @@ export default function getBuiltInMixins (options, type) {
showMixin(type),
i18nMixin()
])
+ if (__mpx_dynamic_runtime__) {
+ bulitInMixins = bulitInMixins.concat([
+ dynamicRenderHelperMixin(),
+ dynamicSlotMixin(),
+ dynamicRefsMixin()
+ ])
+ }
}
}
return bulitInMixins.filter(item => item)
diff --git a/packages/core/src/platform/builtInMixins/proxyEventMixin.js b/packages/core/src/platform/builtInMixins/proxyEventMixin.js
index f69ee196f3..948d6116b0 100644
--- a/packages/core/src/platform/builtInMixins/proxyEventMixin.js
+++ b/packages/core/src/platform/builtInMixins/proxyEventMixin.js
@@ -1,5 +1,11 @@
import { setByPath, error, dash2hump, collectDataset } from '@mpxjs/utils'
import Mpx from '../../index'
+import contextMap from '../../dynamic/vnode/context'
+
+function logCallbackNotFound (context, callbackName) {
+ const location = context.__mpxProxy && context.__mpxProxy.options.mpxFileResource
+ error(`Instance property [${callbackName}] is not function, please check.`, location)
+}
export default function proxyEventMixin () {
const methods = {
@@ -33,6 +39,9 @@ export default function proxyEventMixin () {
}
const eventConfigs = target.dataset.eventconfigs || {}
const curEventConfig = eventConfigs[type] || eventConfigs[fallbackType] || []
+ // 如果有 mpxuid 说明是运行时组件,那么需要设置对应的上下文
+ const rootRuntimeContext = contextMap.get(target.dataset.mpxuid)
+ const context = rootRuntimeContext || this
let returnedValue
curEventConfig.forEach((item) => {
const callbackName = item[0]
@@ -56,10 +65,10 @@ export default function proxyEventMixin () {
}
})
: [$event]
- if (typeof this[callbackName] === 'function') {
- returnedValue = this[callbackName].apply(this, params)
+ if (typeof context[callbackName] === 'function') {
+ returnedValue = context[callbackName].apply(context, params)
} else {
- error(`Instance property [${callbackName}] is not function, please check.`, location)
+ logCallbackNotFound(context, callbackName)
}
}
})
diff --git a/packages/core/src/platform/builtInMixins/renderHelperMixin.js b/packages/core/src/platform/builtInMixins/renderHelperMixin.js
index c90baefa44..dc8859c2b4 100644
--- a/packages/core/src/platform/builtInMixins/renderHelperMixin.js
+++ b/packages/core/src/platform/builtInMixins/renderHelperMixin.js
@@ -36,8 +36,8 @@ export default function renderHelperMixin () {
_sc (key) {
return (this.__mpxProxy.renderData[key] = this[key])
},
- _r (skipPre) {
- this.__mpxProxy.renderWithData(skipPre)
+ _r (skipPre, vnode) {
+ this.__mpxProxy.renderWithData(skipPre, vnode)
}
}
}
diff --git a/packages/core/src/platform/patch/ali/getDefaultOptions.js b/packages/core/src/platform/patch/ali/getDefaultOptions.js
index 70ac339880..5cc74e7678 100644
--- a/packages/core/src/platform/patch/ali/getDefaultOptions.js
+++ b/packages/core/src/platform/patch/ali/getDefaultOptions.js
@@ -63,6 +63,26 @@ function transformApiForProxy (context, currentInject) {
}
})
}
+ if (currentInject.moduleId) {
+ Object.defineProperties(context, {
+ __moduleId: {
+ get () {
+ return currentInject.moduleId
+ },
+ configurable: false
+ }
+ })
+ }
+ if (currentInject.dynamic) {
+ Object.defineProperties(context, {
+ __dynamic: {
+ get () {
+ return currentInject.dynamic
+ },
+ configurable: false
+ }
+ })
+ }
}
}
diff --git a/packages/core/src/platform/patch/wx/getDefaultOptions.js b/packages/core/src/platform/patch/wx/getDefaultOptions.js
index b3fc09f035..33c37f6a29 100644
--- a/packages/core/src/platform/patch/wx/getDefaultOptions.js
+++ b/packages/core/src/platform/patch/wx/getDefaultOptions.js
@@ -101,6 +101,26 @@ function transformApiForProxy (context, currentInject) {
}
})
}
+ if (currentInject.moduleId) {
+ Object.defineProperties(context, {
+ __moduleId: {
+ get () {
+ return currentInject.moduleId
+ },
+ configurable: false
+ }
+ })
+ }
+ if (currentInject.dynamic) {
+ Object.defineProperties(context, {
+ __dynamic: {
+ get () {
+ return currentInject.dynamic
+ },
+ configurable: false
+ }
+ })
+ }
}
}
diff --git a/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js b/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js
index dba03d2463..608defcacf 100644
--- a/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js
+++ b/packages/webpack-plugin/lib/dependencies/DynamicEntryDependency.js
@@ -5,6 +5,7 @@ const addQuery = require('../utils/add-query')
const toPosix = require('../utils/to-posix')
const async = require('async')
const parseRequest = require('../utils/parse-request')
+const hasOwn = require('../utils/has-own')
class DynamicEntryDependency extends NullDependency {
constructor (range, request, entryType, outputPath = '', packageRoot = '', relativePath = '', context = '', extraOptions = {}) {
@@ -131,6 +132,13 @@ class DynamicEntryDependency extends NullDependency {
this.publicPath = compilation.outputOptions.publicPath || ''
const { packageRoot, context } = this
if (context) this.resolver = compilation.resolverFactory.get('normal', module.resolveOptions)
+ // post 分包队列在 sub 分包队列构建完毕后进行
+ if (this.extraOptions.postSubpackageEntry) {
+ mpx.postSubpackageEntriesMap[packageRoot] = mpx.postSubpackageEntriesMap[packageRoot] || []
+ mpx.postSubpackageEntriesMap[packageRoot].push(this)
+ callback()
+ return
+ }
// 分包构建在需要在主包构建完成后在finishMake中处理,返回的资源路径先用key来占位,在合成extractedAssets时再进行最终替换
if (packageRoot && mpx.currentPackageRoot !== packageRoot) {
mpx.subpackagesEntriesMap[packageRoot] = mpx.subpackagesEntriesMap[packageRoot] || []
@@ -190,7 +198,7 @@ DynamicEntryDependency.Template = class DynamicEntryDependencyTemplate {
let replaceContent = ''
- if (extraOptions.replaceContent) {
+ if (hasOwn(extraOptions, 'replaceContent')) {
replaceContent = extraOptions.replaceContent
} else if (resultPath) {
if (extraOptions.isRequireAsync) {
diff --git a/packages/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency.js b/packages/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency.js
new file mode 100644
index 0000000000..db737b71c9
--- /dev/null
+++ b/packages/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency.js
@@ -0,0 +1,66 @@
+const NullDependency = require('webpack/lib/dependencies/NullDependency')
+const makeSerializable = require('webpack/lib/util/makeSerializable')
+
+class RecordRuntimeInfoDependency extends NullDependency {
+ constructor (packageName, resourcePath, { type, info, index } = {}) {
+ super()
+ this.packageName = packageName
+ this.resourcePath = resourcePath
+ this.blockType = type
+ this.info = info
+ this.index = index
+ }
+
+ get type () {
+ return 'mpx record runtime component info'
+ }
+
+ mpxAction (module, compilation, callback) {
+ const mpx = compilation.__mpx__
+
+ const runtimeInfoPackage = mpx.runtimeInfo[this.packageName] = mpx.runtimeInfo[this.packageName] || {}
+ const componentInfo = runtimeInfoPackage[this.resourcePath] = runtimeInfoPackage[this.resourcePath] || {
+ template: {},
+ json: {},
+ style: [],
+ moduleId: '_' + mpx.pathHash(this.resourcePath)
+ }
+
+ const infoConfig = componentInfo[this.blockType]
+ if (this.blockType === 'style') { // 多 style block 的场景
+ infoConfig[this.index] = this.info
+ } else {
+ Object.assign(infoConfig, this.info)
+ }
+
+ return callback()
+ }
+
+ serialize (context) {
+ const { write } = context
+ write(this.packageName)
+ write(this.resourcePath)
+ write(this.blockType)
+ write(this.info)
+ write(this.index)
+ super.serialize(context)
+ }
+
+ deserialize (context) {
+ const { read } = context
+ this.packageName = read()
+ this.resourcePath = read()
+ this.blockType = read()
+ this.info = read()
+ this.index = read()
+ super.deserialize(context)
+ }
+}
+
+RecordRuntimeInfoDependency.Template = class RecordRuntimeInfoDependencyTemplate {
+ apply () {}
+}
+
+makeSerializable(RecordRuntimeInfoDependency, '@mpxjs/webpack-plugin/lib/dependencies/RecordRuntimeInfoDependency')
+
+module.exports = RecordRuntimeInfoDependency
diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js
index 8747a9de17..3f54df5cc5 100644
--- a/packages/webpack-plugin/lib/index.js
+++ b/packages/webpack-plugin/lib/index.js
@@ -40,6 +40,7 @@ const DynamicEntryDependency = require('./dependencies/DynamicEntryDependency')
const FlagPluginDependency = require('./dependencies/FlagPluginDependency')
const RemoveEntryDependency = require('./dependencies/RemoveEntryDependency')
const RecordVueContentDependency = require('./dependencies/RecordVueContentDependency')
+const RecordRuntimeInfoDependency = require('./dependencies/RecordRuntimeInfoDependency')
const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin')
const fixRelative = require('./utils/fix-relative')
const parseRequest = require('./utils/parse-request')
@@ -63,6 +64,7 @@ const stringifyLoadersAndResource = require('./utils/stringify-loaders-resource'
const emitFile = require('./utils/emit-file')
const { MPX_PROCESSED_FLAG, MPX_DISABLE_EXTRACTOR_CACHE } = require('./utils/const')
const isEmptyObject = require('./utils/is-empty-object')
+const DynamicPlugin = require('./resolver/DynamicPlugin')
require('./utils/check-core-version-match')
const isProductionLikeMode = options => {
@@ -123,6 +125,9 @@ class MpxWebpackPlugin {
if (options.mode === 'web' && options.srcMode !== 'wx') {
errors.push('MpxWebpackPlugin supports mode to be "web" only when srcMode is set to "wx"!')
}
+ if (options.dynamicComponentRules && !options.dynamicRuntime) {
+ errors.push('Please make sure you have set dynamicRuntime true in mpx webpack plugin config because you have use the dynamic runtime feature.')
+ }
options.externalClasses = options.externalClasses || ['custom-class', 'i-class']
options.resolveMode = options.resolveMode || 'webpack'
options.writeMode = options.writeMode || 'changed'
@@ -136,7 +141,8 @@ class MpxWebpackPlugin {
options.defs = Object.assign({}, options.defs, {
__mpx_mode__: options.mode,
__mpx_src_mode__: options.srcMode,
- __mpx_env__: options.env
+ __mpx_env__: options.env,
+ __mpx_dynamic_runtime__: options.dynamicRuntime
})
// 批量指定源码mode
options.modeRules = options.modeRules || {}
@@ -174,6 +180,7 @@ class MpxWebpackPlugin {
options.optimizeRenderRules = options.optimizeRenderRules ? (Array.isArray(options.optimizeRenderRules) ? options.optimizeRenderRules : [options.optimizeRenderRules]) : []
options.retryRequireAsync = options.retryRequireAsync || false
options.optimizeSize = options.optimizeSize || false
+ options.dynamicComponentRules = options.dynamicComponentRules || {}// 运行时组件配置
this.options = options
// Hack for buildDependencies
const rawResolveBuildDependencies = FileSystemInfo.prototype.resolveBuildDependencies
@@ -320,6 +327,9 @@ class MpxWebpackPlugin {
const addModePlugin = new AddModePlugin('before-file', this.options.mode, this.options.fileConditionRules, 'file')
const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file')
const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, 'file')
+
+ const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules)
+
if (Array.isArray(compiler.options.resolve.plugins)) {
compiler.options.resolve.plugins.push(addModePlugin)
} else {
@@ -330,6 +340,7 @@ class MpxWebpackPlugin {
}
compiler.options.resolve.plugins.push(packageEntryPlugin)
compiler.options.resolve.plugins.push(new FixDescriptionInfoPlugin())
+ compiler.options.resolve.plugins.push(dynamicPlugin)
const optimization = compiler.options.optimization
if (this.options.mode !== 'web') {
@@ -472,12 +483,12 @@ class MpxWebpackPlugin {
}
}
- const processSubpackagesEntriesMap = (compilation, callback) => {
+ const processSubpackagesEntriesMap = (subPackageEntriesType, compilation, callback) => {
const mpx = compilation.__mpx__
- if (mpx && !isEmptyObject(mpx.subpackagesEntriesMap)) {
- const subpackagesEntriesMap = mpx.subpackagesEntriesMap
- // 执行分包队列前清空mpx.subpackagesEntriesMap
- mpx.subpackagesEntriesMap = {}
+ if (mpx && !isEmptyObject(mpx[subPackageEntriesType])) {
+ const subpackagesEntriesMap = mpx[subPackageEntriesType]
+ // 执行分包队列前清空 mpx[subPackageEntriesType]
+ mpx[subPackageEntriesType] = {}
async.eachOfSeries(subpackagesEntriesMap, (deps, packageRoot, callback) => {
mpx.currentPackageRoot = packageRoot
mpx.componentsMap[packageRoot] = mpx.componentsMap[packageRoot] || {}
@@ -493,7 +504,7 @@ class MpxWebpackPlugin {
}, (err) => {
if (err) return callback(err)
// 如果执行完当前队列后产生了新的分包执行队列(一般由异步分包组件造成),则继续执行
- processSubpackagesEntriesMap(compilation, callback)
+ processSubpackagesEntriesMap(subPackageEntriesType, compilation, callback)
})
} else {
callback()
@@ -505,7 +516,14 @@ class MpxWebpackPlugin {
name: 'MpxWebpackPlugin',
stage: -1000
}, (compilation, callback) => {
- processSubpackagesEntriesMap(compilation, (err) => {
+ async.series([
+ (callback) => {
+ processSubpackagesEntriesMap('subpackagesEntriesMap', compilation, callback)
+ },
+ (callback) => {
+ processSubpackagesEntriesMap('postSubpackageEntriesMap', compilation, callback)
+ }
+ ], (err) => {
if (err) return callback(err)
const checkDynamicEntryInfo = () => {
for (const packageName in mpx.dynamicEntryInfo) {
@@ -578,6 +596,9 @@ class MpxWebpackPlugin {
compilation.dependencyFactories.set(RecordVueContentDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordVueContentDependency, new RecordVueContentDependency.Template())
+ compilation.dependencyFactories.set(RecordRuntimeInfoDependency, new NullFactory())
+ compilation.dependencyTemplates.set(RecordRuntimeInfoDependency, new RecordRuntimeInfoDependency.Template())
+
compilation.dependencyTemplates.set(ImportDependency, new ImportDependencyTemplate())
})
@@ -610,6 +631,7 @@ class MpxWebpackPlugin {
// 记录独立分包
independentSubpackagesMap: {},
subpackagesEntriesMap: {},
+ postSubpackageEntriesMap: {},
replacePathMap: {},
exportModules: new Set(),
// 记录动态添加入口的分包信息
@@ -837,6 +859,126 @@ class MpxWebpackPlugin {
error
})
}
+ },
+ // 以包为维度记录不同 package 需要的组件属性等信息,用以最终 mpx-custom-element 相关文件的输出
+ runtimeInfo: {},
+ // 记录运行时组件依赖的运行时组件当中使用的基础组件 slot,最终依据依赖关系注入到运行时组件的 json 配置当中
+ dynamicSlotDependencies: {},
+ // 依据 package 注入到 mpx-custom-element-*.json 里面的组件路径
+ getPackageInjectedComponentsMap: (packageName = 'main') => {
+ const res = {}
+ const runtimeInfo = mpx.runtimeInfo[packageName] || {}
+ const componentsMap = mpx.componentsMap[packageName] || {}
+ const publicPath = compilation.outputOptions.publicPath || ''
+ for (const componentPath in runtimeInfo) {
+ Object.values(runtimeInfo[componentPath].json).forEach(({ hashName, resourcePath }) => {
+ const outputPath = componentsMap[resourcePath]
+ if (outputPath) {
+ res[hashName] = publicPath + outputPath
+ }
+ })
+ }
+ return res
+ },
+ // 获取生成基础递归渲染模版的节点配置信息
+ getPackageInjectedTemplateConfig: (packageName = 'main') => {
+ const res = {
+ baseComponents: {
+ block: {}
+ },
+ runtimeComponents: {},
+ normalComponents: {}
+ }
+
+ const runtimeInfo = mpx.runtimeInfo[packageName] || {}
+
+ // 包含了某个分包当中所有的运行时组件
+ for (const resourcePath in runtimeInfo) {
+ const { json, template } = runtimeInfo[resourcePath]
+ const customComponents = template.customComponents || {}
+ const baseComponents = template.baseComponents || {}
+
+ // 合并自定义组件的属性
+ for (const componentName in customComponents) {
+ const extraAttrs = {}
+ const attrsMap = customComponents[componentName]
+ const { hashName, isDynamic } = json[componentName] || {}
+ let componentType = 'normalComponents'
+ if (isDynamic) {
+ componentType = 'runtimeComponents'
+ extraAttrs.slots = ''
+ }
+ if (!res[componentType][hashName]) {
+ res[componentType][hashName] = {}
+ }
+
+ Object.assign(res[componentType][hashName], {
+ ...attrsMap,
+ ...extraAttrs
+ })
+ }
+
+ // 合并基础节点的属性
+ for (const componentName in baseComponents) {
+ const attrsMap = baseComponents[componentName]
+ if (!res.baseComponents[componentName]) {
+ res.baseComponents[componentName] = {}
+ }
+ Object.assign(res.baseComponents[componentName], attrsMap)
+ }
+ }
+
+ return res
+ },
+ injectDynamicSlotDependencies: (usingComponents, resourcePath) => {
+ const dynamicSlotReg = /"mpx_dynamic_slot":\s*""*/
+ const content = mpx.dynamicSlotDependencies[resourcePath] ? JSON.stringify(mpx.dynamicSlotDependencies[resourcePath]).slice(1, -1) : ''
+ const result = usingComponents.replace(dynamicSlotReg, content).replace(/,\s*(\}|\])/g, '$1')
+ return result
+ },
+ changeHashNameForAstNode: (templateAst, componentsMap) => {
+ const nameReg = /"tag":\s*"([^"]*)",?/g
+
+ // 替换 astNode hashName
+ const result = templateAst.replace(nameReg, function (match, tag) {
+ if (componentsMap[tag]) {
+ const { hashName, isDynamic } = componentsMap[tag]
+ let content = `"tag": "${hashName}",`
+ if (isDynamic) {
+ content += '"dynamic": true,'
+ }
+ return content
+ }
+ return match
+ }).replace(/,\s*(\}|\])/g, '$1')
+
+ return result
+ },
+ collectDynamicSlotDependencies: (packageName = 'main') => {
+ const componentsMap = mpx.componentsMap[packageName] || {}
+ const publicPath = compilation.outputOptions.publicPath || ''
+ const runtimeInfo = mpx.runtimeInfo[packageName]
+
+ for (const resourcePath in runtimeInfo) {
+ const { template, json } = runtimeInfo[resourcePath]
+ const dynamicSlotDependencies = template.dynamicSlotDependencies || []
+
+ dynamicSlotDependencies.forEach((slotDependencies) => {
+ let lastNeedInjectNode = slotDependencies[0]
+ for (let i = 1; i <= slotDependencies.length - 1; i++) {
+ const componentName = slotDependencies[i]
+ const { resourcePath, isDynamic } = json[componentName] || {}
+ if (isDynamic) {
+ const { resourcePath: path, hashName } = json[lastNeedInjectNode]
+ mpx.dynamicSlotDependencies[resourcePath] = mpx.dynamicSlotDependencies[resourcePath] || {}
+ Object.assign(mpx.dynamicSlotDependencies[resourcePath], {
+ [hashName]: publicPath + componentsMap[path]
+ })
+ lastNeedInjectNode = slotDependencies[i]
+ }
+ }
+ })
+ }
}
}
}
@@ -1094,6 +1236,37 @@ class MpxWebpackPlugin {
}
})
+ compilation.hooks.processAssets.tap({
+ name: 'MpxWebpackPlugin'
+ }, (assets) => {
+ const dynamicAssets = {}
+ for (const packageName in mpx.runtimeInfo) {
+ for (const resourcePath in mpx.runtimeInfo[packageName]) {
+ const { moduleId, template, style, json } = mpx.runtimeInfo[packageName][resourcePath]
+ const templateAst = mpx.changeHashNameForAstNode(template.templateAst, json)
+ dynamicAssets[moduleId] = {
+ template: JSON.parse(templateAst),
+ styles: style.reduce((preV, curV) => {
+ preV.push(...curV)
+ return preV
+ }, [])
+ }
+
+ // 注入 dynamic slot dependency
+ const outputPath = mpx.componentsMap[packageName][resourcePath]
+ if (outputPath) {
+ const jsonAsset = outputPath + '.json'
+ const jsonContent = compilation.assets[jsonAsset].source()
+ compilation.assets[jsonAsset] = new RawSource(mpx.injectDynamicSlotDependencies(jsonContent, resourcePath))
+ }
+ }
+ }
+ if (!isEmptyObject(dynamicAssets)) {
+ // 产出 jsonAst 静态产物
+ compilation.assets['dynamic.json'] = new RawSource(JSON.stringify(dynamicAssets))
+ }
+ })
+
const normalModuleFactoryParserCallback = (parser) => {
parser.hooks.call.for('__mpx_resolve_path__').tap('MpxWebpackPlugin', (expr) => {
if (expr.arguments[0]) {
diff --git a/packages/webpack-plugin/lib/json-compiler/helper.js b/packages/webpack-plugin/lib/json-compiler/helper.js
index 4417f19aef..09a8622b58 100644
--- a/packages/webpack-plugin/lib/json-compiler/helper.js
+++ b/packages/webpack-plugin/lib/json-compiler/helper.js
@@ -99,7 +99,9 @@ module.exports = function createJSONHelper ({ loaderContext, emitWarning, custom
const entry = getDynamicEntry(resource, 'component', outputPath, tarRoot, relativePath, '', extraOptions)
callback(null, entry, {
tarRoot,
- placeholder
+ placeholder,
+ resourcePath,
+ queryObj
})
})
}
diff --git a/packages/webpack-plugin/lib/json-compiler/index.js b/packages/webpack-plugin/lib/json-compiler/index.js
index 243e6bf47a..444b47813d 100644
--- a/packages/webpack-plugin/lib/json-compiler/index.js
+++ b/packages/webpack-plugin/lib/json-compiler/index.js
@@ -12,12 +12,14 @@ const createHelpers = require('../helpers')
const createJSONHelper = require('./helper')
const RecordGlobalComponentsDependency = require('../dependencies/RecordGlobalComponentsDependency')
const RecordIndependentDependency = require('../dependencies/RecordIndependentDependency')
+const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency')
const { MPX_DISABLE_EXTRACTOR_CACHE, RESOLVE_IGNORED_ERR, JSON_JS_EXT } = require('../utils/const')
const resolve = require('../utils/resolve')
const resolveTabBarPath = require('../utils/resolve-tab-bar-path')
const normalize = require('../utils/normalize')
const mpxViewPath = normalize.lib('runtime/components/ali/mpx-view.mpx')
const mpxTextPath = normalize.lib('runtime/components/ali/mpx-text.mpx')
+const resolveMpxCustomElementPath = require('../utils/resolve-mpx-custom-element-path')
module.exports = function (content) {
const nativeCallback = this.async()
@@ -47,6 +49,7 @@ module.exports = function (content) {
const isApp = !(pagesMap[resourcePath] || componentsMap[resourcePath])
const publicPath = this._compilation.outputOptions.publicPath || ''
const fs = this._compiler.inputFileSystem
+ const runtimeCompile = queryObj.isDynamic
const emitWarning = (msg) => {
this.emitWarning(
@@ -173,6 +176,17 @@ module.exports = function (content) {
}
}
+ const dependencyComponentsMap = {}
+
+ if (queryObj.mpxCustomElement) {
+ this.cacheable(false)
+ mpx.collectDynamicSlotDependencies(packageName)
+ }
+
+ if (runtimeCompile) {
+ json.usingComponents = json.usingComponents || {}
+ }
+
// 快应用补全json配置,必填项
if (mode === 'qa' && isApp) {
const defaultConf = {
@@ -219,13 +233,24 @@ module.exports = function (content) {
const processComponents = (components, context, callback) => {
if (components) {
async.eachOf(components, (component, name, callback) => {
- processComponent(component, context, { relativePath }, (err, entry, { tarRoot, placeholder } = {}) => {
+ processComponent(component, context, { relativePath }, (err, entry, { tarRoot, placeholder, resourcePath, queryObj = {} } = {}) => {
if (err === RESOLVE_IGNORED_ERR) {
delete components[name]
return callback()
}
if (err) return callback(err)
components[name] = entry
+ if (runtimeCompile) {
+ // 替换组件的 hashName,并删除原有的组件配置
+ const hashName = 'm' + mpx.pathHash(resourcePath)
+ components[hashName] = entry
+ delete components[name]
+ dependencyComponentsMap[name] = {
+ hashName,
+ resourcePath,
+ isDynamic: queryObj.isDynamic
+ }
+ }
if (tarRoot) {
if (placeholder) {
placeholder = normalizePlaceholder(placeholder)
@@ -250,7 +275,20 @@ module.exports = function (content) {
callback()
}
})
- }, callback)
+ }, () => {
+ const mpxCustomElementPath = resolveMpxCustomElementPath(packageName)
+ if (runtimeCompile) {
+ components.element = mpxCustomElementPath
+ components.mpx_dynamic_slot = '' // 运行时组件打标记,在 processAssets 统一替换
+
+ this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'json', info: dependencyComponentsMap }))
+ }
+ if (queryObj.mpxCustomElement) {
+ components.element = mpxCustomElementPath
+ Object.assign(components, mpx.getPackageInjectedComponentsMap(packageName))
+ }
+ callback()
+ })
} else {
callback()
}
diff --git a/packages/webpack-plugin/lib/loader.js b/packages/webpack-plugin/lib/loader.js
index 4a7c5b4c3f..8fe76ba1c7 100644
--- a/packages/webpack-plugin/lib/loader.js
+++ b/packages/webpack-plugin/lib/loader.js
@@ -16,11 +16,13 @@ const AppEntryDependency = require('./dependencies/AppEntryDependency')
const RecordResourceMapDependency = require('./dependencies/RecordResourceMapDependency')
const RecordVueContentDependency = require('./dependencies/RecordVueContentDependency')
const CommonJsVariableDependency = require('./dependencies/CommonJsVariableDependency')
+const DynamicEntryDependency = require('./dependencies/DynamicEntryDependency')
const tsWatchRunLoaderFilter = require('./utils/ts-loader-watch-run-loader-filter')
const { MPX_APP_MODULE_ID } = require('./utils/const')
const path = require('path')
const processMainScript = require('./web/processMainScript')
const getRulesRunner = require('./platform')
+const genMpxCustomElement = require('./runtime-render/gen-mpx-custom-element')
module.exports = function (content) {
this.cacheable()
@@ -50,6 +52,7 @@ module.exports = function (content) {
const localSrcMode = queryObj.mode
const srcMode = localSrcMode || globalSrcMode
const autoScope = matchCondition(resourcePath, mpx.autoScopeRules)
+ const isRuntimeMode = queryObj.isDynamic
const emitWarning = (msg) => {
this.emitWarning(
@@ -80,6 +83,12 @@ module.exports = function (content) {
const appName = getEntryName(this)
if (appName) this._module.addPresentationalDependency(new AppEntryDependency(resourcePath, appName))
}
+
+ if (isRuntimeMode) {
+ const { request, outputPath } = genMpxCustomElement(packageName)
+ this._module.addPresentationalDependency(new DynamicEntryDependency([0, 0], request, 'component', outputPath, packageRoot, '', '', { replaceContent: '', postSubpackageEntry: true }))
+ }
+
const loaderContext = this
const isProduction = this.minimize || process.env.NODE_ENV === 'production'
const filePath = this.resourcePath
diff --git a/packages/webpack-plugin/lib/resolver/DynamicPlugin.js b/packages/webpack-plugin/lib/resolver/DynamicPlugin.js
new file mode 100644
index 0000000000..977b1cb426
--- /dev/null
+++ b/packages/webpack-plugin/lib/resolver/DynamicPlugin.js
@@ -0,0 +1,18 @@
+const addQuery = require('../utils/add-query')
+const { matchCondition } = require('../utils/match-condition')
+
+module.exports = class DynamicPlugin {
+ constructor (source, dynamicComponentRules) {
+ this.source = source
+ this.dynamicComponentRules = dynamicComponentRules
+ }
+
+ apply (resolver) {
+ resolver.getHook(this.source).tap('DynamicPlugin', request => {
+ const isDynamic = matchCondition(request.path, this.dynamicComponentRules)
+ if (isDynamic) {
+ request.query = addQuery(request.query, { isDynamic: true })
+ }
+ })
+ }
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/base-wxml.js b/packages/webpack-plugin/lib/runtime-render/base-wxml.js
new file mode 100644
index 0000000000..890884ec56
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/base-wxml.js
@@ -0,0 +1,75 @@
+const { getOptimizedComponentInfo } = require('@mpxjs/template-engine/dist/optimizer')
+const mpxConfig = require('../config')
+
+function makeAttrsMap (attrKeys = []) {
+ return attrKeys.reduce((preVal, curVal) => Object.assign(preVal, { [curVal]: '' }), {})
+}
+
+// 部分节点类型不需要被收集
+const RUNTIME_FILTER_NODES = ['import', 'template', 'wxs', 'component', 'slot']
+
+function collectParentCustomComponent (el, isComponentNode, options) {
+ const res = []
+ let parent = el.parent
+ while (parent) {
+ if (isComponentNode(parent, options)) {
+ if (!res.length) res.push(el.tag)
+ res.push(parent.tag)
+ }
+ parent = parent.parent
+ }
+ return res
+}
+
+module.exports = function setBaseWxml (el, config, meta) {
+ const { mode, isComponentNode, options } = config
+ if (RUNTIME_FILTER_NODES.includes(el.tag)) {
+ return
+ }
+
+ if (options.runtimeCompile) {
+ const isCustomComponent = isComponentNode(el, options)
+
+ if (!meta.runtimeInfo) {
+ meta.runtimeInfo = {
+ baseComponents: {},
+ customComponents: {},
+ dynamicSlotDependencies: []
+ }
+ }
+
+ const tag = el.tag
+ // 属性收集
+ const modeConfig = mpxConfig[mode]
+ const directives = new Set([...Object.values(modeConfig.directive), 'slot'])
+ const attrKeys = Object.keys(el.attrsMap).filter(key => !directives.has(key))
+ const componentType = isCustomComponent ? 'customComponents' : 'baseComponents'
+
+ if (!isCustomComponent) {
+ const optimizedInfo = getOptimizedComponentInfo(
+ {
+ nodeType: el.tag,
+ attrs: el.attrsMap
+ },
+ mode
+ )
+ if (optimizedInfo) {
+ el.tag = optimizedInfo.nodeType
+ }
+ } else {
+ // 收集运行时组件模版当中运行时组件使用 slot 的场景,主要因为运行时组件渲染slot时组件上下文发生了变化
+ const slotDependencies = collectParentCustomComponent(el, isComponentNode, options)
+ if (slotDependencies.length) {
+ const dynamicSlotDependencies = meta.runtimeInfo.dynamicSlotDependencies
+ dynamicSlotDependencies.push(slotDependencies)
+ }
+ }
+
+ const componentsConfig = meta.runtimeInfo[componentType]
+
+ if (!componentsConfig[tag]) {
+ componentsConfig[tag] = {}
+ }
+ Object.assign(componentsConfig[tag], makeAttrsMap(attrKeys))
+ }
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/custom-element-json.js b/packages/webpack-plugin/lib/runtime-render/custom-element-json.js
new file mode 100644
index 0000000000..5cd736d6da
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/custom-element-json.js
@@ -0,0 +1,4 @@
+module.exports = {
+ component: true,
+ usingComponents: {}
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/custom-element-script.js b/packages/webpack-plugin/lib/runtime-render/custom-element-script.js
new file mode 100644
index 0000000000..261f791d0f
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/custom-element-script.js
@@ -0,0 +1,32 @@
+import { createComponent } from '@mpxjs/core'
+
+createComponent({
+ options: {
+ addGlobalClass: true,
+ styleIsolation: 'shared',
+ // 超过基础模板的层数,virtualHost 置为 true,避免样式规则失效
+ virtualHost: true
+ },
+ properties: {
+ r: { // vdom 数据
+ type: Object,
+ value: {
+ nt: 'block'
+ }
+ }
+ },
+ data: {
+ // 运行时组件的标识
+ mpxCustomElement: true
+ },
+ computed: {
+ vnodeData () {
+ const data = this.r.d || {}
+ return data
+ },
+ // 真实的组件上下文 uid
+ uid () {
+ return this.vnodeData.uid
+ }
+ }
+})
diff --git a/packages/webpack-plugin/lib/runtime-render/gen-dynamic-template.js b/packages/webpack-plugin/lib/runtime-render/gen-dynamic-template.js
new file mode 100644
index 0000000000..9e223bb9b6
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/gen-dynamic-template.js
@@ -0,0 +1,6 @@
+const { createSetupTemplate } = require('@mpxjs/template-engine')
+
+module.exports = function (packageName) {
+ const basePath = packageName === 'main' ? '' : `/${packageName}`
+ return `${createSetupTemplate()}`
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/gen-mpx-custom-element.js b/packages/webpack-plugin/lib/runtime-render/gen-mpx-custom-element.js
new file mode 100644
index 0000000000..fe7f815bb7
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/gen-mpx-custom-element.js
@@ -0,0 +1,13 @@
+const path = require('path')
+
+module.exports = function (packageName) {
+ const baseName = 'mpx-custom-element'
+ const rawFile = baseName + (packageName === 'main' ? '-main' : '')
+ const filePath = path.resolve(__dirname, `${rawFile}.mpx`)
+ const request = `${filePath}?mpxCustomElement&isComponent&p=${packageName}`
+
+ return {
+ request,
+ outputPath: baseName + '-' + packageName
+ }
+}
diff --git a/packages/webpack-plugin/lib/runtime-render/mpx-custom-element-main.mpx b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element-main.mpx
new file mode 100644
index 0000000000..574a711845
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element-main.mpx
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx
new file mode 100644
index 0000000000..574a711845
--- /dev/null
+++ b/packages/webpack-plugin/lib/runtime-render/mpx-custom-element.mpx
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/webpack-plugin/lib/style-compiler/index.js b/packages/webpack-plugin/lib/style-compiler/index.js
index f7400ae0e7..820b1a9c48 100644
--- a/packages/webpack-plugin/lib/style-compiler/index.js
+++ b/packages/webpack-plugin/lib/style-compiler/index.js
@@ -1,14 +1,16 @@
const path = require('path')
const postcss = require('postcss')
const loadPostcssConfig = require('./load-postcss-config')
-const { MPX_ROOT_VIEW } = require('../utils/const')
+const { MPX_ROOT_VIEW, MPX_DISABLE_EXTRACTOR_CACHE } = require('../utils/const')
const rpx = require('./plugins/rpx')
const vw = require('./plugins/vw')
const pluginCondStrip = require('./plugins/conditional-strip')
const scopeId = require('./plugins/scope-id')
const transSpecial = require('./plugins/trans-special')
+const cssArrayList = require('./plugins/css-array-list')
const { matchCondition } = require('../utils/match-condition')
const parseRequest = require('../utils/parse-request')
+const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency')
module.exports = function (css, map) {
this.cacheable()
@@ -22,6 +24,9 @@ module.exports = function (css, map) {
const isApp = resourcePath === appInfo.resourcePath
const transRpxRulesRaw = mpx.transRpxRules
const transRpxRules = transRpxRulesRaw ? (Array.isArray(transRpxRulesRaw) ? transRpxRulesRaw : [transRpxRulesRaw]) : []
+ const runtimeCompile = queryObj.isDynamic
+ const index = queryObj.index || 0
+ const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main'
const transRpxFn = mpx.webConfig.transRpxFn
const testResolveRange = (include = () => true, exclude) => {
@@ -85,6 +90,11 @@ module.exports = function (css, map) {
const finalPlugins = config.prePlugins.concat(plugins, config.plugins)
+ const cssList = []
+ if (runtimeCompile) {
+ finalPlugins.push(cssArrayList(cssList))
+ }
+
return postcss(finalPlugins)
.process(css, options)
.then(result => {
@@ -128,6 +138,13 @@ module.exports = function (css, map) {
}
}
+ if (runtimeCompile) {
+ // 包含了运行时组件的 style 模块必须每次都创建(但并不是每次都需要build),用于收集组件节点信息,传递信息以禁用父级extractor的缓存
+ this.emitFile(MPX_DISABLE_EXTRACTOR_CACHE, '', undefined, { skipEmit: true })
+ this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'style', info: cssList, index }))
+ return cb(null, '')
+ }
+
const map = result.map && result.map.toJSON()
cb(null, result.css, map)
return null // silence bluebird warning
diff --git a/packages/webpack-plugin/lib/style-compiler/plugins/css-array-list.js b/packages/webpack-plugin/lib/style-compiler/plugins/css-array-list.js
new file mode 100644
index 0000000000..a37787d7a8
--- /dev/null
+++ b/packages/webpack-plugin/lib/style-compiler/plugins/css-array-list.js
@@ -0,0 +1,26 @@
+module.exports = (cssList = []) => {
+ // Work with options here
+
+ return {
+ postcssPlugin: 'css-array-list',
+ /*
+ Root (root, postcss) {
+ // Transform CSS AST here
+ }
+ */
+
+ Rule (rule) {
+ // todo 特殊字符的处理,vtree 内部是否有做处理
+ const selector = rule.selector.trim().replace('\n', '')
+ let decls = ''
+ if (rule.nodes && rule.nodes.length) {
+ rule.nodes.forEach(item => {
+ decls += `${item.prop}: ${item.value}; `
+ })
+ }
+ cssList.push([selector, decls])
+ }
+ }
+}
+
+module.exports.postcss = true
diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js
index 1bc2483a93..2a0020b3fe 100644
--- a/packages/webpack-plugin/lib/template-compiler/compiler.js
+++ b/packages/webpack-plugin/lib/template-compiler/compiler.js
@@ -12,6 +12,9 @@ const transDynamicClassExpr = require('./trans-dynamic-class-expr')
const dash2hump = require('../utils/hump-dash').dash2hump
const makeMap = require('../utils/make-map')
const { isNonPhrasingTag } = require('../utils/dom-tag-config')
+const setBaseWxml = require('../runtime-render/base-wxml')
+const { parseExp } = require('./parse-exps')
+
const shallowStringify = require('../utils/shallow-stringify')
const no = function () {
return false
@@ -677,6 +680,7 @@ function parse (template, options) {
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
const element = createASTElement(tag, attrs, currentParent)
+
if (ns) {
element.ns = ns
}
@@ -743,7 +747,7 @@ function parse (template, options) {
parent: currentParent
}
children.push(el)
- processText(el, meta, options)
+ options.runtimeCompile ? processTextDynamic(el) : processText(el)
}
}
},
@@ -855,13 +859,20 @@ function modifyAttr (el, name, val) {
}
}
-function postMoveBaseDirective (target, source, isDelete = true) {
+function postMoveBaseDirective (target, source, options, isDelete = true) {
target.for = source.for
target.if = source.if
target.elseif = source.elseif
target.else = source.else
- postProcessFor(target)
- postProcessIf(target)
+
+ if (options.runtimeCompile) {
+ postProcessForDynamic(target, config[mode])
+ postProcessIfDynamic(target, config[mode])
+ } else {
+ postProcessFor(target)
+ postProcessIf(target)
+ }
+
if (isDelete) {
delete source.for
delete source.if
@@ -969,7 +980,7 @@ function processComponentIs (el, options) {
const eventIdentifier = '__mpx_event__'
-function parseFuncStr (str) {
+function parseFuncStr (str, extraStr = '') {
const funcRE = /^([^()]+)(\((.*)\))?/
const match = funcRE.exec(str)
if (match) {
@@ -987,7 +998,7 @@ function parseFuncStr (str) {
}
return {
hasArgs,
- expStr: `[${funcName + args}]`
+ expStr: `[${funcName + args + extraStr}]`
}
}
}
@@ -1040,7 +1051,10 @@ function processEvent (el, options) {
if (parsedEvent) {
const type = parsedEvent.eventName
const modifiers = (parsedEvent.modifier || '').split('.')
- const parsedFunc = parseFuncStr(value)
+ const prefix = parsedEvent.prefix
+ // catch 场景下,下发的 eventconfig 里面包含特殊字符,用以运行时的判断
+ const extraStr = options.runtimeCompile && prefix === 'catch' ? `, "__mpx_${prefix}"` : ''
+ const parsedFunc = parseFuncStr(value, extraStr)
if (parsedFunc) {
if (!eventConfigMap[type]) {
eventConfigMap[type] = {
@@ -1741,10 +1755,13 @@ function processText (el, meta) {
// }])
// }
-function injectWxs (meta, module, src) {
+function injectWxs (meta, module, src, options) {
if (addWxsModule(meta, module, src)) {
return
}
+ if (options && options.runtimeCompile) {
+ return
+ }
const wxsNode = createASTElement(config[mode].wxs.tag, [
{
name: config[mode].wxs.module,
@@ -1947,7 +1964,7 @@ function processBuiltInComponents (el, meta) {
}
}
-function postProcessAliComponentRootView (el, options) {
+function postProcessAliComponentRootView (el, options, meta) {
const processAttrsConditions = [
{ condition: /^(on|catch)Tap$/, action: 'clone' },
{ condition: /^(on|catch)TouchStart$/, action: 'clone' },
@@ -2001,9 +2018,16 @@ function postProcessAliComponentRootView (el, options) {
processAppendRules(el)
const componentWrapView = createASTElement('view', newAttrs)
+
replaceNode(el, componentWrapView, true)
addChild(componentWrapView, el)
- postMoveBaseDirective(componentWrapView, el)
+ processAttrs(componentWrapView, options)
+ postMoveBaseDirective(componentWrapView, el, options)
+
+ if (options.runtimeCompile) {
+ collectDynamicInfo(componentWrapView, options, meta)
+ postProcessAttrsDynamic(componentWrapView, config[mode])
+ }
}
// 有virtualHost情况wx组件注入virtualHost。无virtualHost阿里组件注入root-view。其他跳过。
@@ -2063,22 +2087,30 @@ function processShow (el, options, root) {
value: show
}])
} else {
- processShowStyle()
+ if (options.runtimeCompile) {
+ processShowStyleDynamic(el, show)
+ } else {
+ processShowStyle(el, show)
+ }
}
} else {
- processShowStyle()
+ if (options.runtimeCompile) {
+ processShowStyleDynamic(el, show)
+ } else {
+ processShowStyle(el, show)
+ }
}
+}
- function processShowStyle () {
- if (show !== undefined) {
- const showExp = parseMustacheWithContext(show).result
- let oldStyle = getAndRemoveAttr(el, 'style').val
- oldStyle = oldStyle ? oldStyle + ';' : ''
- addAttrs(el, [{
- name: 'style',
- value: `${oldStyle}{{${showExp}?'':'display:none;'}}`
- }])
- }
+function processShowStyle (el, show) {
+ if (show !== undefined) {
+ const showExp = parseMustacheWithContext(show).result
+ let oldStyle = getAndRemoveAttr(el, 'style').val
+ oldStyle = oldStyle ? oldStyle + ';' : ''
+ addAttrs(el, [{
+ name: 'style',
+ value: `${oldStyle}{{${showExp}?'':'display:none;'}}`
+ }])
}
}
@@ -2201,11 +2233,11 @@ function processDuplicateAttrsList (el) {
}
// 处理wxs注入逻辑
-function processInjectWxs (el, meta) {
+function processInjectWxs (el, meta, options) {
if (el.injectWxsProps && el.injectWxsProps.length) {
el.injectWxsProps.forEach((injectWxsProp) => {
const { injectWxsPath, injectWxsModuleName } = injectWxsProp
- injectWxs(meta, injectWxsModuleName, injectWxsPath)
+ injectWxs(meta, injectWxsModuleName, injectWxsPath, options)
})
}
}
@@ -2243,7 +2275,7 @@ function processElement (el, root, options, meta) {
processMpxTagName(el)
- processInjectWxs(el, meta)
+ processInjectWxs(el, meta, options)
const transAli = mode === 'ali' && srcMode === 'wx'
@@ -2271,8 +2303,13 @@ function processElement (el, root, options, meta) {
processIf(el)
processFor(el)
processRef(el, options, meta)
- processClass(el, meta)
- processStyle(el, meta)
+ if (options.runtimeCompile) {
+ processClassDynamic(el, meta)
+ processStyleDynamic(el, meta)
+ } else {
+ processClass(el, meta)
+ processStyle(el, meta)
+ }
processEvent(el, options)
if (!pass) {
@@ -2285,6 +2322,8 @@ function processElement (el, root, options, meta) {
function closeElement (el, meta, options) {
postProcessAtMode(el)
+ collectDynamicInfo(el, options, meta)
+
if (mode === 'web') {
postProcessWxs(el, meta)
// 处理代码维度条件编译移除死分支
@@ -2295,12 +2334,24 @@ function closeElement (el, meta, options) {
postProcessWxs(el, meta)
if (!pass) {
if (isComponentNode(el, options) && !hasVirtualHost && mode === 'ali') {
- postProcessAliComponentRootView(el, options)
+ postProcessAliComponentRootView(el, options, meta)
}
- postProcessComponentIs(el)
+ postProcessComponentIs(el, options)
+ }
+
+ if (options.runtimeCompile) {
+ postProcessForDynamic(el, config[mode])
+ postProcessIfDynamic(el, config[mode])
+ postProcessAttrsDynamic(el, config[mode])
+ } else {
+ postProcessFor(el)
+ postProcessIf(el)
}
- postProcessFor(el)
- postProcessIf(el)
+}
+
+// 运行时组件的模版节点收集,最终注入到 mpx-custom-element-*.wxml 中
+function collectDynamicInfo (el, options, meta) {
+ setBaseWxml(el, { mode, isComponentNode, options }, meta)
}
function postProcessAtMode (el) {
@@ -2331,7 +2382,7 @@ function cloneAttrsList (attrsList) {
})
}
-function postProcessComponentIs (el) {
+function postProcessComponentIs (el, options) {
if (el.is && el.components) {
let tempNode
if (el.for || el.if || el.elseif || el.else) {
@@ -2340,7 +2391,7 @@ function postProcessComponentIs (el) {
tempNode = getTempNode()
}
replaceNode(el, tempNode, true)
- postMoveBaseDirective(tempNode, el)
+ postMoveBaseDirective(tempNode, el, options)
el.components.forEach(function (component) {
const newChild = createASTElement(component, cloneAttrsList(el.attrsList), tempNode)
@@ -2550,6 +2601,150 @@ function genNode (node) {
return exp
}
+function addIfConditionDynamic (el, condition) {
+ if (!el.ifConditions) {
+ el.ifConditions = []
+ }
+ el.ifConditions.push(condition)
+}
+
+function processIfConditionsDynamic (el) {
+ const prevNode = findPrevNode(el)
+ if (prevNode && prevNode.if) {
+ addIfConditionDynamic(prevNode, {
+ ifExp: !!el.elseif,
+ block: el,
+ __exp: el.elseif ? parseExp(el.elseif.exp) : ''
+ })
+ removeNode(el)
+ }
+}
+
+function processClassDynamic (el, meta) {
+ const type = 'class'
+ const targetType = type
+ const dynamicClass = getAndRemoveAttr(el, config[mode].directive.dynamicClass).val
+ let staticClass = getAndRemoveAttr(el, type).val || ''
+ staticClass = staticClass.replace(/\s+/g, ' ')
+ if (dynamicClass) {
+ const staticClassExp = parseMustacheWithContext(staticClass).result
+ const dynamicClassExp = transDynamicClassExpr(parseMustacheWithContext(dynamicClass).result, {
+ error: error$1
+ })
+ addAttrs(el, [{
+ name: targetType,
+ value: `{{[${staticClassExp},${dynamicClassExp}]}}`
+ }])
+ } else if (staticClass) {
+ addAttrs(el, [{
+ name: targetType,
+ value: staticClass
+ }])
+ }
+}
+
+function processStyleDynamic (el, meta) {
+ const type = 'style'
+ const targetType = type
+ const dynamicStyle = getAndRemoveAttr(el, config[mode].directive.dynamicStyle).val
+ let staticStyle = getAndRemoveAttr(el, type).val || ''
+ staticStyle = staticStyle.replace(/\s+/g, ' ')
+ if (dynamicStyle) {
+ const staticStyleExp = parseMustacheWithContext(staticStyle).result
+ const dynamicStyleExp = parseMustacheWithContext(dynamicStyle).result
+ addAttrs(el, [{
+ name: targetType,
+ value: `{{[${staticStyleExp},${dynamicStyleExp}]}}`
+ }])
+ } else if (staticStyle) {
+ addAttrs(el, [{
+ name: targetType,
+ value: staticStyle
+ }])
+ }
+}
+
+function processTextDynamic (vnode) {
+ if (vnode.type !== 3 || vnode.isComment) {
+ return
+ }
+ const parsed = parseMustacheWithContext(vnode.text)
+ if (parsed.hasBinding) {
+ vnode.__exp = parseExp(parsed.result)
+ delete vnode.text
+ }
+}
+
+function postProcessIfDynamic (vnode, config) {
+ if (vnode.if) {
+ const parsedExp = vnode.if.exp
+ addIfConditionDynamic(vnode, {
+ ifExp: true,
+ block: 'self',
+ __exp: parseExp(parsedExp)
+ })
+ getAndRemoveAttr(vnode, config.directive.if)
+ vnode.if = true
+ } else if (vnode.elseif || vnode.else) {
+ const directive = vnode.elseif
+ ? config.directive.elseif
+ : config.directive.else
+ getAndRemoveAttr(vnode, directive)
+ processIfConditionsDynamic(vnode)
+ delete vnode.elseif
+ delete vnode.else
+ }
+}
+
+function postProcessForDynamic (vnode) {
+ if (vnode.for) {
+ vnode.for.__exp = parseExp(vnode.for.exp)
+ delete vnode.for.raw
+ delete vnode.for.exp
+ popForScopes()
+ }
+}
+
+function postProcessAttrsDynamic (vnode, config) {
+ const exps = vnode.exps?.filter(v => v.attrName) || []
+ const expsMap = Object.fromEntries(exps.map(v => ([v.attrName, v])))
+ const directives = Object.values(config.directive)
+ if (vnode.attrsList && vnode.attrsList.length) {
+ // 后序遍历,主要为了做剔除的操作
+ for (let i = vnode.attrsList.length - 1; i >= 0; i--) {
+ const attr = vnode.attrsList[i]
+ if (config.event.parseEvent(attr.name) || directives.includes(attr.name)) {
+ // 原本的事件代理直接剔除,主要是基础模版的事件直接走代理形式,事件绑定名直接写死的,优化 astJson 体积
+ getAndRemoveAttr(vnode, attr.name)
+ } else if (attr.value == null) {
+ attr.__exp = parseExp('true')
+ } else {
+ const expInfo = expsMap[attr.name]
+ if (expInfo && expInfo.exp) {
+ attr.__exp = parseExp(expInfo.exp)
+ }
+ }
+ if (attr.__exp) {
+ delete attr.value
+ }
+ }
+ }
+}
+
+function processShowStyleDynamic (el, show) {
+ if (show !== undefined) {
+ const showExp = parseMustacheWithContext(show).result
+ const oldStyle = getAndRemoveAttr(el, 'style').val
+ const displayExp = `${showExp}? '' : "display:none;"`
+ const isArray = oldStyle?.endsWith(']}}')
+ const value = isArray ? oldStyle?.replace(']}}', `,${displayExp}]}}`) : `${oldStyle ? `${oldStyle};` : ''}{{${displayExp}}}`
+ addAttrs(el, [{
+ name: 'style',
+ value: value
+ }])
+ }
+}
+
module.exports = {
parseComponent,
parse,
@@ -2560,5 +2755,10 @@ module.exports = {
parseMustache,
parseMustacheWithContext,
stringifyWithResolveComputed,
- addAttrs
+ addAttrs,
+ getAndRemoveAttr,
+ findPrevNode,
+ removeNode,
+ replaceNode,
+ createASTElement
}
diff --git a/packages/webpack-plugin/lib/template-compiler/dynamic.js b/packages/webpack-plugin/lib/template-compiler/dynamic.js
new file mode 100644
index 0000000000..84a04ecdb1
--- /dev/null
+++ b/packages/webpack-plugin/lib/template-compiler/dynamic.js
@@ -0,0 +1,13 @@
+const uselessAttrs = ['parent', 'exps', 'unary', 'attrsMap']
+const uselessArrAttrs = ['children', 'attrsList']
+
+function stringify (ast) {
+ return JSON.stringify(ast, (k, v) => {
+ if (uselessAttrs.includes(k)) return undefined
+ if (uselessArrAttrs.includes(k) && v && !v.length) return undefined
+ if (k === 'tag' && v === 'temp-node') return 'block'
+ return v
+ })
+}
+
+module.exports.stringify = stringify
diff --git a/packages/webpack-plugin/lib/template-compiler/index.js b/packages/webpack-plugin/lib/template-compiler/index.js
index 5eb6cacbdb..e8148558e4 100644
--- a/packages/webpack-plugin/lib/template-compiler/index.js
+++ b/packages/webpack-plugin/lib/template-compiler/index.js
@@ -3,6 +3,10 @@ const bindThis = require('./bind-this')
const parseRequest = require('../utils/parse-request')
const { matchCondition } = require('../utils/match-condition')
const loaderUtils = require('loader-utils')
+const { MPX_DISABLE_EXTRACTOR_CACHE } = require('../utils/const')
+const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency')
+const { createTemplateEngine, createSetupTemplate } = require('@mpxjs/template-engine')
+const { stringify } = require('./dynamic')
module.exports = function (raw) {
this.cacheable()
@@ -17,6 +21,7 @@ module.exports = function (raw) {
const decodeHTMLText = mpx.decodeHTMLText
const globalSrcMode = mpx.srcMode
const localSrcMode = queryObj.mode
+ const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main'
const wxsContentMap = mpx.wxsContentMap
const optimizeRenderRules = mpx.optimizeRenderRules
const usingComponents = queryObj.usingComponents || []
@@ -25,6 +30,7 @@ module.exports = function (raw) {
const isNative = queryObj.isNative
const ctorType = queryObj.ctorType
const hasScoped = queryObj.hasScoped
+ const runtimeCompile = queryObj.isDynamic
const moduleId = queryObj.moduleId || '_' + mpx.pathHash(resourcePath)
let optimizeRenderLevel = 0
@@ -49,6 +55,7 @@ module.exports = function (raw) {
const { root: ast, meta } = compiler.parse(raw, {
warn,
error,
+ runtimeCompile,
usingComponents,
componentPlaceholder,
hasComment,
@@ -67,7 +74,7 @@ module.exports = function (raw) {
i18n,
checkUsingComponents: matchCondition(resourcePath, mpx.checkUsingComponentsRules),
globalComponents: Object.keys(mpx.usingComponents),
- forceProxyEvent: matchCondition(resourcePath, mpx.forceProxyEventRules),
+ forceProxyEvent: matchCondition(resourcePath, mpx.forceProxyEventRules) || runtimeCompile,
hasVirtualHost: matchCondition(resourcePath, mpx.autoVirtualHostRules)
})
@@ -77,7 +84,7 @@ module.exports = function (raw) {
}
}
- const result = compiler.serialize(ast)
+ let result = runtimeCompile ? '' : compiler.serialize(ast)
if (isNative) {
return result
@@ -95,7 +102,11 @@ global.currentInject = {
moduleId: ${JSON.stringify(moduleId)}
};\n`
- const rawCode = compiler.genNode(ast)
+ if (runtimeCompile || queryObj.dynamicRuntime) {
+ resultSource += 'global.currentInject.dynamic = true;\n'
+ }
+
+ const rawCode = runtimeCompile ? '' : compiler.genNode(ast)
if (rawCode) {
try {
const ignoreMap = Object.assign({
@@ -157,5 +168,26 @@ global.currentInject.getRefsData = function () {
extractedResultSource: resultSource
})
+ if (queryObj.mpxCustomElement) {
+ this.cacheable(false)
+ const templateEngine = createTemplateEngine(mpx.mode)
+ result += `${createSetupTemplate()}\n` + templateEngine.buildTemplate(mpx.getPackageInjectedTemplateConfig(packageName))
+ }
+
+ // 运行时编译的组件直接返回基础模板的内容,并产出动态文本内容
+ if (runtimeCompile) {
+ // 包含了运行时组件的template模块必须每次都创建(但并不是每次都需要build),用于收集组件节点信息,传递信息以禁用父级extractor的缓存
+ this.emitFile(MPX_DISABLE_EXTRACTOR_CACHE, '', undefined, { skipEmit: true })
+
+ const templateInfo = {
+ templateAst: stringify(ast),
+ ...meta.runtimeInfo
+ }
+
+ // 以 package 为维度存储,meta 上的数据也只是存储了这个组件的 template 上获取的信息,需要在 dependency 里面再次进行合并操作
+ this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'template', info: templateInfo }))
+ // 运行时组件的模版直接返回空,在生成模版静态文件的时候(beforeModuleAssets)再动态注入
+ }
+
return result
}
diff --git a/packages/webpack-plugin/lib/template-compiler/parse-exps.js b/packages/webpack-plugin/lib/template-compiler/parse-exps.js
new file mode 100644
index 0000000000..1c1210f3d2
--- /dev/null
+++ b/packages/webpack-plugin/lib/template-compiler/parse-exps.js
@@ -0,0 +1,148 @@
+// todo 待讨论 parser + interpreter 的部分是否需要单独抽个 package 出去
+const acorn = require('acorn')
+const walk = require('acorn-walk')
+
+/**
+ * 基于目前小程序所支持的模版语法实现,对于不支持的语法在编译阶段直接报错
+ */
+
+const NODE_TYPE = {
+ Program: 1,
+ Identifier: 2,
+ Literal: 3,
+ ArrayExpression: 28,
+ ObjectExpression: 29,
+ Property: 31,
+ UnaryExpression: 33,
+ // UpdateExpression: 34,
+ BinaryExpression: 35,
+ LogicalExpression: 37,
+ MemberExpression: 38,
+ ConditionalExpression: 39,
+ ExpressionStatement: 40
+}
+
+const error = function (msg) {
+ throw new Error(`[Mpx dynamic expression parser error]: ${msg}`)
+}
+
+walk.full = function full (node, baseVisitor, state, override) {
+ const stack = []
+ ; (function c (node, st, override, s) {
+ const type = override || node.type
+ if (!baseVisitor[type]) {
+ error(`${type} grammar is not supported in the template`)
+ }
+ baseVisitor[type](node, st, c, s)
+ })(node, state, override, stack)
+
+ // 限定 bodyStack 长度,仅支持单表达式的写法
+ const bodyStackIndex = 1
+ if (stack[bodyStackIndex].length > 1) {
+ error('only support one expression in the template')
+ }
+ return stack
+}
+
+const baseVisitor = {}
+
+baseVisitor.UnaryExpression = function (node, st, c, s) {
+ // const nodeType = node.type === 'UnaryExpression' ? NODE_TYPE.UnaryExpression : NODE_TYPE.UpdateExpression
+ const nodeType = NODE_TYPE.UnaryExpression
+ const argumentNodeStack = []
+ s.push(nodeType, node.operator, argumentNodeStack, node.prefix)
+ c(node.argument, st, 'Expression', argumentNodeStack)
+}
+
+baseVisitor.BinaryExpression = baseVisitor.LogicalExpression = function (node, st, c, s) {
+ const nodeType = node.type === 'BinaryExpression' ? NODE_TYPE.BinaryExpression : NODE_TYPE.LogicalExpression
+ const leftNodeStack = []
+ const rightNodeStack = []
+ // todo operator 可以严格按照小程序模版能力进行限制
+ s.push(nodeType, node.operator, leftNodeStack, rightNodeStack)
+ c(node.left, st, 'Expression', leftNodeStack)
+ c(node.right, st, 'Expression', rightNodeStack)
+}
+
+baseVisitor.ConditionalExpression = (node, st, c, s) => {
+ const testNodeStack = []
+ const consequentNodeStack = []
+ const alternateNodeStack = []
+ s.push(NODE_TYPE.ConditionalExpression, testNodeStack, consequentNodeStack, alternateNodeStack)
+ c(node.test, st, 'Expression', testNodeStack)
+ c(node.consequent, st, 'Expression', consequentNodeStack)
+ c(node.alternate, st, 'Expression', alternateNodeStack)
+}
+
+const visitor = walk.make({
+ Program (node, st, c, s) {
+ const bodyStack = []
+ s.push(NODE_TYPE.Program, bodyStack)
+ for (let i = 0, list = node.body; i < list.length; i += 1) {
+ const stmt = list[i]
+ bodyStack[i] = []
+ c(stmt, st, 'Statement', bodyStack[i])
+ }
+ },
+ ExpressionStatement (node, st, c, s) {
+ const expressionStack = []
+ s.push(NODE_TYPE.ExpressionStatement, expressionStack)
+ c(node.expression, st, null, expressionStack)
+ },
+ MemberExpression (node, st, c, s) {
+ const objectNodeStack = []
+ const propertyNodeStack = []
+ s.push(NODE_TYPE.MemberExpression, objectNodeStack, propertyNodeStack, node.computed)
+ c(node.object, st, 'Expression', objectNodeStack)
+ c(node.property, st, 'Expression', propertyNodeStack)
+ },
+ ArrayExpression (node, st, c, s) {
+ const elementsStack = []
+ s.push(NODE_TYPE.ArrayExpression, elementsStack)
+ node.elements.forEach((elt, index) => {
+ if (elt) {
+ elementsStack[index] = []
+ c(elt, st, 'Expression', elementsStack[index])
+ }
+ })
+ },
+ ObjectExpression (node, st, c, s) {
+ const propertiesStack = []
+ s.push(NODE_TYPE.ObjectExpression, propertiesStack)
+ node.properties.forEach((prop, index) => {
+ propertiesStack[index] = []
+ c(prop, st, null, propertiesStack[index])
+ })
+ },
+ Property (node, st, c, s) {
+ const keyNodeStack = []
+ const valueNodeStack = []
+ s.push(NODE_TYPE.Property, keyNodeStack, valueNodeStack, node.kind)
+ c(node.key, st, 'Expression', keyNodeStack)
+ c(node.value, st, 'Expression', valueNodeStack)
+ },
+ Literal (node, st, c, s) {
+ // todo node.raw/-1 目前应该都用不到,后续可以优化
+ s.push(NODE_TYPE.Literal, node.value, node.raw, -1) // -1?
+ },
+ Identifier (node, st, c, s) {
+ s.push(NODE_TYPE.Identifier, node.name)
+ },
+ Expression (node, st, c, s) {
+ c(node, st, null, s)
+ },
+ Statement (node, st, c, s) {
+ c(node, st, null, s)
+ }
+}, baseVisitor)
+
+module.exports = {
+ parseExp (str) {
+ // 确保 str 都是为 expressionStatement
+ if (!/^\(*\)$/.test(str)) {
+ str = `(${str})`
+ }
+ return walk.full(acorn.parse(str, { ecmaVersion: 5 }), visitor)
+ },
+ NODE_TYPE
+}
diff --git a/packages/webpack-plugin/lib/utils/resolve-mpx-custom-element-path.js b/packages/webpack-plugin/lib/utils/resolve-mpx-custom-element-path.js
new file mode 100644
index 0000000000..ec1622988b
--- /dev/null
+++ b/packages/webpack-plugin/lib/utils/resolve-mpx-custom-element-path.js
@@ -0,0 +1,7 @@
+module.exports = function (packageName) {
+ let subPath = ''
+ if (packageName !== 'main') {
+ subPath = '/' + packageName
+ }
+ return subPath + `/mpx-custom-element-${packageName}`
+}
diff --git a/packages/webpack-plugin/lib/wxml/loader.js b/packages/webpack-plugin/lib/wxml/loader.js
index 9318bf0708..c3daca64ee 100644
--- a/packages/webpack-plugin/lib/wxml/loader.js
+++ b/packages/webpack-plugin/lib/wxml/loader.js
@@ -5,6 +5,7 @@ const config = require('../config')
const createHelpers = require('../helpers')
const isUrlRequest = require('../utils/is-url-request')
const parseRequest = require('../utils/parse-request')
+const genDynamicTemplate = require('../runtime-render/gen-dynamic-template')
let count = 0
@@ -26,6 +27,14 @@ module.exports = function (content) {
const mode = mpx.mode
const localSrcMode = queryObj.mode
const customAttributes = options.attributes || mpx.attributes || []
+ const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main'
+ const isDynamic = queryObj.isDynamic
+
+ const exportsString = 'module.exports = '
+
+ if (isDynamic) {
+ return exportsString + JSON.stringify(genDynamicTemplate(packageName)) + ';'
+ }
const { getRequestString } = createHelpers(this)
@@ -71,8 +80,6 @@ module.exports = function (content) {
content = content.join('')
content = JSON.stringify(content)
- const exportsString = 'module.exports = '
-
return exportsString + content.replace(/__HTMLLINK__\d+__/g, (match) => {
if (!data[match]) return match
diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json
index 8cdfe00502..70cfffb198 100644
--- a/packages/webpack-plugin/package.json
+++ b/packages/webpack-plugin/package.json
@@ -27,7 +27,9 @@
"@better-scroll/slide": "^2.5.1",
"@better-scroll/wheel": "^2.5.1",
"@better-scroll/zoom": "^2.5.1",
+ "@mpxjs/template-engine": "^2.8.7",
"acorn-walk": "^7.2.0",
+ "acorn": "^8.11.3",
"async": "^2.6.0",
"css": "^2.2.1",
"css-selector-tokenizer": "^0.7.0",
diff --git a/test/e2e/miniprogram-project/package.json b/test/e2e/miniprogram-project/package.json
index 2bb89f67b6..41752db03a 100644
--- a/test/e2e/miniprogram-project/package.json
+++ b/test/e2e/miniprogram-project/package.json
@@ -42,6 +42,7 @@
"@mpxjs/miniprogram-simulate": "1.4.9",
"@mpxjs/mpx-jest": "0.0.27",
"@mpxjs/webpack-plugin": "^2.7.2",
+ "@mpxjs/template-engine": "^2.8.7",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
"babel-eslint": "^10.0.1",
diff --git a/test/e2e/miniprogram-wxss-loader/package.json b/test/e2e/miniprogram-wxss-loader/package.json
index 3f63814b8f..2263a398d8 100644
--- a/test/e2e/miniprogram-wxss-loader/package.json
+++ b/test/e2e/miniprogram-wxss-loader/package.json
@@ -44,6 +44,7 @@
"@mpxjs/miniprogram-simulate": "1.4.9",
"@mpxjs/mpx-jest": "0.0.27",
"@mpxjs/webpack-plugin": "^2.7.55",
+ "@mpxjs/template-engine": "^2.8.7",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
"babel-eslint": "^10.0.1",
diff --git a/test/e2e/plugin-project/package.json b/test/e2e/plugin-project/package.json
index a4d6f7f912..9b14f1420f 100644
--- a/test/e2e/plugin-project/package.json
+++ b/test/e2e/plugin-project/package.json
@@ -43,6 +43,7 @@
"@mpxjs/miniprogram-simulate": "^1.4.17",
"@mpxjs/mpx-jest": "0.0.27",
"@mpxjs/webpack-plugin": "^2.7.2",
+ "@mpxjs/template-engine": "^2.8.7",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
"autoprefixer": "^6.3.1",