├── .autod.conf.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── README.zh-CN.md ├── app ├── controller │ ├── file.js │ ├── home.js │ ├── pipeline.js │ └── templates.js ├── extend │ └── helper.js ├── middleware │ ├── bodyclothes.js │ └── static-cache-ignore.js ├── model │ └── template.js ├── router.js └── service │ └── db.js ├── appveyor.yml ├── config ├── config.default.js ├── config.local.js ├── config.prod.js └── plugin.js ├── package.json └── test └── app └── controller └── home.test.js /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | 'webstorm-disable-index', 24 | ], 25 | exclude: [ 26 | './test/fixtures', 27 | './dist', 28 | ], 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /logs 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | 14 | app/public/* 15 | !app/public/types 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | install: 6 | - npm i npminstall && npminstall 7 | script: 8 | - npm run ci 9 | after_script: 10 | - npminstall codecov && codecov 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [0.2.0](https://github.com/page-pipepline/pipeline-node-server/compare/v0.1.0...v0.2.0) (2018-11-15) 7 | 8 | 9 | ### Features 10 | 11 | * add react SSR ([b29a83a](https://github.com/page-pipepline/pipeline-node-server/commit/b29a83a)) 12 | * add support for database and template model ([07eaf94](https://github.com/page-pipepline/pipeline-node-server/commit/07eaf94)) 13 | 14 | 15 | 16 | 17 | # 0.1.0 (2018-07-12) 18 | 19 | 20 | ### Features 21 | 22 | * add library components drag-and-drop ([a2014de](https://github.com/page-pipepline/pipeline-node-server/commit/a2014de)) 23 | * remove library ([3b04b25](https://github.com/page-pipepline/pipeline-node-server/commit/3b04b25)) 24 | * 对编辑中的页面的 index.html 不使用缓存, 其他资源使用缓存 ([718995a](https://github.com/page-pipepline/pipeline-node-server/commit/718995a)) 25 | * 添加终端前台启动脚本 ([b405f83](https://github.com/page-pipepline/pipeline-node-server/commit/b405f83)) 26 | 27 | 28 | ### BREAKING CHANGES 29 | 30 | * not support library 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pipeline-node-server 2 | > 页面可视化搭建框架的后台服务 3 | 4 | ## 简介 5 | 提供模板生成页面的基本操作接口. 6 | 7 | ## 服务端脚本软件 8 | * unzip 9 | 10 | ## 准备工作 11 | * 需要在该项目同级建立`pipeline-resources`目录. 12 | * 到`pipeline-template`项目中生成模板压缩包`pipeline-template.zip`. 13 | * 添加模板文件 `pipeline-resources/template/1/pipeline-template.zip`.(目前路径写死) 14 | 15 | ## 非必须 16 | * 添加模板时上传的图片, 存储的是相对`pipeline-resources`的路径, 如果要本地访问到, 需要配置一下资源目录的nginx或代理. 17 | 编辑器会将 `1/thumbnail.png` -> `http://res.pipeline/1/thumbnail.png`. 18 | ``` 19 | # Whistle 代理配置 20 | # pipeline 21 | /http://res.pipeline/(.*)/ /Path/To/Your/pipeline-resources/template/$1 22 | ``` 23 | 24 | ## 启动 25 | ``` 26 | $ npm i 27 | $ npm run debug 28 | ``` 29 | 30 | ## QuickStart 31 | 32 | 33 | 34 | see [egg docs][egg] for more detail. 35 | 36 | ### Development 37 | 38 | ```bash 39 | $ npm i 40 | $ npm run dev 41 | $ open http://localhost:7001/ 42 | ``` 43 | 44 | ### Deploy 45 | 46 | ```bash 47 | $ npm start 48 | $ npm stop 49 | ``` 50 | 51 | ### npm scripts 52 | 53 | - Use `npm run lint` to check code style. 54 | - Use `npm test` to run unit test. 55 | - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. 56 | 57 | [egg]: https://eggjs.org 58 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # pipeline-node-server 2 | 3 | 4 | 5 | ## 快速入门 6 | 7 | 8 | 9 | 如需进一步了解,参见 [egg 文档][egg]。 10 | 11 | ### 本地开发 12 | 13 | ```bash 14 | $ npm i 15 | $ npm run dev 16 | $ open http://localhost:7001/ 17 | ``` 18 | 19 | ### 部署 20 | 21 | ```bash 22 | $ npm start 23 | $ npm stop 24 | ``` 25 | 26 | ### 单元测试 27 | 28 | - [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。 29 | - 断言库非常推荐使用 [power-assert]。 30 | - 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。 31 | 32 | ### 内置指令 33 | 34 | - 使用 `npm run lint` 来做代码风格检查。 35 | - 使用 `npm test` 来执行单元测试。 36 | - 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。 37 | 38 | 39 | [egg]: https://eggjs.org 40 | -------------------------------------------------------------------------------- /app/controller/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const Controller = require('egg').Controller; 7 | 8 | class File extends Controller { 9 | async upload() { 10 | const { ctx, config } = this; 11 | 12 | const stream = await ctx.getFileStream(); 13 | const templateId = stream.fields.templateId; 14 | const fileBuffer = await ctx.helper.upload.streamPromise(stream); 15 | const filePath = path.join(config.temporaryDir, templateId, stream.filename); 16 | 17 | console.log(config.temporaryDir); 18 | // 创建临时目录 19 | await ctx.helper.execShell(`mkdir -p ${config.temporaryDir}/${templateId}`); 20 | // 写入文件 21 | fs.writeFileSync(filePath, fileBuffer); 22 | 23 | this.ctx.body = { 24 | templateId, 25 | }; 26 | } 27 | } 28 | 29 | module.exports = File; 30 | -------------------------------------------------------------------------------- /app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class HomeController extends Controller { 6 | async index() { 7 | this.ctx.body = 'hi, egg'; 8 | } 9 | } 10 | 11 | module.exports = HomeController; 12 | -------------------------------------------------------------------------------- /app/controller/pipeline.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 模板生成页面操作接口 3 | * @Author: CntChen 4 | * @Date: 2018-03-29 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | 12 | const Controller = require('egg').Controller; 13 | 14 | // 将组件库源码放入页面工作管道 15 | const makePagePipelineTemplate = async (context, templateId, pageId) => { 16 | const { ctx, config, service } = context; 17 | 18 | const template = await service.db.queryTemplate({ 19 | conditions: { 20 | id: templateId, 21 | }, 22 | }); 23 | // const template = { 24 | // files: '/pipeline-resource/template/1/pipeline-template.zip', 25 | // }; 26 | console.log('test', template, template.files, template.files); 27 | const templateZipFilePath = path.join(config.resourcesPath.templateDir, template.files); 28 | const pagepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId); 29 | const templatepipelineDir = pagepipelineDir; 30 | 31 | await ctx.helper.execShell([ 32 | `mkdir -p ${templatepipelineDir}`, 33 | `unzip -o ${templateZipFilePath} -d ${templatepipelineDir}` ]); 34 | }; 35 | 36 | // 在页面工作管道备份模板(页面)配置数据 37 | const copyTemplateConfig = async (context, pageId) => { 38 | const { ctx, config } = context; 39 | 40 | const pagepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId); 41 | const templatepipelineDir = path.join(pagepipelineDir, 'server/config'); 42 | const baseConfigpipelinePath = path.join(templatepipelineDir, 'base-config.json'); 43 | const originBaseConfigpipelinePath = path.join(templatepipelineDir, 'base-config-origin.json'); 44 | const templatepipelinePath = path.join(templatepipelineDir, 'components.json'); 45 | const originTemplatepipelinePath = path.join(templatepipelineDir, 'components-origin.json'); 46 | 47 | // 复制模板配置文件, 做为页面重置的数据来源 48 | await ctx.helper.execShell([ 49 | `cp -rf ${baseConfigpipelinePath} ${originBaseConfigpipelinePath}`, 50 | `cp -rf ${templatepipelinePath} ${originTemplatepipelinePath}` ]); 51 | }; 52 | 53 | // 基于模板构建页面工作管道 54 | const makePagepipelineFromTemplate = async (context, templateId, pageId) => { 55 | const { ctx } = context; 56 | 57 | await makePagePipelineTemplate(context, templateId, pageId); 58 | await copyTemplateConfig(context, pageId); 59 | await ctx.helper.execShell([ 60 | `cd ./app/public/pipelines/${pageId}/server`, 61 | 'node node.js preview' ]); 62 | }; 63 | 64 | // 基于页面构建页面工作管道 65 | const makePagepipelineFromPage = async (context, templateId, pageId) => { 66 | const { ctx, config, service } = context; 67 | 68 | const page = await service.page.getPageById(pageId); 69 | 70 | const pageZipFilePath = path.join(config.resourcesPath.pageDir, page.files); 71 | const pagepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId); 72 | const templatepipelineDir = path.join(pagepipelineDir, 'server/config'); 73 | 74 | await makePagePipelineTemplate(context, templateId, pageId); 75 | await ctx.helper.execShell([ `unzip -o ${pageZipFilePath} -d ${templatepipelineDir}` ]); 76 | await copyTemplateConfig(context, pageId); 77 | await ctx.helper.execShell([ 78 | `cd ./app/public/pipelines/${pageId}/server`, 79 | 'node node.js preview' ]); 80 | }; 81 | 82 | // 构建用于发布页面源码 83 | const makePageActivity = async (context, pageId) => { 84 | const { ctx, config } = context; 85 | const pagepipelineServerDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server'); 86 | const pageActivityDir = path.join(config.baseDir, 'app/public/activities', pageId); 87 | 88 | // 复制 pipelines 到 activities, 并执行页面发布的构建 89 | await ctx.helper.execShell([ 90 | `mkdir -p ${pageActivityDir}`, 91 | `cp -rf ${pagepipelineServerDir} ${pageActivityDir}`, 92 | `cd ./app/public/activities/${pageId}/server`, 93 | 'node node.js release' ]); 94 | 95 | // 基于 dist 创建纯净的发布目录 96 | await ctx.helper.execShell([ 97 | `mkdir -p ./app/public/activities/${pageId}/${pageId}`, 98 | `cp -rf ./app/public/activities/${pageId}/server/dist/* ./app/public/activities/${pageId}/${pageId}`, 99 | `cd ./app/public/activities/${pageId}/${pageId}`, 100 | 'rm -f index-origin.html', 101 | 'rm -f vue-ssr-server-bundle.json', 102 | 'rm -f vue-ssr-client-manifest.json' ]); 103 | }; 104 | 105 | class EditController extends Controller { 106 | async prepareFromTemplate() { 107 | const { ctx } = this; 108 | const templateId = ctx.request.body.templateId; 109 | 110 | // 生成页面ID: timeStamp + 00 + 2位随机数 111 | const timeStranpStr = new Date().getTime(); 112 | const randomStr = Math.random().toString().slice(-2); 113 | const pageId = `${timeStranpStr}00${randomStr}`; 114 | 115 | await makePagepipelineFromTemplate(this, templateId, pageId); 116 | 117 | ctx.body = { pageId }; 118 | } 119 | 120 | async prepareFromPage() { 121 | const { ctx, service } = this; 122 | const pageId = ctx.request.body.pageId; 123 | 124 | const page = await service.page.getPageById(pageId); 125 | const templateId = page.templateId; 126 | 127 | await makePagepipelineFromPage(this, templateId, pageId); 128 | 129 | ctx.body = { pageId }; 130 | } 131 | 132 | async prepareForRelease() { 133 | const { ctx, service, config } = this; 134 | const pageId = ctx.request.body.pageId; 135 | const page = await service.page.getPageById(pageId); 136 | const templateId = page.templateId; 137 | 138 | const pageActivityDir = path.join(config.baseDir, 'app/public/activities', pageId); 139 | const pagepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId); 140 | 141 | const pagepipelineDirStat = await ctx.helper.file.fsStat(pagepipelineDir).catch(e => e); 142 | if (pagepipelineDirStat instanceof Error) { 143 | await makePagepipelineFromTemplate(this, templateId, pageId); 144 | } 145 | const pageActivityDirStat = await ctx.helper.file.fsStat(pageActivityDir).catch(e => e); 146 | if (pageActivityDirStat instanceof Error) { 147 | await makePageActivity(this, pageId); 148 | } 149 | 150 | ctx.body = { pageId }; 151 | } 152 | 153 | async getBaseConfig() { 154 | const { ctx, config } = this; 155 | const pageId = ctx.query.pageId; 156 | 157 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 158 | const dataPath = path.join(templatepipelineDir, 'base-config.json'); 159 | const dataStr = fs.readFileSync(dataPath, 'utf-8'); 160 | const content = JSON.parse(dataStr); 161 | ctx.body = content; 162 | } 163 | 164 | async putBaseConfig() { 165 | const { ctx, config } = this; 166 | const pageId = ctx.request.body.pageId; 167 | const baseConfig = ctx.request.body.baseConfig; 168 | const baseConfigStr = JSON.stringify(baseConfig, null, 2); 169 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 170 | const baseConfigPath = path.join(templatepipelineDir, 'base-config.json'); 171 | fs.writeFileSync(baseConfigPath, baseConfigStr, 'utf-8'); 172 | await ctx.helper.execShell([ 173 | 'pwd', 174 | `cd ./app/public/pipelines/${pageId}/server`, 175 | 'node node.js preview' ]); 176 | ctx.body = '修改页面基本配置成功.'; 177 | } 178 | 179 | async getBaseConfigSchema() { 180 | const { ctx, config } = this; 181 | const pageId = ctx.query.pageId; 182 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 183 | const schemaPath = path.join(templatepipelineDir, 'base-config-schema.json'); 184 | const schemaStr = fs.readFileSync(schemaPath, 'utf-8'); 185 | const content = JSON.parse(schemaStr); 186 | ctx.body = content; 187 | } 188 | 189 | async getTemplateComponents() { 190 | const { ctx, config } = this; 191 | const pageId = ctx.query.pageId; 192 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 193 | const dataPath = path.join(templatepipelineDir, 'components.json'); 194 | const dataStr = fs.readFileSync(dataPath, 'utf-8'); 195 | const content = JSON.parse(dataStr); 196 | ctx.body = content; 197 | } 198 | 199 | async putTemplateComponents() { 200 | const { ctx, config } = this; 201 | const pageId = ctx.request.body.pageId; 202 | const templateComponents = ctx.request.body.templateComponents; 203 | const templateComponentsStr = JSON.stringify(templateComponents, null, 2); 204 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 205 | const templateComonentsPath = path.join(templatepipelineDir, 'components.json'); 206 | fs.writeFileSync(templateComonentsPath, templateComponentsStr, 'utf-8'); 207 | await ctx.helper.execShell([ 208 | `cd ./app/public/pipelines/${pageId}/server`, 209 | 'node node.js preview' ]); 210 | ctx.body = '修改页面组件列表成功'; 211 | } 212 | 213 | async getComponentsSchema() { 214 | const { ctx, config } = this; 215 | const pageId = ctx.query.pageId; 216 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 217 | const schemaPath = path.join(templatepipelineDir, 'components-schema.json'); 218 | const schemaStr = fs.readFileSync(schemaPath, 'utf-8'); 219 | const content = JSON.parse(schemaStr); 220 | ctx.body = content; 221 | } 222 | 223 | async getLibraryComponentsInfo() { 224 | const { ctx, config } = this; 225 | const pageId = ctx.query.pageId; 226 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 227 | const componentsInfoPath = path.join(templatepipelineDir, 'components-info.json'); 228 | const componentsInfoStr = fs.readFileSync(componentsInfoPath, 'utf-8'); 229 | const content = JSON.parse(componentsInfoStr); 230 | 231 | ctx.body = content; 232 | } 233 | 234 | async getComponentsDefaultData() { 235 | const { ctx, config } = this; 236 | const pageId = ctx.query.pageId; 237 | const templatepipelineDir = path.join(config.baseDir, 'app/public/pipelines', pageId, 'server/config'); 238 | const componentsDefaultDataPath = path.join(templatepipelineDir, 'components-default-data.json'); 239 | const componentsDefaultDataStr = fs.readFileSync(componentsDefaultDataPath, 'utf-8'); 240 | const content = JSON.parse(componentsDefaultDataStr); 241 | console.log(componentsDefaultDataStr); 242 | ctx.body = content; 243 | } 244 | } 245 | 246 | module.exports = EditController; 247 | -------------------------------------------------------------------------------- /app/controller/templates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | class TemplateController extends app.Controller { 5 | async index() { 6 | const { ctx, service } = this; 7 | 8 | const dbParams = { 9 | conditions: ctx.request.query, 10 | }; 11 | 12 | const result = await service.db.queryManyTemplates(dbParams); 13 | ctx.body = result; 14 | } 15 | 16 | async show() { 17 | const { ctx, service } = this; 18 | 19 | const dbParams = { 20 | conditions: { 21 | id: ctx.params.id, 22 | }, 23 | }; 24 | 25 | const result = await service.db.queryTemplate(dbParams); 26 | ctx.body = result; 27 | } 28 | 29 | async create() { 30 | const { ctx, service, config } = this; 31 | 32 | const id = Number(ctx.request.body.id); 33 | 34 | const querydbParams = { 35 | conditions: { 36 | id, 37 | }, 38 | }; 39 | const templates = await service.db.queryManyTemplates(querydbParams); 40 | if (templates.length > 0) { 41 | throw new Error('Template exist.'); 42 | } 43 | 44 | const name = ctx.request.body.name; 45 | const fileName = ctx.request.body.fileName; 46 | const imageName = ctx.request.body.imageName; 47 | 48 | const files = `${id}/${fileName}`; 49 | const thumbnail = `${id}/${imageName}`; 50 | 51 | // 移动文件到资源目录 52 | await ctx.helper.execShell(`mkdir -p ${config.resourcesPath.templateDir}/${id}`); 53 | await ctx.helper.execShell(`mv ${config.temporaryDir}/${id}/* ${config.resourcesPath.templateDir}/${id}`); 54 | 55 | const dbParams = { 56 | conditions: {}, 57 | payload: { 58 | id, 59 | name, 60 | files, 61 | thumbnail, 62 | }, 63 | }; 64 | 65 | const result = await service.db.createTemplate(dbParams); 66 | ctx.body = result; 67 | } 68 | 69 | async update() { 70 | const { ctx, service, config } = this; 71 | 72 | const id = ctx.params.id; 73 | const body = ctx.request.body; 74 | 75 | const name = body.name; 76 | const fileName = body.fileName; 77 | const imageName = body.imageName; 78 | 79 | const payload = {}; 80 | if (name) { 81 | payload.name = name; 82 | } 83 | if (fileName) { 84 | const files = `${id}/${fileName}`; 85 | payload.files = files; 86 | } 87 | if (imageName) { 88 | const thumbnail = `${id}/${imageName}`; 89 | payload.thumbnail = thumbnail; 90 | } 91 | 92 | const dbParams = { 93 | conditions: { 94 | id, 95 | }, 96 | payload, 97 | }; 98 | 99 | const files = await ctx.helper.file.dir(`${config.temporaryDir}/${id}`); 100 | if (files.length > 0) { 101 | await ctx.helper.execShell(`mv ${config.temporaryDir}/${id}/* ${config.resourcesPath.templateDir}/${id}`); 102 | } 103 | 104 | const result = await service.db.updateTemplate(dbParams); 105 | ctx.body = result; 106 | } 107 | 108 | async destroy() { 109 | const { ctx, service } = this; 110 | 111 | const dbParams = { 112 | conditions: { 113 | id: ctx.params.id, 114 | }, 115 | }; 116 | 117 | const result = await service.db.deleteTemplate(dbParams); 118 | ctx.body = result; 119 | } 120 | 121 | async getTemplateId() { 122 | const { ctx } = this; 123 | ctx.body = ctx.helper.uuid.getUuid(); 124 | } 125 | } 126 | 127 | return TemplateController; 128 | }; 129 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const exec = require('child_process').exec; 5 | const generate = require('nanoid/generate'); 6 | 7 | module.exports = { 8 | file: { 9 | fsStat: path => new Promise((resolve, reject) => { 10 | fs.stat(path, (e, stat) => { 11 | if (e instanceof Error) { 12 | reject(e); 13 | } 14 | resolve(stat); 15 | }); 16 | }), 17 | dir: path => new Promise((resolve, reject) => { 18 | fs.readdir(path, (e, stat) => { 19 | if (e instanceof Error) { 20 | reject(e); 21 | } 22 | resolve(stat); 23 | }); 24 | }), 25 | }, 26 | execShell: function execShell(shellCommands = 'pwd') { 27 | const shellCommandArray = shellCommands instanceof Array ? shellCommands : [ shellCommands ]; 28 | const shellCommandLine = shellCommandArray.join(' && '); 29 | 30 | return new Promise((resolve, reject) => { 31 | exec(shellCommandLine, { 32 | cwd: this.config.baseDir, 33 | }, (error, stdout, stderr) => { 34 | if (error) { 35 | console.error(`exec error: ${error}`); 36 | reject(error); 37 | return; 38 | } 39 | if (stderr) { 40 | console.log(`stderr: ${stderr}`); 41 | reject(stderr); 42 | return; 43 | } 44 | console.log(`stdout: ${stdout}`); 45 | resolve(stdout); 46 | }); 47 | }); 48 | }, 49 | upload: { 50 | streamPromise: stream => new Promise((resolve, reject) => { 51 | const data = []; 52 | stream.on('data', chunk => { 53 | data.push(chunk); 54 | }); 55 | stream.on('end', () => { 56 | resolve(Buffer.concat(data)); 57 | }); 58 | stream.on('error', e => { 59 | reject(e); 60 | }); 61 | }), 62 | }, 63 | uuid: { 64 | getUuid: () => { 65 | return generate('01234567890', 8); 66 | }, 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /app/middleware/bodyclothes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | return async (ctx, next) => { 5 | try { 6 | await next(); 7 | 8 | if (ctx.status === 404 && !ctx.body) { 9 | throw new Error('404: 请求的接口不存在'); 10 | } 11 | 12 | const body = ctx.body || {}; 13 | ctx.body = { 14 | data: body, 15 | ret: '0', 16 | errMsg: '', 17 | }; 18 | 19 | // 记录接口调用日志 20 | ctx.logger.info(`${ctx.session.user}: ${JSON.stringify(body)}`); 21 | } catch (err) { 22 | ctx.body = { 23 | data: {}, 24 | ret: '1', 25 | errMsg: err.message, 26 | }; 27 | 28 | // 记录报错日志 29 | ctx.logger.error(err); 30 | } 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /app/middleware/static-cache-ignore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | module.exports = options => { 7 | return async (ctx, next) => { 8 | const reqPath = ctx.path; 9 | 10 | if (reqPath.startsWith('/public') && options.dontCache.some(item => item.test(reqPath))) { 11 | const body = fs.readFileSync(path.join(__dirname, '../', reqPath)).toString(); 12 | ctx.body = body; 13 | ctx.set('content-type', 'text/html, chartset=utf8'); 14 | ctx.set('cache-control', 'he, max-age=0'); 15 | return; 16 | } 17 | 18 | await next(); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /app/model/template.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 模板表 model 3 | * @Author: CntChen 4 | * @Date: 2018-11-02 5 | */ 6 | 7 | 'use strict'; 8 | 9 | module.exports = app => { 10 | const mongoose = app.mongoose; 11 | const Schema = mongoose.Schema; 12 | 13 | const TemplateSchema = new Schema({ 14 | id: { 15 | type: Number, 16 | unique: true, 17 | required: true, 18 | }, 19 | name: { 20 | type: String, 21 | required: true, 22 | }, 23 | createTime: { 24 | type: Date, 25 | default: Date.now, 26 | }, 27 | updateTime: { 28 | type: Date, 29 | default: Date.now, 30 | }, 31 | files: { 32 | type: String, 33 | required: true, 34 | }, 35 | thumbnail: { 36 | type: String, 37 | default: 'https://avatars3.githubusercontent.com/u/38666040', 38 | }, 39 | }); 40 | 41 | return mongoose.model('Template', TemplateSchema); 42 | }; 43 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | const { router, controller } = app; 8 | 9 | // 模板相关接口 10 | app.resources('/templates', controller.templates); 11 | router.get('/templateid', controller.templates.getTemplateId); 12 | 13 | // 模板生成页面相关接口 14 | router.post('/pipeline/prepareFromTemplate', controller.pipeline.prepareFromTemplate); 15 | router.post('/pipeline/prepareFromPage', controller.pipeline.prepareFromPage); 16 | router.post('/pipeline/prepareForRelease', controller.pipeline.prepareForRelease); 17 | router.get('/pipeline/baseConfig', controller.pipeline.getBaseConfig); 18 | router.put('/pipeline/baseConfig', controller.pipeline.putBaseConfig); 19 | router.get('/pipeline/baseConfigSchema', controller.pipeline.getBaseConfigSchema); 20 | router.get('/pipeline/templateComponents', controller.pipeline.getTemplateComponents); 21 | router.put('/pipeline/templateComponents', controller.pipeline.putTemplateComponents); 22 | router.get('/pipeline/libraryComponentsInfo', controller.pipeline.getLibraryComponentsInfo); 23 | router.get('/pipeline/componentsSchema', controller.pipeline.getComponentsSchema); 24 | router.get('/pipeline/componentsDefaultData', controller.pipeline.getComponentsDefaultData); 25 | 26 | // 文件上传 27 | router.post('/file/upload', controller.file.upload); 28 | 29 | 30 | router.get('/', controller.home.index); 31 | }; 32 | -------------------------------------------------------------------------------- /app/service/db.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 基本 mongodb 操作函数 3 | * @Author: CntChen 4 | * @Date: 2018-11-02 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const DBLOG_PREFIX = '[db-service]:'; 10 | 11 | const existCheck = (object, keysWithErrorInfo, prefix = '') => { 12 | const keys = Object.keys(keysWithErrorInfo); 13 | const errors = keys.map(key => { 14 | if (object[key]) { 15 | return true; 16 | } 17 | return new Error(`${prefix}, ${key}, ${keysWithErrorInfo[key]}`); 18 | }).filter(item => item instanceof Error); 19 | 20 | // return first error or true 21 | if (errors.length) { 22 | return errors[0]; 23 | } 24 | return true; 25 | }; 26 | 27 | module.exports = app => { 28 | class db extends app.Service { 29 | // CRUD for Template 30 | async queryManyTemplates({ conditions = {} } = {}) { 31 | const query = await app.model.Template.find(conditions) 32 | .catch(err => { 33 | app.logger.error(DBLOG_PREFIX, err); 34 | throw err; 35 | }); 36 | return query; 37 | } 38 | async queryTemplate({ conditions = {} } = {}) { 39 | const checkResult = existCheck(conditions, { 40 | id: 'should not be undefined.', 41 | }, 'queryTemplate'); 42 | if (checkResult instanceof Error) { 43 | throw new Error(`${DBLOG_PREFIX} ${checkResult.toString()}`); 44 | } 45 | 46 | const query = await app.model.Template.findOne(conditions) 47 | .catch(err => { 48 | app.logger.error(DBLOG_PREFIX, err); 49 | throw err; 50 | }); 51 | return query; 52 | } 53 | async createTemplate({ payload = {} }) { 54 | const newTemplate = new app.model.Template(payload); 55 | const query = await newTemplate.save() 56 | .catch(err => { 57 | app.logger.error(DBLOG_PREFIX, err); 58 | throw err; 59 | }); 60 | return query; 61 | } 62 | async updateTemplate({ conditions = {}, payload = {} }) { 63 | const checkResult = existCheck(conditions, { 64 | id: 'should not be undefined.', 65 | }, 'updateTemplate'); 66 | if (checkResult instanceof Error) { 67 | throw new Error(`${DBLOG_PREFIX} ${checkResult.toString()}`); 68 | } 69 | 70 | const query = await app.model.Template.updateOne(conditions, payload) 71 | .catch(err => { 72 | app.logger.error(DBLOG_PREFIX, err); 73 | throw err; 74 | }); 75 | return query; 76 | } 77 | async deleteTemplate({ conditions = {} }) { 78 | const checkResult = existCheck(conditions, { 79 | id: 'should not be undefined.', 80 | }, 'deleteTemplate'); 81 | if (checkResult instanceof Error) { 82 | throw new Error(`${DBLOG_PREFIX} ${checkResult.toString()}`); 83 | } 84 | 85 | const query = await app.model.Template.deleteOne(conditions) 86 | .catch(err => { 87 | app.logger.error(DBLOG_PREFIX, err); 88 | throw err; 89 | }); 90 | return query; 91 | } 92 | } 93 | 94 | return db; 95 | }; 96 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = appInfo => { 6 | const config = exports = {}; 7 | 8 | // 服务采用 7011 端口 9 | config.cluster = { 10 | listen: { 11 | port: 7011, 12 | }, 13 | }; 14 | 15 | config.middleware = [ 'staticCacheIgnore', 'bodyclothes' ]; 16 | config.keys = appInfo.name + '_pipeline'; 17 | 18 | config.static = { 19 | enable: true, 20 | ignore: ctx => [ /public\/pipelines\/\d+\/server\/dist\/index\.html/ ].some(item => item.test(ctx.path)), 21 | }; 22 | 23 | config.staticCacheIgnore = { 24 | dontCache: [ /public\/pipelines\/\d+\/server\/dist\/index\.html/ ], 25 | }; 26 | 27 | config.security = { 28 | csrf: { 29 | enable: false, 30 | }, 31 | // 编辑中的的页面在 iframe 中浏览, 所以需要允许 32 | xframe: { 33 | enable: false, 34 | }, 35 | }; 36 | 37 | exports.logger = { 38 | dir: path.join(__dirname, '..', 'logs'), 39 | }; 40 | 41 | config.cors = { 42 | origin: ctx => ctx.get('origin'), 43 | credentials: true, 44 | }; 45 | 46 | // mongodb 连接 47 | const modeConfig = { 48 | mongo: { 49 | user: 'mongouser', 50 | password: encodeURIComponent('password'), 51 | host: 'localhost', 52 | port: '27017', 53 | DB: 'pipeline', 54 | authSource: 'admin', 55 | }, 56 | }; 57 | config.mongoose = { 58 | url: `mongodb://${modeConfig.mongo.user}:${modeConfig.mongo.password}@${modeConfig.mongo.host}:${modeConfig.mongo.port}/${modeConfig.mongo.DB}?authSource=${modeConfig.mongo.authSource}`, 59 | options: { 60 | poolSize: 16, 61 | reconnectTries: Number.MAX_VALUE, 62 | reconnectInterval: 500, 63 | bufferMaxEntries: 0, 64 | }, 65 | }; 66 | 67 | // 与 node 源码同一级 68 | const resourceBaseDir = path.join(__dirname, '..', '..', 'pipeline-resources'); 69 | config.resourcesPath = { 70 | templateDir: path.join(resourceBaseDir, 'template'), 71 | pageDir: path.join(resourceBaseDir, 'page'), 72 | }; 73 | 74 | config.publicDir = path.join(__dirname, '..', 'app', 'public'); 75 | config.temporaryDir = path.join(__dirname, '..', 'app', 'public', 'temp'); 76 | 77 | return config; 78 | }; 79 | -------------------------------------------------------------------------------- /config/config.local.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const config = exports = {}; 5 | 6 | return config; 7 | }; 8 | -------------------------------------------------------------------------------- /config/config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const config = exports = {}; 5 | 6 | /** 7 | * 通过 static 中间件的 ignore 和自定义不缓存文件的中间件 static-cache-ignore, 实现 index.html 文件的不缓存 8 | * 以下代码不需要, 使用会降低前端页面性能: 页面刷新都重新请求资源 9 | */ 10 | // 覆盖 egg-static 生产模式对静态资源的缓存, 因为模板生成的页面放在静态资源中预览, 需要实时获取最新的渲染文件 11 | // exports.static = { 12 | // maxAge: 0, 13 | // buffer: false, 14 | // }; 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.cors = { 4 | enable: true, 5 | package: 'egg-cors', 6 | }; 7 | 8 | exports.mongoose = { 9 | enable: true, 10 | package: 'egg-mongoose', 11 | }; 12 | 13 | // had enabled by egg 14 | // exports.static = true; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pipeline-node-server", 3 | "version": "0.2.0", 4 | "description": "", 5 | "private": true, 6 | "dependencies": { 7 | "archiver": "^2.1.1", 8 | "await-stream-ready": "^1.0.1", 9 | "egg": "^2.2.1", 10 | "egg-cors": "^2.0.0", 11 | "egg-mongoose": "^3.1.0", 12 | "egg-scripts": "^2.5.0", 13 | "egg-validate": "^1.0.0", 14 | "nanoid": "^2.0.1", 15 | "react": "^16.4.1", 16 | "react-dom": "^16.4.1", 17 | "stream-wormhole": "^1.0.3", 18 | "vue": "^2.5.15", 19 | "vue-server-renderer": "^2.5.15" 20 | }, 21 | "devDependencies": { 22 | "apidoc": "^0.17.6", 23 | "autod": "^3.0.1", 24 | "autod-egg": "^1.0.0", 25 | "egg-bin": "^4.3.5", 26 | "egg-ci": "^1.8.0", 27 | "egg-mock": "^3.14.0", 28 | "eslint": "^4.11.0", 29 | "eslint-config-egg": "^6.0.0", 30 | "jsdoc": "^3.5.5", 31 | "standard-version": "^4.4.0", 32 | "webstorm-disable-index": "^1.2.0" 33 | }, 34 | "engines": { 35 | "node": ">=8.9.0" 36 | }, 37 | "scripts": { 38 | "start": "egg-scripts start --daemon --title=egg-server-pipeline-node-server", 39 | "docker-start": "egg-scripts start --title=egg-server-pipeline-node-server", 40 | "stop": "egg-scripts stop --title=egg-server-pipeline-node-server", 41 | "dev": "egg-bin dev", 42 | "debug": "egg-bin debug", 43 | "test": "npm run lint -- --fix && npm run test-local", 44 | "test-local": "egg-bin test", 45 | "doc": "node_modules/apidoc/bin/apidoc -i app/controller -o app/public/docs", 46 | "cov": "egg-bin cov", 47 | "lint": "eslint .", 48 | "ci": "npm run lint && npm run cov", 49 | "autod": "autod", 50 | "release": "standard-version" 51 | }, 52 | "ci": { 53 | "version": "8" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "" 58 | }, 59 | "author": "", 60 | "license": "MIT" 61 | } 62 | -------------------------------------------------------------------------------- /test/app/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | 5 | describe('test/app/controller/home.test.js', () => { 6 | 7 | it('should assert', function* () { 8 | const pkg = require('../../../package.json'); 9 | assert(app.config.keys.startsWith(pkg.name)); 10 | 11 | // const ctx = app.mockContext({}); 12 | // yield ctx.service.xx(); 13 | }); 14 | 15 | it('should GET /', () => { 16 | return app.httpRequest() 17 | .get('/') 18 | .expect('hi, egg') 19 | .expect(200); 20 | }); 21 | }); 22 | --------------------------------------------------------------------------------