first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
const path = require('path')
const fs = require('fs-extra')
const uniI18n = require('@dcloudio/uni-cli-i18n')
const validate = require('./validate')
const patchVant = require('./vant')
const migraters = {
'mp-weixin': require('./mp-weixin')
}
module.exports = function migrate (input, out, options = {}) {
options.platform = options.platform || 'mp-weixin'
const migrater = migraters[options.platform]
if (!migrater) {
return console.error(uniI18n.__('migration.errorOnlySupportConvert', { 0: Object.keys(migraters).join(',') }))
}
input = path.resolve(input)
out = path.resolve(out || input)
if (!validate(input, out, options)) {
return
}
const [files, assets] = migrater.transform(input, out, options)
files.forEach(file => {
options.silent !== true && console.log(`write: ${file.path}`)
fs.outputFileSync(file.path, file.content)
})
const styleExtname = options.extname.style
const needCopy = input !== out
assets.forEach(asset => {
if (typeof asset === 'string') {
const src = path.resolve(input, asset)
const dest = path.resolve(out, asset.replace(styleExtname, '.css'))
if (
needCopy || (
asset.indexOf(styleExtname) !== -1 &&
styleExtname !== '.css'
)
) {
options.silent !== true && console.log(`copy: ${dest}`)
try {
fs.copySync(src, dest)
} catch (e) {
// ignore Source and destination must not be the same
}
}
} else {
options.silent !== true && console.log(`write: ${path.resolve(out, asset.path)}`)
fs.outputFileSync(path.resolve(out, asset.path), asset.content)
}
})
patchVant(files, assets, out)
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
options: {
extname: {
template: '.wxml',
style: '.wxss'
}
},
transform: require('./transform')
}
@@ -0,0 +1,69 @@
const path = require('path')
const {
transformJsonFile
} = require('./json-transformer')
const {
transformTemplateFile
} = require('./template-transformer')
const {
transformStyleFile
} = require('./style-transformer')
const {
transformScriptFile
} = require('./script-transformer')
const {
normalizePath
} = require('../../util')
const pkg = require('../../../package.json')
module.exports = function transformFile(input, options) {
const {
template: templateExtname,
style: styleExtname
} = options.extname
const filepath = input.replace(templateExtname, '')
const deps = [
filepath + templateExtname
]
const [jsCode, isComponent] = transformJsonFile(filepath + '.json', deps)
options.isComponent = isComponent
options.filepath = filepath
options.filename = path.basename(filepath)
if (options.base) {
options.route = normalizePath(path.relative(options.base, filepath))
} else {
options.route = options.filename
}
options.shadowRootHost = options.route.replace(/\//g, '-')
const [templateCode, wxsCode = '', wxsFiles = []] = transformTemplateFile(filepath + templateExtname, options)
const styleCode = transformStyleFile(filepath + styleExtname, options, deps) || ''
const scriptCode = transformScriptFile(filepath + '.js', jsCode, options, deps)
const commentsCode = options.silent ? '' :
`<!-- @dcloudio/uni-migration@${pkg.version} -->
<!-- ${new Date().toLocaleString()} -->
`
return [
`${commentsCode}<template>
${templateCode}
</template>
${wxsCode}
<script>
${scriptCode}
</script>
<style platform="mp-weixin">
${styleCode}
</style>`,
deps,
wxsFiles
]
}
@@ -0,0 +1,22 @@
const fs = require('fs')
const path = require('path')
const parse = require('./template-transformer/parser')
function getTemplate (content) {
const template = []
const node = parse(content)
node.children.forEach(node => {
if (node.name === 'template') {
const name = node.attribs.name
if (name) {
template.push(name)
}
}
})
return template
}
module.exports = function (filepath, options) {
filepath = path.join(path.dirname(options.filepath), filepath)
return getTemplate(fs.readFileSync(filepath, 'utf8').toString().trim())
}
+79
View File
@@ -0,0 +1,79 @@
const path = require('path')
const glob = require('glob')
const transformFile = require('./file-transformer')
function generateVueFile(input, out, options) {
try {
const [content, deps, wxsFiles] = transformFile(input, options)
return {
path: path.resolve(out, path.basename(input).replace(options.extname.template, '.vue')),
content,
deps,
wxsFiles
}
} catch (e) {
console.error(input)
throw e
}
}
function generateVueFolder(input, out, options) {
const extname = options.extname.template
const files = []
const assets = []
const deps = []
glob.sync('**/*', {
cwd: input,
nodir: true
}).map(file => {
if (path.extname(file) === extname) {
const vueFile = generateVueFile(
path.resolve(input, file),
path.dirname(path.resolve(out, file)),
options
)
files.push(vueFile)
deps.push(...vueFile.deps)
const dirname = path.dirname(file)
vueFile.wxsFiles.forEach(wxsFile => {
wxsFile.path = path.join(dirname, wxsFile.path)
assets.push(wxsFile)
})
} else {
assets.push(file)
}
})
return [files, assets.filter(asset => {
if (typeof asset === 'string') {
return !deps.includes(path.resolve(input, asset))
}
return true
})]
}
function generateVueApp(input, out, options) {
console.error(`暂不支持转换整个 App`)
return [
[],
[]
]
}
module.exports = function transform(input, out, options) {
switch (options.target) {
case 'file':
return [
[generateVueFile(input, out, options)],
[]
]
case 'folder':
return generateVueFolder(input, out, options)
case 'app':
return generateVueApp(input, out, options)
}
return [
[],
[]
]
}
@@ -0,0 +1,38 @@
const fs = require('fs')
const {
camelize,
capitalize
} = require('../../util')
function transformJson(content) {
const {
component,
usingComponents
} = JSON.parse(content)
if (!usingComponents) {
return ['']
}
const importCode = []
const componentsCode = []
Object.keys(usingComponents).forEach(name => {
const identifier = capitalize(camelize(name))
importCode.push(`import ${identifier} from '${usingComponents[name]}.vue'`)
componentsCode.push(`'${name}': ${identifier}`)
})
return [`${importCode.join('\n')}
global['__wxVueOptions'] = {components:{${componentsCode.join(',')}}}
`, component]
}
module.exports = {
transformJson,
transformJsonFile(filepath, deps) {
if (!fs.existsSync(filepath)) {
return ['']
}
deps.push(filepath)
return transformJson(fs.readFileSync(filepath, 'utf8').toString().trim())
}
}
@@ -0,0 +1,54 @@
const fs = require('fs')
const importTemplate = require('./import-template')
function transformScript (content, route, code) {
return `${code}
global['__wxRoute'] = '${route}'
${content}
export default global['__wxComponents']['${route}']`
}
function genJsCode(components, code, state) {
const wxTemplateComponentProps = '__wxTemplateComponentProps'
const props = state.props
const importCode = []
const propsCode = []
const componentsCode = []
components.forEach((node, index) => {
const src = node.attribs.src
const templates = importTemplate(src, state)
const identifier = `__wxTemplateComponent${index}`
importCode.push(`import ${identifier} from '${src.replace(/.wxml$/, '.vue')}'`)
templates.forEach(template => {
// TODO 改为在 template 编译时静态分析
propsCode.push(`${wxTemplateComponentProps}['${template}'] && ${wxTemplateComponentProps}['${template}'].forEach(prop => ${identifier}.props[prop] = {type: null})`)
componentsCode.push(`'${template}' : ${identifier}`)
})
})
return components.length ? `
const ${wxTemplateComponentProps} = ${JSON.stringify(props)}
${importCode.join('\n')}
${propsCode.join('\n')}
${code.trim().replace(/\}\}$/, '')},${componentsCode.join(',')}}}
`: code
}
module.exports = {
transformScript,
transformScriptFile(filepath, code, options, deps) {
let content = ''
if (options.components.length) {
code = genJsCode(options.components, code, options)
}
if (!fs.existsSync(filepath)) {
content = `
Component({})
`
} else {
content = fs.readFileSync(filepath, 'utf8').toString().trim()
deps.push(filepath)
}
return transformScript(content, options.route, code, options)
}
}
@@ -0,0 +1,17 @@
const fs = require('fs')
function transformStyle(content, options) {
return content.replace(new RegExp(`\\${options.extname.style}`, 'g'), '.css')
.replace(':host', '.' + options.shadowRootHost)
}
module.exports = {
transformStyle,
transformStyleFile(filepath, options, deps) {
if (!fs.existsSync(filepath)) {
return ''
}
deps.push(filepath)
return transformStyle(fs.readFileSync(filepath, 'utf8').toString().trim(), options)
}
}
@@ -0,0 +1,16 @@
const fs = require('fs')
const path = require('path')
const parse = require('./parser')
const transform = require('./transform')
function transformTemplate(content, options = {}) {
return transform(parse(content), options)
}
module.exports = {
transformTemplate,
transformTemplateFile(filepath, options = {}) {
return transformTemplate(fs.readFileSync(filepath, 'utf8').toString().trim(), options)
}
}
@@ -0,0 +1,21 @@
const {
Parser,
DomHandler
} = require('stricter-htmlparser2')
module.exports = function parse(sourceCode) {
const handler = new DomHandler()
new Parser(handler, {
xmlMode: false,
lowerCaseAttributeNames: false,
recognizeSelfClosing: true,
lowerCaseTags: false
}).end(sourceCode)
return {
type: 'tag',
name: 'root',
attribs: {},
children: Array.isArray(handler.dom) ? handler.dom : [handler.dom]
}
}
@@ -0,0 +1,65 @@
const BOOL_ATTRS = [
'v-else'
]
function genAttrs(node) {
const attribs = node.attribs
const attribsArr = Object.keys(attribs).map(name => {
if (BOOL_ATTRS.includes(name) || attribs[name] === '') { // boolean attribute
return name
}
return `${name}="${attribs[name]}"`
})
if (!attribsArr.length) {
return ''
}
return ' ' + attribsArr.join(' ')
}
function genChildren(node) {
if (!node.children) {
return ''
}
return node.children.map(childNode => genElement(childNode)).join('')
}
function genElement(node) {
if (node.type === 'text') {
return node.data
} else if (node.type === 'tag') {
const name = node.name
return `<${name}${genAttrs(node)}>${genChildren(node)}</${name}>`
}
return ''
}
function genWxs(wxs, state) {
const wxsCode = []
const wxsFiles = []
wxs.forEach(wxsNode => {
const {
src,
module
} = wxsNode.attribs
if (!module) {
return
}
if (!src) {
wxsNode.attribs.src = './' + (state.filename ? (state.filename + '-' + module) : module) + '.wxs'
wxsFiles.push({
path: wxsNode.attribs.src,
content: genChildren(wxsNode)
})
}
wxsNode.children.length = 0
wxsCode.push(genElement(wxsNode))
})
return [wxsCode.join('').trim(), wxsFiles]
}
module.exports = function generate(node, state) {
return [
`<uni-shadow-root${state.shadowRootHost?(` class="${state.shadowRootHost}"`):''}>${genChildren(node).trim()}</uni-shadow-root>`,
...genWxs(state.wxs, state)
]
}
@@ -0,0 +1,14 @@
const traverse = require('./traverse')
const generate = require('./generate')
module.exports = function transform(ast, options) {
options.wxs = []
// wxml 中使用 import 导入的组件
options.components = []
// wxml 中使用 <template name> 声明的模板
options.templates = []
// wxml 中 <template is> 分析得到的 props
options.props = {}
options.shouldWrapper = options.shouldWrapper || function noop () { }
return generate(traverse(ast, options), options)
}
@@ -0,0 +1,270 @@
const {
parse
} = require('mustache')
const recast = require('recast')
const TAGS = [
'ad',
'audio',
'button',
'camera',
'canvas',
'checkbox',
'checkbox-group',
'cover-image',
'cover-view',
'editor',
'form',
'functional-page-navigator',
'icon',
'image',
'input',
'label',
'live-player',
'live-pusher',
'map',
'movable-area',
'movable-view',
'navigator',
'official-account',
'open-data',
'picker',
'picker-view',
'picker-view-column',
'progress',
'radio',
'radio-group',
'rich-text',
'scroll-view',
'slider',
'swiper',
'swiper-item',
'switch',
'text',
'textarea',
'video',
'view',
'web-view',
]
const EVENTS = {
'touchstart': 'touchstart',
'touchmove': 'touchmove',
'touchcancel': 'touchcancel',
'touchend': 'touchend',
'tap': 'click',
'longpress': 'longpress',
'longtap': 'longpress',
'transitionend': 'transitionend',
'animationstart': 'animationstart',
'animationiteration': 'animationiteration',
'animationend': 'animationend',
'touchforcechange': 'touchforcechange'
}
const ATTRS = {
'wx:if': 'v-if',
'wx:elif': 'v-else-if',
'wx:else': 'v-else'
}
const FOR = {
for: 'wx:for',
item: 'wx:for-item',
index: 'wx:for-index',
key: 'wx:key'
}
const FOR_DEFAULT = {
item: 'item',
index: 'index',
index_fallback: '___i___'
}
function parseMustache(expr, identifier = false) {
if (!expr) {
return ''
}
const tokens = parse(expr)
const isIdentifier = tokens.length === 1
return tokens.map(token => {
if (token[0] === 'text') {
if (identifier) {
return token[1]
}
return `'${token[1]}'`
} else if (token[0] === '!') { // {{ !loading }}
return `(!${token[1]})`
} else if (token[0] === 'name') {
if (isIdentifier) {
return token[1]
}
return `(${token[1]})`
}
}).join('+')
}
function transformDirective(name, value, attribs) {
if (ATTRS[name]) {
attribs[ATTRS[name]] = parseMustache(value)
return true
}
}
function transformFor(attribs) {
const vFor = attribs[FOR.for]
if (!vFor) {
return
}
let vKey = parseMustache(attribs[FOR.key], true)
const vItem = parseMustache(attribs[FOR.item], true) || FOR_DEFAULT.item
const vIndex = parseMustache(attribs[FOR.index], true) || (
FOR_DEFAULT.index === vItem ? FOR_DEFAULT.index_fallback : FOR_DEFAULT.index
//处理 wx:for-item="index"
)
attribs['v-for'] = `(${vItem},${vIndex}) in (${parseMustache(vFor)})`
if (vKey) {
if (vKey === '*this') {
vKey = vItem
} else if (vKey !== vItem && vKey.indexOf('.') === -1) { // wx:for-key="{{item.value}}"
vKey = vItem + '.' + vKey
}
attribs[':key'] = vKey
}
delete attribs[FOR.for]
delete attribs[FOR.item]
delete attribs[FOR.index]
delete attribs[FOR.key]
}
const bindRE = /bind:?/
const catchRE = /catch:?/
const captureBindRE = /capture-bind:?/
const captureCatchRE = /capture-catch:?/
function transformEventName(name, state) {
if (state.isComponent) {
return '@' + (EVENTS[name] ? (EVENTS[name] + '.native') : name)
}
return '@' + (EVENTS[name] || name)
}
function transformEvent(name, value, attribs, state) {
let event = name
if (name.indexOf('bind') === 0) {
event = transformEventName(name.replace(bindRE, ''), state)
} else if (name.indexOf('catch') === 0) {
event = transformEventName(name.replace(catchRE, ''), state) + '.stop.prevent'
} else if (name.indexOf('capture-bind') === 0) {
event = transformEventName(name.replace(captureBindRE, ''), state) + '.capture'
} else if (name.indexOf('capture-catch') === 0) {
event = transformEventName(name.replace(captureCatchRE, ''), state) + '.stop.prevent.capture'
}
if (event !== name) {
// 模板 <template name> 中用到的方法在其父组件
let newValue = parseMustache(value, !state.isTemplate)
if (state.isTemplate) {
// TODO 改为运行时判断
newValue = `_$self.$parent${process.env.UNI_PLATFORM === 'h5' ? '.$parent' : ''}[(${newValue})]($event)`
} else if (newValue !== value) {
newValue = `_$self[(${newValue})||'_$noop']($event)`
}
attribs[event] = newValue
return true
}
}
function transformAttr(name, value, attribs, state) {
if (
name.indexOf('v-') === 0 ||
name.indexOf(':') === 0
) { // 已提前处理
return
}
delete attribs[name]
if (transformDirective(name, value, attribs)) {
return
}
if (transformEvent(name, value, attribs, state)) {
return
}
if (value.indexOf('{{') === -1) {
attribs[name] = value
return
}
attribs[':' + name] = parseMustache(value)
}
function transformAttrs(node, state) {
const attribs = node.attribs
if (!attribs) {
return
}
transformFor(attribs)
const isComponent = !TAGS.includes(node.name)
const isTemplate = state.templates.length
Object.keys(attribs).forEach(name => {
transformAttr(name, attribs[name], attribs, {
isComponent,
isTemplate
})
})
}
function transformChildren(node, state) {
node.children = node.children.filter(childNode => transformNode(childNode, state))
}
function transformTemplate(node, state) {
const attribs = node.attribs
if (attribs.name) {
const name = attribs.name
// 用于处理一个 wxml 文件内包含多个 template
attribs['v-if'] = `wxTemplateName === '${name}'`
delete attribs.name
state.templates.push(name)
} else if (attribs.is) {
const name = attribs.is
delete attribs.is
node.name = name
attribs['wx-template-name'] = name
const data = attribs.data
if (data && data.indexOf('{{') !== -1) {
const object = `{${parseMustache(data)}}`
attribs['v-bind'] = object
const ast = recast.parse(`const object = ${object}`)
const props = state.props[name] || ['wxTemplateName']
ast.program.body[0].declarations[0].init.properties.forEach(property => props.push(property.key.name))
state.props[name] = [...new Set(props)]
delete attribs.data
}
}
}
function transformNode(node, state) {
if (node.name === 'import') {
state.components.push(node)
return false
}
if (node.name === 'template') {
transformTemplate(node, state)
}
if (node.name === 'wxs') {
state.wxs.push(node)
return false
}
if (node.type === 'tag') {
transformAttrs(node, state)
transformChildren(node, state)
}
return true
}
module.exports = function traverse(node, state) {
transformNode(node, state)
return node
}
+22
View File
@@ -0,0 +1,22 @@
const isWin = /^win/.test(process.platform)
const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path)
const camelizeRE = /-(\w)/g
function camelize (str) {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}
/**
* Capitalize a string.
*/
function capitalize (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
module.exports = {
camelize,
capitalize,
normalizePath
}
+32
View File
@@ -0,0 +1,32 @@
const fs = require('fs')
const path = require('path')
const uniI18n = require('@dcloudio/uni-cli-i18n')
const migraters = {
'mp-weixin': require('./mp-weixin')
}
module.exports = function validate (input, out, options) {
if (!fs.existsSync(input)) {
return console.error(uniI18n.__('migration.errorInputNotExists', { 0: input }))
}
Object.assign(options, migraters[options.platform].options)
const templateExtname = options.extname.template
const stat = fs.lstatSync(input)
if (stat.isFile()) {
if (path.extname(input) !== templateExtname) {
return console.error(uniI18n.__('migration.errorConvertRequireFileUrl', { 0: templateExtname.substr(1) }))
}
options.target = 'file'
} else if (stat.isDirectory()) {
options.base = input
if (fs.existsSync(path.resolve(input, 'app.json'))) {
options.target = 'app'
} else {
options.target = 'folder'
}
} else {
return console.error(uniI18n.__('migration.errorCannotConvert', { 0: input }))
}
return true
}
+43
View File
@@ -0,0 +1,43 @@
const path = require('path')
const fs = require('fs-extra')
const {
normalizePath
} = require('./util')
module.exports = function patchVant (files, assets, out) {
files.forEach(file => {
const filepath = normalizePath(file.path)
let changed = false
if (filepath.indexOf('/image/index.vue') !== -1) {
changed = true
// onLoad 与 onError 是生命周期函数名,需要替换为其他
file.content = file.content
.replace(/onLoad/g, 'onImageLoad')
.replace(/onError/g, 'onImageError')
changed = true
} else if (filepath.indexOf('/notify/index.vue') !== -1) {
changed = true
// notify show方法与show属性冲突
file.content = file.content.replace('show()', 'showNotify()')
}
changed && fs.outputFileSync(file.path, file.content)
})
assets.forEach(asset => {
if (typeof asset === 'string') {
const dest = normalizePath(path.resolve(out, asset))
if (dest.indexOf('array.wxs') !== -1) {
// 兼容 Array.isArray
const content = fs.readFileSync(dest, 'utf8').toString()
.replace('array && array.constructor === \'Array\'',
'array && (array.constructor === \'Array\' || (typeof Array !== \'undefined\' && Array.isArray(array)))')
fs.outputFileSync(dest, content)
} else if (dest.indexOf('notify/notify.js') !== -1) {
// notify.js show 方法与 show 属性冲突
const content = fs.readFileSync(dest, 'utf8').toString()
.replace('show()', 'showNotify()')
fs.outputFileSync(dest, content)
}
}
})
}