xpn-cli
Version:
橡皮泥脚手架,最灵活的代码生成平台
417 lines (385 loc) • 12.1 kB
JavaScript
#!/usr/bin/env node
// 引用类库
const https = require("https");
const iconv = require("iconv-lite");
const program = require('commander');
const moment = require('moment');
const fs = require('fs');
const art = require('art-template');
const path = require('path');
const shell = require("shelljs");
const md5 = require("md5");
const clc = require('cli-color');
// 配置运行参数
program.usage("--overwrite [projectId]")
.option('-o, --overwrite', 'need to overwrite all file if already exists');
program.parse(process.argv);
// 获得项目标识
const overwrite = !!program.overwrite;
const projectId = program.args[0];
// 验证参数
if (!projectId) {
return program.help();
}
// 启动项目
main(projectId);
// 入口函数
async function main(projId) {
let root = './';
// root = './.tmp/';
// 载入项目信息
const proj = await readFromHttp("module=XPN.Projects.Profile&do=cli&proj_id=" + projId);
console.log();
console.log(" Work Space: " + proj.proj_sp_name);
console.log(" Project: " + proj.proj_name);
console.log(" Description: " + proj.proj_description);
console.log(" Framework: " + proj.proj_fw_name);
console.log(" Props Data: " + proj.proj_fw_props_data);
console.log();
console.log(" Object Count: " + proj.proj_objects.length);
console.log(" Object Items: " + join(proj.proj_objects, 'obj_comment'));
console.log();
console.log(" Latest Time: " + moment(proj.proj_last_write_time * 1000).format('YYYY-MM-DD HH:mm:ss'));
console.log();
console.log();
// 数据转换
proj.proj_fw_props_data = parseJson(proj.proj_fw_props_data);
proj.proj_objects.map((obj) => {
obj.obj_props_data = parseJson(obj.obj_props_data);
obj.obj_fields = parseJson(obj.obj_fields);
});
// 还原过滤器
const imports = mergeFilters(proj.proj_fw_filters);
// 分类模板
gTplList = [];
lTplList = [];
for (const tpl of proj.proj_fw_templates) {
if (tpl.tpl_is_global) {
gTplList.push(tpl);
} else {
lTplList.push(tpl);
}
}
// 是否首次安装
const init = !fs.existsSync(root + '.xpn');
// 删除文件夹
remove(root + ".xpn/low-code");
// 获得文件列表
const mapOrd = readXpnSourceMap(root + '.xpn/low-code.map');
// 文件列表
const map = new Map();
// 遍历全局模板
for (const tpl of gTplList) {
try {
const filename = art.render(tpl.tpl_filename, proj, { imports, escape: false });
let contents, changed;
try {
// 渲染代码内容
contents = art.render(tpl.tpl_content, proj, { imports, escape: false });
// 检查是否修改
const oldHash = mapOrd.has(filename) ? mapOrd.get(filename).hash : null;
const nowHash = hashStr(readUtf8Str(root + filename));
changed = oldHash !== nowHash;
} catch (err) {
throw err;
} finally {
if (changed) {
console.log(clc.yellow(" download: " + filename + ' (changed)'));
} else {
console.log(" download: " + filename);
}
}
// 追加源码地图
map.set(filename, {
filename, contents, hash: hashStr(contents)
});
// 覆盖文件
if (init || overwrite || !changed) {
remove(root + filename);
write(root + filename, contents);
}
write(root + ".xpn/low-code/" + filename, contents);
} catch (err) {
console.error(err);
}
}
// 遍历对象模板
for (const obj of proj.proj_objects) {
for (const tpl of lTplList) {
try {
const dat = Object.assign(JSON.parse(JSON.stringify(obj)), proj);
const filename = art.render(tpl.tpl_filename, dat, { imports, escape: false });
let contents, changed;
try {
// 渲染代码内容
contents = art.render(tpl.tpl_content, dat, { imports, escape: false });
// 检查是否修改
const oldHash = mapOrd.has(filename) ? mapOrd.get(filename).hash : null;
const nowHash = hashStr(readUtf8Str(root + filename));
changed = oldHash !== nowHash;
} catch (err) {
throw err;
} finally {
if (changed) {
console.log(clc.yellow(" download: " + filename + ' (changed)'));
} else {
console.log(" download: " + filename);
}
}
// 追加源码地图
map.set(filename, {
filename, contents, hash: hashStr(contents)
});
// 覆盖文件
if (init || overwrite || !changed) {
remove(root + filename);
write(root + filename, contents);
}
write(root + ".xpn/low-code/" + filename, contents);
} catch (err) {
console.error(err);
}
}
}
// 写入
writeXpnSourceMap(root + '.xpn/low-code.map', map);
console.log();
console.log();
// 拷贝子模块
const initGit = !fs.existsSync(root + ".git");
if (initGit) {
console.log('init git...');
shell.exec("git init");
const submodules = readGitSubmodules(root + ".gitmodules");
for (const mod of submodules) {
console.log("git add " + mod.name + " submodule");
shell.exec("git submodule add --name " + mod.name + " " + mod.url + " " + mod.path);
}
console.log();
} else {
console.log('.git exists');
}
// 安装项目
const initNode = !fs.existsSync(root + "node_modules");
if (initNode) {
console.log('install modules...');
shell.exec("npm i");
console.log();
} else {
console.log('node_modules exists');
}
} // end main
/**
*
* @param {*} basedir
*/
function readGitSubmodules(path) {
// const path = basedir + '.gitmodules';
if (fs.existsSync(path)) {
const buf = fs.readFileSync(path);
const str = iconv.decode(buf, "utf8");
const arr = str.split(/\[submodule\s+"([\da-z_\-]+?)"\]\s+path\s*=\s*(.+)\s+url\s*=\s*(.+)/ig);
const count = Math.floor(arr.length / 4);
const ret = [];
for (let i = 0; i < count; i++) {
const idx = i * 4;
ret.push({
name: arr[idx + 1],
path: arr[idx + 2],
url: arr[idx + 3],
})
}
return ret;
}
return [];
}
/**
* 递归删除文件或文件夹
* @param {string} path
*/
function remove(path) {
if (fs.existsSync(path)) {
const stats = fs.statSync(path);
if (stats.isFile()) {
fs.unlinkSync(path);
} else if (stats.isDirectory()) {
files = fs.readdirSync(path);
files.forEach(async (file, index) => {
const curPath = path + "/" + file;
remove(curPath);
});
fs.rmdirSync(path);
}
}
}
/**
*
* @param {*} basedir
*/
function readUtf8Str(path) {
if (fs.existsSync(path)) {
const buf = fs.readFileSync(path);
const str = iconv.decode(buf, "utf8");
return str;
}
return "";
}
/**
* 递归写入文件
* @param {string} filename
* @param {string} contents
*/
function write(filename, contents) {
if (fs.existsSync(filename)) {
return;
}
mkdir(path.dirname(filename));
fs.writeFileSync(filename, contents, { 'flag': 'a' });
}
/**
* 递归创建目录
* @param {string} dirname
*/
function mkdir(dirname) {
if (fs.existsSync(dirname)) {
return;
}
const parent = path.dirname(dirname);
mkdir(parent);
fs.mkdirSync(dirname, "0777");
}
/**
* JSON 解析
* @param {string} str
*/
function parseJson(str) {
try {
return JSON.parse(str);
} catch (err) {
console.error(err);
}
return null;
}
/**
* 合并过滤器
* @param {array} items
*/
function mergeFilters(items) {
const ret = {};
const arr = [];
for (const fl of items) {
try {
const methods = eval(fl.fl_code);
arr.push(methods);
for (const key in methods) {
if (typeof methods[key] === "function") {
ret[key] = (...opts) => methods[key](...opts);
}
}
} catch (err) {
console.error(err);
} // try
} // for
// 逆向绑定,相互共享方法
for (const methods of arr) {
for (const key in ret) {
if (typeof ret[key] !== "function") {
continue;
}
if (key in methods) {
continue;
}
methods[key] = (...opts) => ret[key](...opts);
}
}
return ret;
}
/**
* 关联内容
* @param {*} arr
* @param {*} key
* @param {*} separator
*/
function join(arr, key, separator = ', ') {
let items = [];
for (const obj of arr) {
items.push(obj[key]);
}
return items.join(separator);
}
/**
* 载入网络代码
* @param {*} uri
*/
function readFromHttp(uri) {
return new Promise((resolve, rejson) => {
https.get("https://if.lxzyz.com.cn/index.php?" + uri, (res) => {
if (res.statusCode !== 200) {
return rejson(new Error("Network Error, status:" + res.statusCode));
}
const data = [];
let size = 0;
res.on('data', (pkg) => {
data.push(pkg);
size += pkg.length;
});
res.on("end", () => {
const buf = Buffer.concat(data, size);
const str = iconv.decode(buf, "utf8");
const ret = JSON.parse(str);
if (ret.code === 0) {
resolve(ret.data);
} else {
rejson(new Error(ret.data.message));
}
});
}); // end https
}); // end Promise
} // end function
/**
* 获得
*/
function readXpnSourceMap(path) {
// const path = basedir + '.gitmodules';
const map = new Map();
if (fs.existsSync(path)) {
const buf = fs.readFileSync(path);
const str = iconv.decode(buf, "utf8");
const obj = JSON.parse(str);
if (obj.version !== 1) {
console.error('SourceMap format invalid');
} else {
for (const src of obj.sources) {
map.set(src.filename, src);
}
}
}
return map;
}
/**
*
* @param {string} path
* @param {object} data
*/
function writeXpnSourceMap(path, map) {
const obj = {
version: 1,
sources: [],
}
map.forEach((v, k) => {
obj.sources.push(v);
});
remove(path);
write(path, JSON.stringify(obj));
}
/**
* 清理掉空白再哈希,防止误格式化代码
* @param {string} str
*/
function hashStr(str) {
if (typeof str !== 'string') {
return null;
}
const noSpace = str.replace(/\s+/g, '');
return md5(noSpace);
}