tiny-vue/gulp/buildLowcodeBundle.mjs

354 lines
9.4 KiB
JavaScript
Raw Normal View History

import gulp from 'gulp'
import minimist from 'minimist'
import shell from 'shelljs'
import path from 'path'
import { generateKey } from 'crypto'
const build = gulp.series(init, read, write, clean)
build.description = '将本地文档的组件api, 转换为低代码需要的bundle.json 格式'
build.flags = {
'--apiPath': '设置api文档路径默认为 examples/sites/demos/pc',
'--tinyVer': '设置生成中tinyVer的版本号默认为 3.11.0'
}
export default build
const rootPath = process.cwd()
let apiPath = 'examples/sites/demos/pc'
let tinyVer = '3.11.0'
let guid = 1
let menus = []
const ignoreKeys = ['color', 'font', 'icon']
const mixinKeys = {
'form': [{ key: 'form-item', nameCn: '表单项', desc: '表单中的一行表单域对象' }],
'layout': [
{ key: 'row', nameCn: '行元素', desc: '行元素' },
{ key: 'col', nameCn: '列元素', desc: '列元素' }
],
'breadcrumb': [{ key: 'breadcrumb-item', nameCn: '面包屑项', desc: '面包屑的层级对象' }],
'timeline': [{ key: 'timeline-item', nameCn: '时间线数据项', desc: '时间线数据项' }],
'dropdown': [
{ key: 'dropdown-menu', nameCn: '下拉菜单', desc: '下拉菜单' },
{ key: 'dropdown-item', nameCn: '下拉菜单项', desc: '下拉菜单项' }
],
'carousel': [{ key: 'carousel-item', nameCn: '走马灯数据项', desc: '走马灯数据项' }],
'checkbox': [
{ key: 'checkbox-group', nameCn: '复选框组', desc: '复选框组' },
{ key: 'checkbox-button', nameCn: '复选框按钮', desc: '复选框按钮' }
],
'radio': [
{ key: 'radio-group', nameCn: '单选框组', desc: '单选框组' },
{ key: 'radio-button', nameCn: '单选框按钮', desc: '单选框按钮' }
],
'grid': [
{ key: 'grid-column', nameCn: '表格列对象', desc: '表格列对象' },
{ key: 'grid-toolbar', nameCn: '表格工具栏', desc: '表格工具栏' }
]
}
const result = {
'data': {
'framework': 'Vue',
'materials': {
'components': [],
'snippets': [],
'blocks': []
}
}
}
// 1. 初始化
function init(cb) {
const argv = minimist(process.argv.slice(2))
if (argv.apiPath) {
apiPath = argv.apiPath
}
if (argv.tinyVer) {
tinyVer = argv.tinyVer
}
// 读菜单
shell.cp(path.join(apiPath, '/menus.js'), 'gulp/menus.mjs')
shell.sed('-i', 'import.meta.env.VITE_BUILD_TARGET', 'false', 'gulp/menus.mjs')
import('./menus.mjs').then((res) => {
menus = res.cmpMenus
cb()
})
}
// 2. 遍历菜单,处理每一个组件
function read(cb) {
menus.forEach((group) => {
const snippetItem = {
'group': group.label,
'children': []
}
group.children.forEach((component) => {
if (ignoreKeys.includes(component.key)) {
return
}
if (component.key.startsWith('grid-') || component.key.startsWith('chart-')) {
return
}
const componentInfo = _readOneComp(component)
const componentItem = genComp(componentInfo)
const snipItem = genSnip(componentInfo)
result.data.materials.components.push(componentItem)
snippetItem.children.push(snipItem)
// 特殊的混合组件判断, 比如遍历到form时要插入 form-item的定义
if (mixinKeys[component.key]) {
mixinKeys[component.key].forEach((mixin) => {
const componentInfo = _readOneComp(component, mixin)
const componentItem = genComp(componentInfo)
const snipItem = genSnip(componentInfo)
result.data.materials.components.push(componentItem)
snippetItem.children.push(snipItem)
})
}
})
result.data.materials.snippets.push(snippetItem)
})
cb()
}
function _readOneComp(component, mixin = '') {
if (!mixin) {
shell.echo('---正在读取组件', component.key)
const key = component.key
const camelizeKey = camelize(key)
const cmpName = component.nameCn
const desc = readMdDesc(key)
const jsStr = shell.cat(`${apiPath}/app/${key}/webdoc/${key}.js`).replace('export default', '(') + ')'
const api = eval(jsStr).apis.filter((item) => item.name === key)[0]
const props = api?.props || api?.properties || []
const events = api?.events || []
const slots = api?.slots || []
guid++
const componentInfo = { id: guid, version: tinyVer, key, camelizeKey, cmpName, desc, props, events, slots }
return componentInfo
} else {
shell.echo('---正在读取混入的组件', component.key, mixin.key)
const key = mixin.key
const camelizeKey = camelize(key)
const cmpName = mixin.nameCn
const desc = mixin.desc
const jsStr =
shell.cat(`${apiPath}/app/${component.key}/webdoc/${component.key}.js`).replace('export default', '(') + ')'
const api = eval(jsStr).apis.filter((item) => item.name === mixin.key)[0]
const props = api?.props || api?.properties || []
const events = api?.events || []
const slots = api?.slots || []
guid++
const componentInfo = { id: guid, version: tinyVer, key, camelizeKey, cmpName, desc, props, events, slots }
return componentInfo
}
}
// 3. 将结果写bundle.json
function write(cb) {
shell.ShellString(JSON.stringify(result, null, ' ')).to('gulp/bundle.json')
cb()
}
// 4. 清除
function clean(cb) {
shell.rm('gulp/menus.mjs')
cb()
}
// 以下辅助方法
// 转换button-group ==> ButtonGroup
const camelize = (str) => {
return str
.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
.replace(/^(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}
// 从标签中,提取文字。 <div>常用的操作按钮。</div> ==> 常用的操作按钮。
const readMdDesc = (key) => {
const mdStr = shell.cat(`${apiPath}/app/${key}/webdoc/${key}.cn.md`)
const matched = mdStr.match(/<div>([\w|\s|\u4e00-\u9fa5]*)<\/div>/i)
return matched ? matched[1] || '' : ''
}
const genComp = ({ id, version, key, camelizeKey, cmpName, desc, props, events, slots }) => {
const item = {
'id': id,
'version': version,
'name': {
'zh_CN': cmpName
},
'component': 'Tiny' + camelizeKey,
'icon': '',
'description': desc,
'doc_url': '',
'screenshot': '',
'tags': '',
'keywords': '',
'dev_mode': 'proCode',
'npm': {
'package': '@opentiny/vue',
'exportName': camelizeKey,
'version': version,
'destructuring': true
},
'group': 'component',
'configure': {
'loop': true,
'condition': true,
'styles': true,
'isContainer': false,
'isModal': false,
'nestingRule': {
'childWhitelist': '',
'parentWhitelist': '',
'descendantBlacklist': '',
'ancestorWhitelist': ''
},
'isNullNode': false,
'isLayout': false,
'rootSelector': '',
'shortcuts': {
'properties': []
},
'contextMenu': {
'actions': [],
'disable': []
},
'framework': 'Vue'
},
'schema': {
'properties': [
{
'label': {
'zh_CN': '基础信息'
},
'description': {
'zh_CN': '基础信息'
},
'collapse': {
'number': 6,
'text': {
'zh_CN': '显示更多'
}
},
'content': props.map((prop) => genProp(prop))
}
],
'events': {
...events.map((event) => genEvent(event)).reduce((pre, curr) => ({ ...pre, ...curr }), {})
},
'slots': {
...slots.map((slot) => genSlot(slot)).reduce((pre, curr) => ({ ...pre, ...curr }), {})
}
}
}
return item
}
const genProp = ({ name, type, defaultValue, desc }) => {
const typeMap = {
string: {
component: 'MetaInput',
props: {}
},
boolean: {
component: 'MetaSwitch',
props: {}
},
number: {
component: 'MetaNumberic',
props: {}
},
object: {
component: 'MetaCodeEditor',
props: {
language: 'json'
}
},
array: {
component: 'MetaCodeEditor',
props: {
language: 'json'
}
},
function: {
component: 'MetaCodeEditor',
props: {
language: 'javascript'
}
}
}
const normalizeType = type.trim().toLowerCase()
return {
'property': name,
'type': type,
'defaultValue': defaultValue == '--' ? '' : defaultValue,
'label': {
'text': {
'zh_CN': desc['zh-CN']
}
},
'cols': 12,
'rules': [],
'hidden': false,
'required': true,
'readOnly': true,
'disabled': true,
'widget': typeMap[normalizeType]
? typeMap[normalizeType] //
: { 'component': '', 'props': {} },
'description': {
'zh_CN': desc['zh-CN']
}
}
}
const genEvent = ({ name, type, defaultValue, desc }) => {
return {
['on' + camelize(name)]: {
'label': {
'zh_CN': desc['zh-CN']
},
'description': {
'zh_CN': desc['zh-CN']
},
'type': 'event',
'functionInfo': {
'params': [],
'returns': {}
},
'defaultValue': ''
}
}
}
const genSlot = ({ name, type, defaultValue, desc }) => {
return {
[name]: {
'label': {
'zh_CN': desc['zh-CN']
},
'description': {
'zh_CN': desc['zh-CN']
}
}
}
}
const genSnip = ({ id, version, key, camelizeKey, cmpName, desc, props, events, slots }) => {
return {
'name': {
'zh_CN': cmpName
},
'icon': key,
'screenshot': '',
'snippetName': 'Tiny' + camelizeKey,
'schema': {}
}
}