Compare commits

...

3 Commits

43 changed files with 1999 additions and 33 deletions

View File

@ -55,7 +55,8 @@
"vite": "^4.3.7",
"vite-plugin-monaco-editor": "^1.0.10",
"vite-plugin-svg-icons": "^2.0.1",
"vue-eslint-parser": "^8.0.1"
"vue-eslint-parser": "^8.0.1",
"vue-i18n": "^9.9.0"
},
"browserslist": [
"> 1%",

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { transform } from './src/transform.js'
import { transformSFC } from './src/transform-sfc.js'
export default function () {
return {
name: 'vite-plugin-generate-comments',
enforce: 'pre',
transform(code, id) {
if (id.endsWith('.vue')) {
const result = transformSFC(code, id)
return result
}
if (id.endsWith('.js') || id.endsWith('.jsx') || id.endsWith('.ts')) {
const result = transform(code, id)
return result
}
}
}
}

View File

@ -0,0 +1,23 @@
{
"name": "@opentiny/vite-plugin-generate-comments",
"version": "1.0.0",
"description": "",
"main": "index.js",
"module": "index.js",
"type": "module",
"scripts": {
"test": "node ./src/test/index.js"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.7",
"@babel/parser": "^7.18.13",
"@babel/traverse": "^7.18.13",
"@babel/generator": "^7.18.13",
"@babel/template": "^7.18.13",
"@vue/compiler-sfc": "^3.4.21"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@ -0,0 +1,57 @@
/* metaService */
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { reactive, onMounted, onBeforeMount as beforeMount } from 'vue'
import { deepCopy } from 'loash-es'
export const useRenderless = ({ props }) => {
const state = reactive({
tableData: props.data || props.op.data || []
})
onMounted(() => {})
onMounted(() => {})
onMounted(() => {})
beforeMount(() => {})
const logMessage = () => {
console.log('我是纯函数我不需要闭包参数')
}
const aaa = 'aaa',
bbb = 'bbb'
const handleClick = (e) => {
console.log(e.target, aaa)
state.tableData.push({
key: 'TinyEngine',
zhCN: '低代码引擎',
enUS: 'TinyEngine'
})
}
const ccc = 111
const sendMessage = () => {
logMessage('自定义是的范德萨')
}
function last() {}
return {
state,
aa,
handleClick,
sendMessage
}
}

View File

@ -0,0 +1,575 @@
import { callEntry as _callEntry, beforeCallEntry as _beforeCallEntry, afterCallEntry as _afterCallEntry, useCompile as _useCompile } from '@opentiny/tiny-engine-entry';
import _metaData from '../meta.js';
/* metaService */
import { reactive, onMounted, onBeforeMount as beforeMount } from 'vue';
import { deepCopy } from 'loash-es';
export const useRenderless = _callEntry(({
props
}) => {
const state = reactive({
tableData: props.data || props.op.data || []
});
onMounted(_callEntry(() => {}, {
metaData: {
id: `${_metaData.id}.onMounted[0]`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
}));
onMounted(_callEntry(() => {}, {
metaData: {
id: `${_metaData.id}.onMounted[1]`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
}));
onMounted(_callEntry(() => {}, {
metaData: {
id: `${_metaData.id}.onMounted[2]`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
}));
beforeMount(_callEntry(() => {}, {
metaData: {
id: `${_metaData.id}.onBeforeMount[0]`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
}));
_beforeCallEntry({
metaData: {
id: `${_metaData.id}.logMessage`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
const logMessage = _callEntry(() => {
console.log('我是纯函数我不需要闭包参数');
}, {
metaData: {
id: `${_metaData.id}.logMessage`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
_afterCallEntry({
metaData: {
id: `${_metaData.id}.logMessage`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
const aaa = 'aaa',
bbb = 'bbb';
_beforeCallEntry({
metaData: {
id: `${_metaData.id}.handleClick`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
e,
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
logMessage,
aaa,
bbb,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
const handleClick = _callEntry(e => {
console.log(e.target, aaa);
state.tableData.push({
key: 'TinyEngine',
zhCN: '低代码引擎',
enUS: 'TinyEngine'
});
}, {
metaData: {
id: `${_metaData.id}.handleClick`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
e,
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
logMessage,
aaa,
bbb,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
_afterCallEntry({
metaData: {
id: `${_metaData.id}.handleClick`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
e,
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
logMessage,
aaa,
bbb,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
const ccc = 111;
_beforeCallEntry({
metaData: {
id: `${_metaData.id}.sendMessage`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
const sendMessage = _callEntry(() => {
logMessage('自定义是的范德萨');
}, {
metaData: {
id: `${_metaData.id}.sendMessage`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
_afterCallEntry({
metaData: {
id: `${_metaData.id}.sendMessage`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
}
return asyncVars;
}
});
function last() {}
return {
state,
aa,
handleClick,
sendMessage
};
}, {
metaData: {
id: `${_metaData.id}.useRenderless`
},
ctx: () => {
let asyncVars = {};
try {
asyncVars = {
props,
state,
logMessage,
aaa,
bbb,
handleClick,
ccc,
sendMessage,
last,
reactive,
onMounted,
beforeMount,
deepCopy,
useRenderless
};
} catch (e) {
return {
reactive,
onMounted,
beforeMount,
deepCopy
};
}
return asyncVars;
}
});

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import fs from 'fs'
import { transform } from '../transform.js'
import { fileURLToPath } from 'node:url'
import * as path from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const code = fs.readFileSync(path.join(__dirname, './code/entry.js'), 'utf8')
const id = path.resolve(__dirname, './code/entry.js')
fs.writeFileSync(path.join(__dirname, './code/output.js'), transform(code, id) || '', 'utf8')

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { parse } from '@vue/compiler-sfc' //vue 处理sfc 的专用库
import { transform } from './transform.js'
import { isCallEntryFile } from './utils.js'
const getAttrsString = (attrs) => {
let attrStr = ''
if (!attrs) {
return attrStr
}
Object.keys(attrs).forEach((key) => {
const val = attrs[key]
if (val === true) {
attrStr += ` ${key}`
} else {
attrStr += ` ${key}="${val}"`
}
})
return attrStr
}
const generateTagContent = (tagDescriptor) => {
if (!tagDescriptor) {
return ''
}
const { attrs, content, type } = tagDescriptor
return `<${type}${getAttrsString(attrs)}>${content}</${type}>`
}
export const generateSFC = (descriptor) => {
const { script, scriptSetup, styles = [], template } = descriptor
return `${generateTagContent(template)}
${generateTagContent(script)}
${generateTagContent(scriptSetup)}
${styles.map(generateTagContent).join('\n')}
`
}
export const transformSFC = (code, id) => {
const { descriptor } = parse(code)
const { script, scriptSetup } = descriptor
if (!isCallEntryFile(code) || (!script && !scriptSetup)) {
return
}
if (script) {
script.content = transform(script.content, id) || script.content
}
if (scriptSetup) {
scriptSetup.content = transform(scriptSetup.content, id) || scriptSetup.content
}
return generateSFC(descriptor)
}

View File

@ -0,0 +1,179 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { parse } from '@babel/parser'
import generate from '@babel/generator'
import traverse from '@babel/traverse'
import template from '@babel/template'
import {
wrapEntryFuncNode,
COMMON_PACKAGE_NAME,
CALLENTRY,
BEFORE_CALLENTRY,
AFTER_CALLENTRY,
USE_COMPILE,
METADATANAME,
isCallEntryFile,
isCompileFile,
getMeataPath,
wrapExportComp,
vueLifeHook,
wrapHookCall,
getModuleId
} from './utils.js'
const generateTraverse = traverse.default
export const transform = (code, id) => {
// 如果不包含metaService或者metaComponent的文件直接退出
const isCallEntry = isCallEntryFile(code)
const isCompile = isCompileFile(code)
if (!isCallEntry && !isCompile) {
return
}
// 本次转换保存的状态
const state = {
varName: {}, // 变量名对应的映射表
hooksName: {},
hooksIndex: {},
varDeclartion: new Map(),
moduleId: '', // 自定义的模块ID用于区分元服务中不同文件,
noUseVars: []
}
// 找不到meta.js告警并返回
const metaPath = getMeataPath(id)
if (!metaPath) {
console.log('找不到对应的meta.js')
return
}
// 将源码解析为ast语法数
const resultAst = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx']
})
generateTraverse(resultAst, {
// 使用特定的类型回调处理、函数表达式、箭头函数、带导出的函数
'ArrowFunctionExpression|FunctionExpression'(path) {
const parentNode = path.parentPath || {}
const functionName = parentNode.node?.id?.name
// 只有拿到函数的名称才可以被复写
if (functionName) {
wrapEntryFuncNode({
path,
functionName,
varName: state.varName,
state
})
}
},
ImportDeclaration(path) {
// 解析vue的引入
const depName = path.node?.source?.value
if (depName === 'vue') {
const specifiers = path.node.specifiers
specifiers?.forEach((importSpecifier) => {
const { imported, local } = importSpecifier
const hookName = vueLifeHook.find((name) => imported.name === name)
if (hookName) {
state.hooksName[local.name] = hookName
}
state.noUseVars.push(local.name)
})
} else if (depName === '@opentiny/vue') {
const specifiers = path.node.specifiers
specifiers?.forEach((importSpecifier) => {
const { local } = importSpecifier
state.noUseVars.push(local.name)
})
}
},
VariableDeclaration(path) {
path.node.declarations?.forEach((val) => {
const name = val.id.name
const block = path.scope.block
if (!state.varDeclartion.has(block)) {
const arr = [name]
state.varDeclartion.set(block, arr)
} else {
const arr = state.varDeclartion.get(block)
arr.push(name)
}
})
},
ExpressionStatement(path) {
const { hooksName, varName, hooksIndex } = state
const callName = path.node.expression?.callee?.name
const hookName = hooksName[callName]
if (hookName) {
let hookIndex
if (hooksIndex[hookName]) {
hookIndex = hooksIndex[hookName]
hooksIndex[hookName] = hookIndex + 1
} else {
hooksIndex[hookName] = 1
hookIndex = 0
}
const functionName = `${hookName}[${hookIndex}]`
wrapHookCall({
path,
varName,
hooksName,
functionName,
callName,
state
})
}
},
Program(path) {
const code = path.toString()
state.moduleId = getModuleId(code)
const metaData = path.scope.generateUid(METADATANAME)
state.varName[METADATANAME] = metaData
path.node.body.unshift(template.statement(`import ${metaData} from '${metaPath}'`)())
const callEntry = path.scope.generateUid(CALLENTRY)
const beforeCallEntry = path.scope.generateUid(BEFORE_CALLENTRY)
const afterCallEntry = path.scope.generateUid(AFTER_CALLENTRY)
const useCompile = path.scope.generateUid(USE_COMPILE)
state.varName[CALLENTRY] = callEntry
state.varName[BEFORE_CALLENTRY] = beforeCallEntry
state.varName[AFTER_CALLENTRY] = afterCallEntry
state.varName[USE_COMPILE] = useCompile
path.node.body.unshift(
template.statement(
`import {
${CALLENTRY} as ${callEntry},
${BEFORE_CALLENTRY} as ${beforeCallEntry},
${AFTER_CALLENTRY} as ${afterCallEntry},
${USE_COMPILE} as ${useCompile}
} from '${COMMON_PACKAGE_NAME}'`
)()
)
},
ExportDefaultDeclaration(path) {
const comment = path.node.leadingComments
const lastComment = comment && comment[comment.length - 1].value
// 只判断最接近export default的注释节点
if (comment && lastComment.includes('metaComponent')) {
wrapExportComp({ path, varName: state.varName })
path.skip()
}
}
})
return generate.default(resultAst).code || ''
}

View File

@ -0,0 +1,248 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import template from '@babel/template'
import path from 'node:path'
import fs from 'node:fs'
export const CALLENTRY = 'callEntry'
export const BEFORE_CALLENTRY = 'beforeCallEntry'
export const AFTER_CALLENTRY = 'afterCallEntry'
export const USE_COMPILE = 'useCompile'
export const METADATANAME = 'metaData'
export const COMMON_PACKAGE_NAME = '@opentiny/tiny-engine-entry'
export const vueLifeHook = [
'onMounted',
'onUpdated',
'onUnmounted',
'onBeforeMount',
'onBeforeUpdate',
'onBeforeUnmount',
'onActivated',
'onDeactivated'
]
const callEntryExp = /\/\*\s*metaService/
const compileExp = /\/\*\s*metaComponent/
const statement = (code) => template.statement(code, { placeholderPattern: false })
export const isCallEntryFile = (code) => {
return callEntryExp.test(code)
}
export const isCompileFile = (code) => {
return compileExp.test(code)
}
export const getModuleId = (str) => {
const [, moduleId = ''] = str.match(/\/\*\s*metaService: \s*(.+?)\s*\*\//) || []
return moduleId
}
// 将注释中的参数提取出来,并组合成目前参数格式
export const getEntryParam = ({ functionName = '', syncVars, asyncVars, state }) => {
const { varName, moduleId, noUseVars } = state
const metaData = varName[METADATANAME]
const id = moduleId ? `'${moduleId}.${functionName}'` : `\`\${${metaData}.id}.${functionName}\``
const syncVarsKey = Object.keys(syncVars).filter((key) => !noUseVars.includes(key))
const asyncVarsKey = Object.keys(asyncVars).filter((key) => !noUseVars.includes(key) && !syncVarsKey.includes(key))
const ctx = ` () => {
let asyncVars = {}
const syncVars = {${syncVarsKey.join(',')}}
try {
asyncVars = { ${asyncVarsKey.join(',')} }
} catch {
return syncVars
}
return { ...syncVars, ...asyncVars }
}`
if (functionName) {
return `{ ${METADATANAME}: { id: ${id} }, ctx: ${ctx}}`
}
return `{ ${METADATANAME}: ${metaData} }`
}
const getParentVariableDeclaration = (path) => {
if (!path) {
return
}
if (path.type === 'VariableDeclaration' && path.parentPath.type !== 'ExportNamedDeclaration') {
return path
} else {
return getParentVariableDeclaration(path?.parentPath)
}
}
const generateBeforeAfterEntry = ({ path, beforeEntryAst, afterEntryAst }) => {
const parent = getParentVariableDeclaration(path)
if (parent) {
parent.insertBefore(beforeEntryAst)
parent.insertAfter(afterEntryAst)
}
}
export const getOuterBingdings = (path) => {
const outerBindings = {}
const allBindings = path.scope.getAllBindings()
const selfBindings = path.scope.bindings
Object.keys(allBindings).forEach((key) => {
if (allBindings[key] && !selfBindings[key]) {
outerBindings[key] = allBindings[key]
}
})
return outerBindings
}
// 获取当前上下文已经可以使用的scope变量
export const getValidBingdinngs = ({ path, state, functionName }) => {
const validBindings = {}
const { varDeclartion } = state
let varArr = []
let parentPath = path.parentPath
let block
while (parentPath) {
const newBlock = parentPath.scope.block
parentPath = parentPath.parentPath
if (newBlock === block) {
continue
}
block = newBlock
varArr = varArr.concat(varDeclartion.get(block))
}
const allBindings = path.scope.getAllBindings()
const selfBindings = path.scope.bindings
Object.keys(allBindings).forEach((key) => {
if (selfBindings[key]) {
return
}
const value = allBindings[key]
// 如果是变量定义,并且此时还没有初始化,则过滤掉
if ((['var', 'const', 'let'].includes(value.kind) && !varArr.includes(key)) || key === functionName) {
return
}
validBindings[key] = value
})
return validBindings
}
export const getModuleBindings = (path) => {
const moduleBindings = {}
const allBindings = path.scope.getAllBindings()
Object.keys(allBindings).forEach((key) => {
if (allBindings[key].kind === 'module') {
moduleBindings[key] = allBindings[key]
}
})
return moduleBindings
}
// 生成callEntry表达式并包裹当前函数如果有参与还需要处理参数
export const wrapEntryFuncNode = ({ path, functionName = '', varName, state }) => {
const syncVars = getValidBingdinngs({ path, state, functionName })
const asyncVars = getOuterBingdings(path)
const entryParam = getEntryParam({
functionName,
syncVars,
asyncVars,
varName,
state
})
const callEntry = varName[CALLENTRY]
const beforeCallEntry = varName[BEFORE_CALLENTRY]
const afterCallEntry = varName[AFTER_CALLENTRY]
const entryAst = statement(`${callEntry}(${entryParam})`)()
const beforeEntryAst = statement(`${beforeCallEntry}(${entryParam})`)()
const afterEntryAst = statement(`${afterCallEntry}(${entryParam})`)()
const resultNode = path.node
generateBeforeAfterEntry({ path, beforeEntryAst, afterEntryAst })
entryAst.expression.arguments.unshift(JSON.parse(JSON.stringify(resultNode)))
// 替换整个节点
path.replaceWith(entryAst)
}
// 获取两个文件路径的相对路径,入参为两个文件绝对路径
export const getRelFilePath = (path1, path2) => {
const dir1 = path.join(path1, '..')
const dir2 = path.join(path2, '..')
const relPath = path.relative(dir1, dir2) || '.'
return `${relPath}/${path.basename(path2)}`.replaceAll('\\', '/')
}
// 向上获取meta.js的相对路径
export const getMeataPath = (id) => {
let tempPath = path.join(id, '../meta.js')
const endCondition = () => {
// 找到了meta.js
const findMeta = fs.existsSync(tempPath)
// 发现了package.json说明到达子包根目录
const isSubRoot = fs.existsSync(path.join(tempPath, '../package.json'))
// 到达系统根节点,防止死循环
const isRoot = tempPath === path.join(tempPath, '../../meta.js')
return findMeta || isSubRoot || isRoot
}
while (!endCondition()) {
tempPath = path.join(tempPath, '../../meta.js')
}
if (fs.existsSync(tempPath)) {
return getRelFilePath(id, tempPath)
}
return null
}
export const wrapExportComp = ({ path, varName }) => {
const properties = path.node.declaration?.properties || []
const metaData = varName[METADATANAME]
const useCompile = varName[USE_COMPILE]
// 对键值为component属性包一层useCompile
properties.forEach((prop) => {
if (prop.key?.name === 'component') {
const val = prop.value
const compileAst = statement(`${useCompile}({ component: null, ${METADATANAME}: ${metaData} });`)()
compileAst.expression.arguments[0].properties[0].value = val
path.traverse({
enter(subPath) {
if (subPath.node === val) {
subPath.replaceWith(compileAst)
subPath.skip()
}
}
})
}
})
}
export const wrapHookCall = ({ path, varName, functionName, callName, state }) => {
// vue的生命周期hook只有一个参数
const argument = path.node.expression.arguments[0]
const callEntry = varName[CALLENTRY]
const syncVars = getValidBingdinngs({ path, state, functionName })
const asyncVars = path.scope.getAllBindings()
const entryParam = getEntryParam({
functionName,
syncVars,
asyncVars,
varName,
state
})
const wrapAst = statement(`${callName}(${callEntry}(${entryParam}))`)()
wrapAst.expression.arguments[0].arguments.unshift(argument)
path.replaceWith(wrapAst)
path.skip()
}

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { defineEntry } from '@opentiny/tiny-engine-entry'
import { createApp } from 'vue'
import EngineApp from './src/index.js'
import defaultResigry from './registry.js'
import { merge } from 'lodash-es'
import initSvgs from '@opentiny/tiny-engine-svgs'
import { setGlobalConfig } from '@opentiny/tiny-engine-controller'
import i18n from '@opentiny/tiny-engine-controller/js/i18n'
import globalConfig from './config/lowcode.config'
import { initMonitor } from '@opentiny/tiny-engine-controller/js/monitor'
import { injectGlobalComponents } from '@opentiny/tiny-engine-common'
import { initHttp } from '@opentiny/tiny-engine-http'
import 'virtual:svg-icons-register'
import TinyThemeTool from '@opentiny/vue-theme/theme-tool'
import { tinySmbTheme } from '@opentiny/vue-theme/theme' // SMB 主题
const type = (obj) => {
return Object.prototype.toString.call(obj).match(/\[object (.*)\]/)[1]
}
const mergeRegistry = (registry) => {
Object.entries(registry).forEach(([key, value]) => {
const defaultConfig = defaultResigry[key]
if (Array.isArray(value) && defaultConfig) {
value.forEach((meta, index) => {
const defaultMeta = defaultConfig.find((item) => item.id === meta.id)
if (defaultMeta) {
value[index] = merge(defaultMeta, meta)
}
})
}
if (type(value) === 'Object' && defaultConfig) {
registry[key] = merge(defaultConfig, registry[key])
}
})
return registry
}
const init = (selector, registry) => {
// 合并用户自定义注册表
const newRegistry = mergeRegistry(registry)
// 在common层注入合并后的注册表
defineEntry(newRegistry)
const app = createApp(EngineApp)
initHttp({ env: import.meta.env })
// eslint-disable-next-line no-new
new TinyThemeTool(tinySmbTheme, 'smbtheme') // 初始化主题
if (import.meta.env.VITE_ERROR_MONITOR === 'true' && import.meta.env.VITE_ERROR_MONITOR_URL) {
initMonitor(import.meta.env.VITE_ERROR_MONITOR_URL)
}
window.TinyGlobalConfig = globalConfig
setGlobalConfig(globalConfig)
initSvgs(app)
window.lowcodeI18n = i18n
app.use(i18n).use(injectGlobalComponents).mount('#app')
app.mount(selector)
}
export { init }

View File

@ -1,6 +1,7 @@
{
"name": "@opentiny/tiny-engine",
"version": "1.0.0-beta.4",
"type": "module",
"description": "TinyEngine enables developers to customize low-code platforms, build low-bit platforms online in real time, and support secondary development or integration of low-bit platform capabilities.",
"homepage": "https://opentiny.design/tiny-engine",
"keywords": [
@ -11,6 +12,14 @@
"lowcode",
"tiny-engine"
],
"module": "index.js",
"main": "index.js",
"exports": {
".": "index.js",
"./vite.config.js": "./vite.config.js",
"./config/lowcode.config": "./config/lowcode.config.js",
"./scripts/externalDeps": "./scripts/externalDeps.js"
},
"scripts": {
"dev": "cross-env NODE_OPTIONS=--max-old-space-size=10240 VITE_API_MOCK=mock vite",
"serve": "cross-env NODE_OPTIONS=--max-old-space-size=10240 vite",
@ -34,6 +43,8 @@
"@babel/generator": "~7.23.2",
"@babel/parser": "~7.23.2",
"@babel/traverse": "~7.23.2",
"@opentiny/vite-plugin-generate-comments": "workspace:*",
"@opentiny/tiny-engine-entry": "workspace:*",
"@opentiny/tiny-engine-canvas": "workspace:*",
"@opentiny/tiny-engine-common": "workspace:*",
"@opentiny/tiny-engine-controller": "workspace:*",
@ -126,7 +137,8 @@
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-static-copy": "^0.16.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue-eslint-parser": "^8.0.1"
"vue-eslint-parser": "^8.0.1",
"lodash-es": "^4.17.21"
},
"browserslist": [
"> 1%",

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import i18nMeta from '@opentiny/tiny-engine-plugin-i18n'
export default {
plugins: [i18nMeta]
}

View File

@ -1,5 +1,5 @@
import path from 'node:path'
import { readJsonSync } from 'fs-extra'
import fs from 'fs-extra'
import { installPackageTemporary } from '../vite-plugins/installPackageTemporary'
import { configServerAddProxy } from '../vite-plugins/configureServerAddProxy'
import { viteStaticCopy } from 'vite-plugin-static-copy'
@ -10,6 +10,8 @@ import {
copyfileToDynamicSrcMapper
} from './locateCdnNpmInfo'
const { readJsonSync } = fs
export function extraBundleCdnLink(filename, originCdnPrefix) {
const result = []
const bundle = readJsonSync(filename)

View File

@ -1,5 +1,5 @@
import path from 'node:path'
import { readJsonSync } from 'fs-extra'
import fs from 'fs-extra'
import {
getPackageNeedToInstallAndFilesUsingSameVersion,
copyfileToDynamicSrcMapper,
@ -10,6 +10,8 @@ import {
import { viteStaticCopy } from 'vite-plugin-static-copy'
import { installPackageTemporary } from '../vite-plugins/installPackageTemporary'
const { readJsonSync } = fs
export function extraPreviewImport(filename, originCdnPrefix) {
const result = []
const importMap = readJsonSync(filename)

View File

@ -1,10 +1,11 @@
import path from 'node:path'
import fs from 'node:fs'
import fs from 'fs-extra'
import fg from 'fast-glob'
import { normalizePath } from 'vite'
import { readJsonSync } from 'fs-extra'
import { babelReplaceImportPathWithCertainFileName } from './replaceImportPath.mjs'
const { readJsonSync } = fs
function transform(content, filename) {
if (filename.endsWith('.js')) {
const result = babelReplaceImportPathWithCertainFileName(content, filename, console)

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import component from './App.vue'
export default component

View File

@ -2,18 +2,29 @@ import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
import monacoEditorPluginCjs from 'vite-plugin-monaco-editor'
import vueJsx from '@vitejs/plugin-vue-jsx'
import nodeGlobalsPolyfillPlugin from '@esbuild-plugins/node-globals-polyfill'
import nodeModulesPolyfillPlugin from '@esbuild-plugins/node-modules-polyfill'
import nodeGlobalsPolyfillPluginCjs from '@esbuild-plugins/node-globals-polyfill'
import nodeModulesPolyfillPluginCjs from '@esbuild-plugins/node-modules-polyfill'
import nodePolyfill from 'rollup-plugin-polyfill-node'
import esbuildCopy from 'esbuild-plugin-copy'
import lowcodeConfig from './config/lowcode.config'
import lowcodeConfig from './config/lowcode.config.js'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { importmapPlugin } from './scripts/externalDeps'
import visualizer from 'rollup-plugin-visualizer'
import { importmapPlugin } from './scripts/externalDeps.js'
import visualizerCjs from 'rollup-plugin-visualizer'
import { fileURLToPath } from 'node:url'
import generateComment from '@opentiny/vite-plugin-generate-comments'
import { getBaseUrlFromCli, copyBundleDeps, copyPreviewImportMap, copyLocalImportMap } from './scripts/localCdnFile'
const monacoEditorPlugin = monacoEditorPluginCjs.default
const nodeGlobalsPolyfillPlugin = nodeGlobalsPolyfillPluginCjs.default
const nodeModulesPolyfillPlugin = nodeModulesPolyfillPluginCjs.default
const visualizer = visualizerCjs.default
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const origin = 'http://localhost:9090/'
const config = {
@ -52,6 +63,7 @@ const config = {
open: false
},
plugins: [
generateComment(),
visualizer({
filename: 'tmp/report.html',
title: 'Bundle Analyzer'
@ -183,7 +195,8 @@ const devAlias = {
'@opentiny/tiny-engine-utils': path.resolve(__dirname, '../utils/src/index.js'),
'@opentiny/tiny-engine-webcomponent-core': path.resolve(__dirname, '../webcomponent/src/lib.js'),
'@opentiny/tiny-engine-i18n-host': path.resolve(__dirname, '../i18n/src/lib.js'),
'@opentiny/tiny-engine-builtin-component': path.resolve(__dirname, '../builtinComponent/index.js')
'@opentiny/tiny-engine-builtin-component': path.resolve(__dirname, '../builtinComponent/index.js'),
'@opentiny/tiny-engine-entry': path.resolve(__dirname, '../entry/index.js')
}
const prodAlias = {
@ -197,8 +210,12 @@ const commonAlias = {
'@opentiny/tiny-engine-app-addons': path.resolve(__dirname, './config/addons.js')
}
export default defineConfig(({ command, mode }) => {
const { VITE_CDN_DOMAIN, VITE_LOCAL_IMPORT_MAPS, VITE_LOCAL_BUNDLE_DEPS } = loadEnv(mode, process.cwd(), '')
export default defineConfig(({ command = 'serve', mode = 'serve' }) => {
const {
VITE_CDN_DOMAIN = 'https://npm.onmicrosoft.cn',
VITE_LOCAL_IMPORT_MAPS,
VITE_LOCAL_BUNDLE_DEPS
} = loadEnv(mode, process.cwd(), '')
const isLocalImportMap = VITE_LOCAL_IMPORT_MAPS === 'true' // true公共依赖库使用本地打包文件false公共依赖库使用公共CDN
const isCopyBundleDeps = VITE_LOCAL_BUNDLE_DEPS === 'true' // true bundle里的cdn依赖处理成本地依赖 false 不处理

View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../src/index.js'

View File

@ -0,0 +1,27 @@
{
"name": "@opentiny/tiny-engine-cli",
"version": "1.0.0",
"type": "module",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"engine-cli": "./bin/cli.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"chalk": "^5.3.0",
"commander": "^12.0.0",
"execa": "^8.0.1",
"fs-extra": "^11.2.0",
"globby": "^14.0.1",
"handlebars": "^4.7.8",
"inquirer": "^9.2.17",
"ora": "^8.0.1",
"vite": "^5.1.6"
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { build } from 'vite'
import { resolveViteConfig } from '../common.js'
export default async function () {
const viteConfig = await resolveViteConfig()
await build({
configFile: false,
...viteConfig,
// root: __dirname,
server: {
port: 8000
}
})
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { fileURLToPath } from 'node:url'
import { cwd } from 'node:process'
import path from 'node:path'
import fs from 'fs-extra'
import chalk from 'chalk'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export default function (name) {
const sourcePath = path.join(__dirname, '../../template')
const destPath = path.join(cwd(), name)
fs.copySync(sourcePath, destPath)
console.log(
chalk.green(`create finish, run the follow command to start project: \ncd ${name} && npm install && npm run dev`)
)
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { createServer } from 'vite'
import { resolveViteConfig } from '../common.js'
export default async function () {
const viteConfig = await resolveViteConfig()
const server = await createServer({
configFile: false,
...viteConfig,
// root: __dirname,
server: {
...{
port: 8000
},
...viteConfig.server
}
})
await server.listen()
server.printUrls()
server.bindCLIShortcuts({ print: true })
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { cwd } from 'node:process'
import path from 'node:path'
import { defineConfig } from 'vite'
const resolveViteConfig = async () => {
const configPath = path.join(cwd(), 'engine.config.js')
let config = {}
try {
const allConfig = (await import(`file://${configPath}`)).default
config = allConfig?.viteConfig || {}
} catch (err) {
console.log(err)
}
return defineConfig(config)
}
export { resolveViteConfig }

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { fileURLToPath } from 'node:url'
import fs from 'fs-extra'
import * as globby from 'globby'
import { Command } from 'commander'
import * as path from 'path'
import chalk from 'chalk'
import create from './commands/create.js'
import serve from './commands/serve.js'
import build from './commands/build.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const program = new Command()
let commandsPath = []
let pkgVersion = ''
let pkgName = ''
// 获取当前包的信息
const getPkgInfo = () => {
const jsonPath = path.join(__dirname, '../package.json')
const jsonResult = fs.readJSONSync(jsonPath)
pkgVersion = jsonResult.version
return pkgVersion
}
program
.command('create <name>')
.description('创建一个新工程')
.action((name) => {
create(name)
})
program
.command('serve')
.description('开启服务')
.action(() => {
serve()
})
program
.command('build')
.description('构建')
.action(() => {
build()
})
program.parse(process.argv)

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import vue from '@vitejs/plugin-vue'
// TODO: 考虑是否把各项配置合并到一个配置文件当中
export default {
registry: {},
viteConfig: {
plugins: [vue()]
}, // 将vite.config.js配置到此处
otherConfig: {}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,20 @@
{
"name": "engine-cli-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "engine-cli serve",
"build": "engine-cli build"
},
"dependencies": {
"vue": "^3.4.21",
"@opentiny/tiny-engine": "workspace:^",
"@opentiny/tiny-engine-entry": "workspace:^"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.7",
"@opentiny/tiny-engine-cli": "workspace:^"
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export default {
layout: { id: 'engine.layout' },
toolbars: [
{
id: 'engine.toolbars.download'
},
{
id: 'engine.toolbars.refresh'
}
],
plugins: [{ id: 'engine.plugins.i18n' }, { id: 'engine.plugins.status' }],
dsls: [{ id: 'engine.dsls.dslvue' }],
settings: [],
canvas: {},
utils: { id: 'engine.utils' }
}

View File

@ -0,0 +1,15 @@
<script setup>
import { onMounted } from 'vue'
import { init } from '@opentiny/tiny-engine'
import registry from '../registry.js'
onMounted(() => {
init('#engine-app', registry)
})
</script>
<template>
<div id="engine-app"></div>
</template>
<style scoped></style>

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import registry from '../registry.js'
import { defineEntry } from '@opentiny/tiny-engine-entry'
defineEntry(registry)

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import './defineEntry.js'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

28
packages/entry/index.js Normal file
View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { getMergeMeta } from './src/common'
import { useCompile } from './src/templateHash'
import { defineEntry, callEntry, beforeCallEntry, afterCallEntry, getMergeRegistry } from './src/entryHash'
import { getLayoutComponent } from './src/layoutHash'
export {
getMergeMeta,
useCompile,
defineEntry,
callEntry,
beforeCallEntry,
afterCallEntry,
getLayoutComponent,
getMergeRegistry
}

View File

@ -0,0 +1,15 @@
{
"name": "@opentiny/tiny-engine-entry",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"vue": "^3.4.21"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@ -0,0 +1,127 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
const vueLifeHook = [
'onMounted',
'onUpdated',
'onUnmounted',
'onBeforeMount',
'onBeforeUpdate',
'onBeforeUnmount',
'onActivated',
'onDeactivated'
]
/**
* 自定义方法注册哈希表形式如下
* {
* 'engine.plugins.i18n.handleClick': () => { // do something }
* }
*/
export const entryHashMap = {}
/**
* 自定义模板注册哈希表形式如下
* {
* 'engine.plugins.status.metas.app': <template></template>
* }
*/
export const templateHashMap = {}
/**
* 自定布局hash形式如下
* {
* 'engine.plugins.status': customLayout
* }
*/
export const layoutHashMap = {}
export const metasHashMap = {}
const handleMethods = (id, methods) => {
Object.entries(methods).forEach(([fileId, idMethods]) => {
if (typeof idMethods === 'object') {
Object.entries(idMethods).forEach(([name, method]) => {
const prefix = fileId ? `.${fileId}` : ''
const methodId = `${id}${prefix}.${name}`
entryHashMap[methodId] = method
})
}
})
}
const handleVueLifeCycle = (id, value) => {
vueLifeHook.forEach((hookName) => {
const hookConfig = value[hookName]
if (!hookConfig) {
return
}
if (typeof hookConfig === 'function') {
const hookId = `${id}.${hookName}[0]`
entryHashMap[hookId] = hookConfig
}
if (hookConfig instanceof Array) {
hookConfig.forEach((hookFn, index) => {
if (typeof hookFn === 'function') {
const hookId = `${id}.${hookName}[${index}]`
entryHashMap[hookId] = hookFn
}
})
}
})
}
const handleLifeCycles = (id, lifeCycles) => {
Object.entries(lifeCycles).forEach(([fileId, idLifeCycles]) => {
const prefix = fileId ? `.${fileId}` : ''
const lifeCycleId = `${id}${prefix}`
handleVueLifeCycle(lifeCycleId, idLifeCycles)
})
}
const handleRegistryProp = (id, value) => {
const { template, layout, methods, lifeCycles } = value
// 处理生命周期
if (lifeCycles) {
handleLifeCycles(id, lifeCycles)
}
// 如果id和模板配置同时存在则放到模板hash表中
if (template) {
templateHashMap[id] = template
}
if (layout) {
layoutHashMap[id] = layout
}
if (methods) {
handleMethods(id, methods)
}
}
export const generateRegistry = (registry) => {
Object.entries(registry).forEach(([key, value]) => {
if (typeof value === 'object' && value) {
const { id } = value
// 如果匹配到了id说明是元服务配置对元服务配置做读取和写入
if (id && key !== 'metaData') {
handleRegistryProp(id, value)
metasHashMap[id] = value
} else {
// TODO: 其他类型配置处理
}
generateRegistry(value)
}
})
}
export const getMergeMeta = (meta) => {
return metasHashMap[meta?.id]
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { generateRegistry, entryHashMap } from './common'
const lowcodeRegistry = { registry: null }
export const getMergeRegistry = () => lowcodeRegistry.registry
export const defineEntry = (registry) => {
if (!registry) {
throw new Error('请传递正确的注册表')
}
lowcodeRegistry.registry = registry
generateRegistry(registry)
}
export const callEntry = (fn, params) => {
const { metaData, ctx } = params
const customMethod = entryHashMap[metaData?.id]
if (customMethod) {
const customFn = customMethod.entry ? customMethod.entry : customMethod
if (typeof customFn === 'function') {
return customFn(ctx, fn)
}
}
return fn
}
export const beforeCallEntry = ({ metaData, ctx }) => {
const id = metaData?.id
const customMethod = entryHashMap[id]?.before
if (customMethod) {
customMethod(ctx)
}
}
export const afterCallEntry = ({ metaData, ctx }) => {
const id = metaData?.id
const customMethod = entryHashMap[id]?.after
if (customMethod) {
customMethod(ctx)
}
}

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { layoutHashMap } from './common'
export const getLayoutComponent = (metaData) => {
const customLayout = layoutHashMap[metaData.id]
return customLayout
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2024 - present TinyEngine Authors.
* Copyright (c) 2024 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { compile } from 'vue/dist/vue.esm-bundler.js'
import { templateHashMap } from './common'
const generateTemplate = (template) => {
const templateString = template.trim()
if (templateString.startsWith('<template>') && templateString.endsWith('</template>')) {
return templateString.slice(10, -11)
}
return templateString
}
export const useCompile = ({ component, metaData }) => {
// 此处compile会缓存template对应的render函数并且render函数一个纯函数用到的所有变量都来自参数
const customTem = templateHashMap[metaData.id]
if (customTem) {
const template = generateTemplate(customTem)
component.render = compile(template)
}
return component
}

View File

@ -1,21 +1,18 @@
/**
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import component from './src/Main.vue'
import metaData from './meta.js'
export default {
id: 'I18n',
title: '国际化',
icon: 'plugin-icon-language',
align: 'top',
...metaData,
component
}

View File

@ -0,0 +1,8 @@
import { iconLanguage } from '@opentiny/vue-icon'
export default {
id: 'engine.plugins.i18n',
title: '国际化',
type: 'plugins',
align: 'top',
icon: iconLanguage()
}

View File

@ -24,6 +24,7 @@
"license": "MIT",
"homepage": "https://opentiny.design/tiny-engine",
"dependencies": {
"@opentiny/tiny-engine-entry": "workspace:*",
"@opentiny/tiny-engine-common": "workspace:*",
"@opentiny/tiny-engine-controller": "workspace:*",
"@opentiny/tiny-engine-http": "workspace:*",
@ -40,4 +41,4 @@
"@opentiny/vue-icon": "^3.14.0",
"vue": "^3.4.15"
}
}
}

View File

@ -119,6 +119,7 @@
</template>
<script lang="jsx">
/* metaService */
import { computed, ref, watchEffect, reactive, onMounted, nextTick, resolveComponent } from 'vue'
import useClipboard from 'vue-clipboard3'
import { Grid, GridColumn, Input, Popover, Button, FileUpload, Loading, Tooltip, Select } from '@opentiny/vue'
@ -127,7 +128,7 @@ import { PluginPanel, LinkButton, SearchEmpty } from '@opentiny/tiny-engine-comm
import { useTranslate, useApp, useModal, getGlobalConfig, useHelp } from '@opentiny/tiny-engine-controller'
import { utils } from '@opentiny/tiny-engine-utils'
import { useHttp } from '@opentiny/tiny-engine-http'
import { BASE_URL } from '@opentiny/tiny-engine-controller/js/environments'
// import { BASE_URL } from '@opentiny/tiny-engine-controller/js/environments'
export default {
components: {
@ -288,7 +289,7 @@ export default {
}
const downloadFile = () => {
window.open(`${BASE_URL}src/app/public/i18n-mock/i18n-template-for-batch-import.zip`)
// window.open(`${BASE_URL}src/app/public/i18n-mock/i18n-template-for-batch-import.zip`)
}
const openDeletePopover = (row) => {