feat: add materials script (#195)

增加拆分物料与构建物料脚本
This commit is contained in:
yaoyun8 2023-12-29 16:21:16 +08:00 committed by GitHub
parent 11767f2b64
commit b343220d77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 654 additions and 2 deletions

5
.env.local Normal file
View File

@ -0,0 +1,5 @@
SQL_HOST=localhost
SQL_PORT=3306
SQL_USER=root
SQL_PASSWORD=admin
SQL_DATABASE=tiny_engine

View File

@ -19,7 +19,9 @@
"pub:preminor": "pnpm run build:plugin && pnpm run build:alpha && pnpm lerna version preminor --preid beta --no-push --yes && lerna publish from-package --pre-dist-tag beta --yes",
"pub:prepatch": "pnpm run build:plugin && pnpm run build:alpha && pnpm lerna version prepatch --preid beta --no-push --yes && lerna publish from-package --pre-dist-tag beta --yes",
"pub:prerelease": "pnpm run build:plugin && pnpm run build:alpha && pnpm lerna version prerelease --preid beta --no-push --yes && lerna publish from-package --pre-dist-tag beta --yes",
"setup": "node ./scripts/setup.js"
"setup": "node ./scripts/setup.js",
"splitMaterials": "node ./scripts/splitMaterials.mjs",
"buildMaterials": "node ./scripts/buildMaterials.mjs"
},
"devDependencies": {
"@babel/eslint-parser": "^7.21.3",
@ -30,16 +32,21 @@
"@vitejs/plugin-vue-jsx": "^1.3.2",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"chokidar": "^3.5.3",
"concurrently": "^8.2.0",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.38.0",
"eslint-plugin-vue": "^8.0.0",
"fast-glob": "^3.3.2",
"fs-extra": "^10.1.0",
"husky": "^8.0.0",
"concurrently": "^8.2.0",
"lerna": "^7.2.0",
"less": "^4.1.2",
"lint-staged": "^13.2.0",
"mysql": "^2.18.1",
"path": "^0.12.7",
"picocolors": "^1.0.0",
"rimraf": "^3.0.2",
"rollup-plugin-polyfill-node": "^0.12.0",
"rollup-plugin-terser": "^7.0.2",

199
scripts/buildMaterials.mjs Normal file
View File

@ -0,0 +1,199 @@
import fsExtra from 'fs-extra'
import path from 'node:path'
import chokidar from 'chokidar'
import fg from 'fast-glob'
import MysqlConnection from './connection.mjs'
import Logger from './logger.mjs'
const logger = new Logger('buildMaterials')
// 物料文件存放文件夹名称
const materialsDir = 'materials'
// 物料资产包
const bundlePath = path.join(process.cwd(), '/packages/design-core/public/mock/bundle.json')
// mockServer应用数据
const appInfoPath = path.join(process.cwd(), '/mockServer/src/services/appinfo.json')
const appInfo = fsExtra.readJSONSync(appInfoPath)
const bundle = {
data: {
framework: 'Vue',
materials: {
components: [],
blocks: [],
snippets: []
}
}
}
const connection = new MysqlConnection()
/**
* 更新物料资产包和应用mock数据
*/
const write = () => {
fsExtra.outputJSONSync(bundlePath, bundle, { spaces: 2 })
fsExtra.outputJSONSync(appInfoPath, appInfo, { spaces: 2 })
}
/**
* 校验组件文件数据
* @param {string} file 组件文件路径
* @param {object} component 组件数据
* @returns
*/
const validateComponent = (file, component) => {
const requiredFields = ['component']
const fields = Object.keys(component)
const requiredList = requiredFields.filter((field) => !fields.includes(field))
if (requiredList.length) {
logger.error(`组件文件 ${file} 缺少必要字段:${requiredList.join('、')}`)
return false
}
if (!component.npm) {
logger.warn(`组件文件 ${file} 缺少 npm 字段出码时将不能通过import语句导入组件。`)
return false
}
return true
}
/**
* 校验区块文件数据
* @param {string} file 区块文件路径
* @param {object} block 区块数据
* @returns
*/
const validateBlock = (file, block) => {
const requiredFields = ['label', 'assets']
const fields = Object.keys(block)
const requiredList = requiredFields.filter((field) => !fields.includes(field))
if (requiredList.length) {
logger.error(`区块文件 ${file} 缺少必要字段:${requiredList.join('、')}`)
return false
}
return true
}
/**
* 读取materials目录下的json文件执行下列操作
* 1. 合并生成物料资产包
* 2. 更新应用的组件数据componentsMap
* 3. 连接上数据库后将组件数据写入数据库新增或更新
*/
const generateComponents = () => {
try {
fg([`${materialsDir}/**/*.json`]).then((files) => {
if(!files.length) {
logger.warn('物料文件夹为空,请先执行`pnpm splitMaterials`命令拆分物料资产包')
}
const { components = [], snippets = [], blocks = [] } = bundle.data.materials
const componentsMap = []
const appInfoBlocksLabels = appInfo.blockHistories.map((item) => item.label)
files.forEach((file) => {
const material = fsExtra.readJsonSync(file, { throws: false })
if (!material) {
logger.error(`读取物料文件 ${file} 失败`)
return
}
if (file.includes('/blocks/')) {
const valid = validateBlock(file, material)
if (!valid) return
blocks.push(material)
if (!appInfoBlocksLabels.includes(material.label)) {
appInfo.blockHistories.push(material)
}
return
}
const valid = validateComponent(file, material)
if (!valid) return
const { snippets: componentSnippets, category, ...componentInfo } = material
components.push(componentInfo)
const snippet = snippets.find((item) => item.group === category)
if (snippet) {
componentSnippets && snippet.children.push(componentSnippets[0])
} else if (category && componentInfo) {
snippets.push({
group: category,
children: componentSnippets || []
})
}
const { component, npm = {} } = componentInfo
componentsMap.push({ component, npm })
if (connection.connected) {
connection.initDB(material)
}
})
appInfo.materialHistory.components = componentsMap
write()
})
logger.success('构建物料资产包成功')
} catch (error) {
logger.error(`构建物料资产包失败:${error}`)
}
}
// 监听materials下json文件的变化
const watcher = chokidar.watch(`${materialsDir}/**/*.json`, { ignoreInitial: true })
watcher.on('all', (event, file) => {
const eventMap = {
add: '新增',
change: '更新',
unlink: '删除'
}
logger.info(`${eventMap[event]}组件文件 ${file}`)
// 监听物料文件变化,更新物料资产包
generateComponents()
if (!connection.connected || event === 'unlink') return
const component = fsExtra.readJsonSync(path.join(process.cwd(), file))
if (event === 'change') {
connection.updateComponent(component)
} else if (event === 'add') {
connection.insertComponent(component)
}
})
// 连接数据库
connection
.connect()
.then(() => {
connection.initUserComponentsTable().finally(() => {
generateComponents()
})
})
.catch(() => {
// 未能连接数据库也可以执行更新本地mock数据
generateComponents()
})

338
scripts/connection.mjs Normal file
View File

@ -0,0 +1,338 @@
import mysql from 'mysql'
import Logger from './logger.mjs'
import fs from 'node:fs'
import path from 'node:path'
import dotenv from 'dotenv'
const logger = new Logger('buildMaterials')
// 先构造出.env*文件的绝对路径
const appDirectory = fs.realpathSync(process.cwd())
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath)
const pathsDotenv = resolveApp('.env')
// 加载.env.local
dotenv.config({ path: `${pathsDotenv}.local` })
const { SQL_HOST, SQL_PORT, SQL_USER, SQL_PASSWORD, SQL_DATABASE } = process.env
// 组件表名称
const componentsTableName = 'user_components'
// 组件关联到物料资产包的id
const materialHistoryId = 639
// 数据库配置
const mysqlConfig = {
host: SQL_HOST, // 主机名(服务器地址)
port: SQL_PORT, // 端口号
user: SQL_USER, // 用户名
password: SQL_PASSWORD, // 密码
database: SQL_DATABASE // 数据库名称
}
class MysqlConnection {
constructor(config) {
this.config = config || mysqlConfig
// 是否连接上了数据库
this.connected = false
this.connection = mysql.createConnection(this.config)
}
connect() {
return new Promise((resolve, reject) => {
this.connection.connect((error) => {
if (error) {
logger.warn('未能连接到数据库,请查看数据库配置是否正确')
reject()
} else {
logger.success('连接数据库成功')
this.connected = true
resolve()
}
})
})
}
/**
* 执行sql语句更新数据库
* @param {string} sql sql语句
* @param {string} componentName 组件名称
*/
query(sql) {
return new Promise((resolve, reject) => {
this.connection.query(sql, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
/**
* 组件字段映射
* @param {string} field 字段名
* @returns 映射后的字段名
*/
fieldTransform(field) {
const fieldMap = {
docUrl: 'doc_url',
devMode: 'dev_mode',
schema: 'schema_fragment'
}
return fieldMap[field] || field
}
/**
* 格式化单引号
* @param {string} str 待格式化的字符串
* @returns 格式化后的字符串
*/
formatSingleQuoteValue(str) {
if (typeof str !== 'string') {
return str
}
return str.replace(/'/g, "\\'")
}
/**
* 生成更新组件的sql语句
* @param {object} component 组件数据
* @returns 更新组件的sql语句
*/
updateComponent(component) {
const values = []
let sqlContent = `update ${componentsTableName} set `
Object.keys(component).forEach((key) => {
const { [key]: value } = component
const field = this.fieldTransform(key)
let updateContent = ''
if (['id', 'component'].includes(field)) {
return
}
if (typeof value === 'string') {
const formatValue = this.formatSingleQuoteValue(value)
updateContent = `\`${field}\` = '${formatValue}'`
} else if (typeof field === 'number' || field === null) {
updateContent = `\`${field}\` = ${value}`
} else {
const formatValue = this.formatSingleQuoteValue(JSON.stringify(value))
updateContent = `\`${field}\` = '${formatValue}'`
}
values.push(updateContent)
})
sqlContent += values.join()
sqlContent += ` where component = '${component.component}';`
this.query(sqlContent, component.component)
.then(() => {
logger.success(`更新组件 ${component.component} 成功`)
})
.catch((error) => {
logger.success(`更新组件 ${component.component} 失败:${error}`)
})
}
/**
* 新建的组件关联物料资产包
* @param {number} id 新建的组件id
*/
relationMaterialHistory(id) {
const uniqSql = `SELECT * FROM \`material_histories_components__user_components_mhs\` WHERE \`material-history_id\`=${materialHistoryId} AND \`user-component_id\`=${id}`
this.query(uniqSql).then((result) => {
if (!result.length) {
const sqlContent = `INSERT INTO \`material_histories_components__user_components_mhs\` (\`material-history_id\`, \`user-component_id\`) VALUES (${materialHistoryId}, ${id})`
this.query(sqlContent)
}
})
}
/**
* 生成新增组件的sql语句
* @param {object} component 组件数据
* @returns 新增组件的sql语句
*/
insertComponent(component) {
const {
version,
name,
component: componentName,
icon,
description,
docUrl,
screenshot,
tags,
keywords,
devMode,
npm,
group,
category,
priority = 1,
snippets,
schema,
configure,
public: publicRight = 0,
framework = 'vue',
isOfficial = 0,
isDefault = 0,
tiny_reserved = 0,
tenant = 1,
createBy = 86,
updatedBy = 86
} = component
const values = `('${version}',
'${this.formatSingleQuoteValue(JSON.stringify(name))}',
'${componentName}',
'${icon}',
'${this.formatSingleQuoteValue(description)}',
'${docUrl}',
'${screenshot}',
'${tags}',
'${keywords}',
'${devMode}',
'${this.formatSingleQuoteValue(JSON.stringify(npm))}',
'${group}',
'${category}',
'${priority}',
'${this.formatSingleQuoteValue(JSON.stringify(snippets))}',
'${this.formatSingleQuoteValue(JSON.stringify(schema))}',
'${this.formatSingleQuoteValue(JSON.stringify(configure))}',
'${publicRight}',
'${framework}',
'${isOfficial}',
'${isDefault}',
'${tiny_reserved}',
'${tenant}',
'${createBy}',
'${updatedBy}'
);`
const sqlContent = `INSERT INTO ${componentsTableName} (version, name, component, icon, description, doc_url,
screenshot, tags, keywords, dev_mode, npm, \`group\`, \`category\`, priority, snippets,
schema_fragment, configure, \`public\`, framework, isOfficial, isDefault, tiny_reserved,
tenant, createdBy, updatedBy) VALUES ${values}`.replace(/\n/g, '')
this.query(sqlContent, componentName)
.then((result) => {
const id = result.insertId
logger.success(`新增组件 ${component.component} 成功`)
this.relationMaterialHistory(id)
})
.catch((error) => {
logger.success(`新增组件 ${component.component} 失败:${error}`)
})
}
/**
* 初始化数据库数据判断是否已存在组件不存在时执行新增组件
* @param {object} component 组件数据
*/
initDB(component) {
const selectSqlContent = `SELECT * FROM ${this.config.database}.${componentsTableName} WHERE component = '${component.component}'`
this.query(selectSqlContent)
.then((result) => {
if (!result.length) {
this.insertComponent(component)
}
})
.catch((error) => {
logger.success(`查询组件 ${component.component} 失败:${error}`)
})
}
/**
* 创建组件表
* @returns promise
*/
createUserComponentsTable() {
const sqlContent = `
CREATE TABLE ${componentsTableName} (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
version varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
name longtext CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
component varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
icon varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
description varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
doc_url varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
screenshot varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
tags varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
keywords varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
dev_mode varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
npm longtext CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
\`group\` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
category varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
priority int(11) NULL DEFAULT NULL,
snippets longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
schema_fragment longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
configure longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
createdBy int(11) NULL DEFAULT NULL,
updatedBy int(11) NULL DEFAULT NULL,
created_by int(11) NULL DEFAULT NULL,
updated_by int(11) NULL DEFAULT NULL,
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
public int(11) NULL DEFAULT NULL,
framework varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
isOfficial tinyint(1) NULL DEFAULT NULL,
isDefault tinyint(1) NULL DEFAULT NULL,
tiny_reserved tinyint(1) NULL DEFAULT NULL,
tenant int(11) NULL DEFAULT NULL,
component_metadata longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
library int(11) NULL DEFAULT NULL,
PRIMARY KEY (id) USING BTREE,
UNIQUE INDEX unique_component(createdBy, framework, component, version) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
`.replace(/\n/g, '')
return new Promise((resolve, reject) => {
this.query(sqlContent)
.then((result) => {
logger.success(`创建表 ${componentsTableName} 成功`)
resolve(result)
})
.catch((error) => {
logger.success(`创建表 ${componentsTableName} 失败:${error}`)
reject(error)
})
})
}
/**
* 初始化数据库的组件表
* @returns promise
*/
initUserComponentsTable() {
return new Promise((resolve, reject) => {
// 查询是否已存在表
this.query(`SHOW TABLES LIKE '${componentsTableName}'`)
.then((result) => {
if (result.length) {
// 已存在
resolve()
} else {
this.createUserComponentsTable()
.then(() => {
resolve()
})
.catch((err) => {
reject(err)
})
}
})
.catch((error) => {
reject(error)
})
})
}
}
export default MysqlConnection

43
scripts/logger.mjs Normal file
View File

@ -0,0 +1,43 @@
import colors from 'picocolors'
class Logger {
constructor(command) {
this.command = command
}
output(type, msg) {
const format = () => {
const colorMap = {
info: 'cyan',
warn: 'yellow',
error: 'red',
success: 'green'
}
const time = new Date().toLocaleTimeString()
const colorMsg = colors[colorMap[type]](msg)
return `[${this.command}] [${colors.dim(time)}] ${colorMsg}`
}
// eslint-disable-next-line no-console
return console.log(format())
}
info(msg) {
this.output('info', msg)
}
warn(msg) {
this.output('warn', msg)
}
error(msg) {
this.output('error', msg)
}
success(msg) {
this.output('success', msg)
}
}
export default Logger

View File

@ -0,0 +1,60 @@
import fs from 'fs-extra'
import path from 'node:path'
import Logger from './logger.mjs'
const logger = new Logger('splitMaterials')
// 物料资产包mock数据路径
const bundlePath = path.join(process.cwd(), '/packages/design-core/public/mock/bundle.json')
// 物料文件存放文件夹名称
const materialsDir = 'materials'
const bundle = fs.readJSONSync(bundlePath)
const { components, snippets, blocks } = bundle.data.materials
const capitalize = (str) => `${str.charAt(0).toUpperCase()}${str.slice(1)}`
const toPascalCase = (str) => str.split('-').map(capitalize).join('')
/**
* 将物料资产包拆分为单个组件
*/
const splitMaterials = () => {
try {
components.forEach((comp) => {
snippets.some((child) => {
const snippet = child.children.find((item) => {
if (Array.isArray(comp.component)) {
return toPascalCase(comp.component[0]) === toPascalCase(item.snippetName)
}
return toPascalCase(comp.component) === toPascalCase(item.snippetName)
})
if (snippet) {
comp.snippets = [snippet]
comp.category = child.group
return true
}
return false
})
const fileName = Array.isArray(comp.component) ? comp.component[0] : comp.component
const componentPath = path.join(process.cwd(), materialsDir, 'components', `${toPascalCase(fileName)}.json`)
fs.outputJsonSync(componentPath, comp, { spaces: 2 })
})
blocks.forEach((block) => {
const blockPath = path.join(process.cwd(), materialsDir, 'blocks', `${block.label}.json`)
fs.outputJsonSync(blockPath, block, { spaces: 2 })
})
logger.success('拆分物料资产包完成')
} catch (error) {
logger.error(`拆分物料资产包失败: ${error}`)
}
}
splitMaterials()