UNPKG

xpn-cli

Version:

橡皮泥脚手架,最灵活的代码生成平台

417 lines (385 loc) 12.1 kB
#!/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); }