├── .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 |
--------------------------------------------------------------------------------