whistle.mockbubu 1.0.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,18 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ indent_size = 2
8
+ indent_style = space
9
+ insert_final_newline = true
10
+ max_line_length = 80
11
+ trim_trailing_whitespace = true
12
+
13
+ [*.md]
14
+ max_line_length = 0
15
+ trim_trailing_whitespace = false
16
+
17
+ [COMMIT_EDITMSG]
18
+ max_line_length = 0
package/.gitignore ADDED
@@ -0,0 +1,63 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+
8
+ # Runtime data
9
+ pids
10
+ *.pid
11
+ *.seed
12
+ *.pid.lock
13
+
14
+ # Directory for instrumented libs generated by jscoverage/JSCover
15
+ lib-cov
16
+
17
+ # Coverage directory used by tools like istanbul
18
+ coverage
19
+
20
+ # nyc test coverage
21
+ .nyc_output
22
+
23
+ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24
+ .grunt
25
+
26
+ # Bower dependency directory (https://bower.io/)
27
+ bower_components
28
+
29
+ # node-waf configuration
30
+ .lock-wscript
31
+
32
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
33
+ build/Release
34
+
35
+ # Dependency directories
36
+ node_modules/
37
+ jspm_packages/
38
+
39
+ # TypeScript v1 declaration files
40
+ typings/
41
+
42
+ # Optional npm cache directory
43
+ .npm
44
+
45
+ # Optional eslint cache
46
+ .eslintcache
47
+
48
+ # Optional REPL history
49
+ .node_repl_history
50
+
51
+ # Output of 'npm pack'
52
+ *.tgz
53
+
54
+ # Yarn Integrity file
55
+ .yarn-integrity
56
+
57
+ # dotenv environment variables file
58
+ .env
59
+
60
+ # next.js build output
61
+ .next
62
+ /.history
63
+ /dist
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # whistle.mockbubu
2
+
3
+ whistle.mockbubu 是[whistle](https://github.com/avwo/whistle)的一个扩展脚本插件,包括以下功能
4
+
5
+ - 实时生成捕获接口的 mock 文件
6
+ - 管理 mock 文件
7
+
8
+ # 安装
9
+
10
+ $ w2 install whistle.mockbubu
11
+
12
+ # 用法
13
+
14
+ ### 配置 [whistle 规则](https://avwo.github.io/whistle/rules/)
15
+
16
+ > mode 有三种选项 pathname | href | pattern,默认是 pathname
17
+
18
+ ```text
19
+ pattern mockbubu://[mode]
20
+ ```
21
+
22
+ ### 生成 mock 文件
23
+
24
+ ---
25
+
26
+ #### pathname 模式:使用接口的 origin + pathname 作为文件名
27
+
28
+ ```text
29
+ pattern mockbubu:// 或者 pattern mockbubu://pathname
30
+ ```
31
+
32
+ ---
33
+
34
+ #### href 模式:使用接口的 路径 作为文件名
35
+
36
+ ```text
37
+ pattern mockbubu://href
38
+
39
+ ```
40
+
41
+ ---
42
+
43
+ #### pattern 模式:使用 pattern 作为文件名
44
+
45
+ ```text
46
+ pattern mockbubu://pattern
47
+ ```
48
+
49
+ ---
50
+
51
+ ### 开始 mock
52
+
53
+ 第一步:开启文件的 mock 开关,系统生成一条规则:
54
+
55
+ ```
56
+ 文件名 resBody:{文件内容}
57
+ ```
58
+
59
+ 第二步:在 Response 面板管理文件内容
60
+
61
+ 手动添加文件
62
+
63
+ - 如果需要提前生成 mock 文件,可手动添加(通常不需要手动添加,通过实时捕获接口即可生成 mock 文件)
64
+
65
+ 编辑文件
66
+
67
+ - 支持文件编辑
68
+
69
+ 多版本
70
+
71
+ - 支持新增不同版本的文件内容
72
+ - 其中 source 版本是默认生成的,是原始接口响应数据,支持修改
73
+
74
+ 切换 mock 文件
75
+
76
+ - mock 返回内容使用当前选中的版本
77
+
78
+ ![alt text](step1.png)
79
+
80
+ ![alt text](step2.png)
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ exports.uiServer = require('./lib/uiServer')
2
+ // exports.rulesServer = require('./lib/rulesServer')
3
+ // exports.tunnelRulesServer = require('./lib/tunnelRulesServer');
4
+ exports.resRulesServer = require('./lib/resRulesServer')
5
+ // exports.statsServer = require('./lib/statsServer')
6
+ // exports.resStatsServer = require('./lib/resStatsServer')
7
+ exports.server = require('./lib/server')
8
+ // exports.reqRead = require('./lib/reqRead')
9
+ // exports.reqWrite = require('./lib/reqWrite');
10
+ // exports.resRead = require('./lib/resRead')
11
+ // exports.resWrite = require('./lib/resWrite')
12
+ // exports.wsReqRead = require('./lib/wsReqRead');
13
+ // exports.wsReqWrite = require('./lib/wsReqWrite');
14
+ // exports.wsResRead = require('./lib/wsResRead');
15
+ // exports.wsResWrite = require('./lib/wsResWrite');
16
+ // exports.tunnelReqRead = require('./lib/tunnelReqRead');
17
+ // exports.tunnelReqWrite = require('./lib/tunnelReqWrite');
18
+ // exports.tunnelResRead = require('./lib/tunnelResRead');
19
+ // exports.tunnelResWrite = require('./lib/tunnelResWrite');
package/lib/const.js ADDED
@@ -0,0 +1,28 @@
1
+ exports.RuleValueMap = {
2
+ href: 'href',
3
+ pathname: 'pathname',
4
+ pattern: 'pattern',
5
+ }
6
+
7
+ exports.RangeMap = {
8
+ MOCKING: 'mocking',
9
+ NOT_MOCKING: 'not-mocking',
10
+ LOCKED: 'locked',
11
+ }
12
+
13
+ exports.RangeFilterMap = {
14
+ 'mocking': { key: 'mock', value: true, method: 'equal' },
15
+ 'not-mocking': { key: 'mock', value: false, method: 'equal' },
16
+ 'locked': { key: 'locked', value: true, method: 'equal' },
17
+ }
18
+
19
+ exports.RuleFilterMap = {
20
+ 'href': { key: 'ruleValue', value: 'href', method: 'equal' },
21
+ 'pathname': { key: 'ruleValue', value: 'pathname', method: 'equal' },
22
+ 'pattern': { key: 'ruleValue', value: 'pattern', method: 'equal' },
23
+ }
24
+
25
+ exports.LockedFilterMap = {
26
+ 'locked': { key: 'locked', value: true, method: 'equal' },
27
+ 'unlocked': { key: 'locked', value: false, method: 'equal' },
28
+ }
@@ -0,0 +1,9 @@
1
+ module.exports = (server, options) => {
2
+ server.on('request', (req, res) => {
3
+ if (req.originalReq.headers['from-res-cache']) {
4
+ res.end('* style://color=red')
5
+ return
6
+ }
7
+ res.end()
8
+ })
9
+ }
package/lib/server.js ADDED
@@ -0,0 +1,107 @@
1
+ const {
2
+ getProperty,
3
+ setProperty,
4
+ getFilename,
5
+ getRule,
6
+ isJsonReq,
7
+ setApiListUpdated,
8
+ getVersionContent,
9
+ handleBuffer2String,
10
+ withTryCatch,
11
+ } = require('./utils')
12
+ const qs = require('qs')
13
+
14
+ module.exports = (server, { storage }) => {
15
+ server.on('request', (req, res) => {
16
+ const { originalReq, method } = req
17
+ const { headers, ruleValue, url, pattern } = originalReq
18
+ const filename = getFilename(originalReq)
19
+ const rule = getRule(originalReq)
20
+
21
+ // 非json请求直接透传
22
+ if (!isJsonReq(headers)) return req.passThrough()
23
+
24
+ // 获取请求信息
25
+ let str = ''
26
+ req.on('data', (chunk) => {
27
+ str += chunk
28
+ })
29
+ req.on('end', () => {
30
+ setProperty(storage, filename, { payload: str })
31
+ })
32
+ setProperty(storage, filename, {
33
+ query: qs.parse(new URL(url).searchParams.toString()),
34
+ })
35
+
36
+ try {
37
+ const { mock, mockVersion } = getProperty(storage, filename) || {}
38
+ const sessionCache = storage.readFile(filename)
39
+
40
+ if (mock) {
41
+ if (sessionCache) {
42
+ const resCache = JSON.parse(sessionCache)?.res
43
+ const { statusCode, statusMessage, headers } = resCache
44
+
45
+ headers['from-res-cache'] = 'true'
46
+ delete headers['content-encoding']
47
+ delete headers['content-length']
48
+ res.writeHead(statusCode, statusMessage, headers)
49
+ if (mockVersion) {
50
+ const mockVersionContent = getVersionContent({ storage, filename, versionName: mockVersion })
51
+ res.end(JSON.stringify(mockVersionContent))
52
+ } else {
53
+ res.end(resCache.body)
54
+ }
55
+ }
56
+ } else {
57
+ // 已有缓存则直接透传
58
+ if (sessionCache) return req.passThrough()
59
+
60
+ // 命中插件规则,没有勾选mock的缓存最新的接口数据
61
+ const client = req.request((svrRes) => {
62
+ const encoding = svrRes.headers['content-encoding']
63
+ let body
64
+
65
+ svrRes.on('data', (data) => {
66
+ body = body ? Buffer.concat([body, data]) : data
67
+ })
68
+ svrRes.on('end', withTryCatch(async () => {
69
+ if (!body) return
70
+
71
+ const content = await handleBuffer2String({ body, encoding })
72
+ // 获取完整的抓包数据,要等待响应完成
73
+ req.getSession(async (session) => {
74
+ // 如果设置了 enable://hide 会获取到空数据
75
+ if (!session) {
76
+ return
77
+ }
78
+ setProperty(storage, filename, {
79
+ method,
80
+ rule,
81
+ status: session.res.statusCode,
82
+ pattern,
83
+ ruleValue: ruleValue || 'pathname',
84
+ url,
85
+ mock: false,
86
+ locked: false,
87
+ date: Date.now(),
88
+ mockTime: null,
89
+ })
90
+
91
+ const tempSession = JSON.parse(JSON.stringify(session))
92
+ tempSession.res.body = content
93
+ storage.writeFile(filename, JSON.stringify(tempSession))
94
+
95
+ setApiListUpdated(storage, true)
96
+ })
97
+ }))
98
+ })
99
+
100
+ req.pipe(client)
101
+ req.passThrough()
102
+ }
103
+ } catch (error) {
104
+ req.passThrough()
105
+ }
106
+ })
107
+ }
@@ -0,0 +1,35 @@
1
+ const Koa = require('koa')
2
+ const bodyParser = require('koa-bodyparser')
3
+ const onerror = require('koa-onerror')
4
+ const serve = require('koa-static')
5
+ const path = require('path')
6
+ const router = require('koa-router')()
7
+ const setupRouter = require('./router')
8
+ const cors = require('koa2-cors')
9
+ const MAX_AGE = 1000 * 60 * 5
10
+
11
+ module.exports = (server, options) => {
12
+ const app = new Koa()
13
+
14
+ app.proxy = true
15
+ app.silent = true
16
+ onerror(app)
17
+ setupRouter(router, options)
18
+ app.use(
19
+ cors({
20
+ origin: '*',
21
+ maxAge: 10,
22
+ credentials: true,
23
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
24
+ }),
25
+ )
26
+ app.use(bodyParser({
27
+ jsonLimit: '10mb',
28
+ formLimit: '10mb',
29
+ textLimit: '10mb',
30
+ }))
31
+ app.use(router.routes())
32
+ app.use(router.allowedMethods())
33
+ app.use(serve(path.join(__dirname, '../../public'), { maxage: MAX_AGE }))
34
+ server.on('request', app.callback())
35
+ }
@@ -0,0 +1,171 @@
1
+ const {
2
+ updateFile,
3
+ removeFile,
4
+ writeFile,
5
+ readFile,
6
+ setProperty,
7
+ getProperty,
8
+ getApiListUpdated,
9
+ setApiListUpdated,
10
+ } = require('../../utils')
11
+ const {
12
+ createErrorResponse,
13
+ createSuccessResponse,
14
+ handleFilterList,
15
+ getFullDataList,
16
+ } = require('../util')
17
+ const versionRouter = require('./version-router')
18
+
19
+ module.exports = (router) => {
20
+ // 文件版本路由
21
+ versionRouter(router)
22
+
23
+ // 获取列表数据接口
24
+ router.post('/cgi-bin/mockbubu/api-list', async (ctx) => {
25
+ try {
26
+ const { localStorage } = ctx.req
27
+
28
+ const filteredList = handleFilterList(
29
+ getFullDataList(localStorage),
30
+ ctx.request.body,
31
+ )
32
+
33
+ setApiListUpdated(localStorage, false)
34
+ ctx.body = createSuccessResponse(filteredList || [])
35
+ } catch (error) {
36
+ ctx.body = createErrorResponse(error.message)
37
+ }
38
+ })
39
+
40
+ // 检测接口是否更新
41
+ router.get('/cgi-bin/mockbubu/check-api-list', (ctx) => {
42
+ try {
43
+ const { localStorage } = ctx.req
44
+ const updated = getApiListUpdated(localStorage)
45
+ ctx.body = createSuccessResponse(updated)
46
+ } catch (error) {
47
+ ctx.body = createErrorResponse(error.message)
48
+ }
49
+ })
50
+
51
+ // 更新文件详情接口
52
+ router.post('/cgi-bin/mockbubu/update-api-data', (ctx) => {
53
+ try {
54
+ const { localStorage } = ctx.req
55
+ const { name, data } = ctx.request.body
56
+
57
+ updateFile(localStorage, name, data)
58
+ const file = readFile(localStorage, name)
59
+ const props = getProperty(localStorage, name) || {}
60
+
61
+ ctx.body = createSuccessResponse({
62
+ ...props,
63
+ name,
64
+ data: file,
65
+ })
66
+ } catch (error) {
67
+ ctx.body = createErrorResponse(error.message)
68
+ }
69
+ })
70
+
71
+ // 获取文件详情接口
72
+ router.post('/cgi-bin/mockbubu/get-api-data', (ctx) => {
73
+ try {
74
+ const { localStorage } = ctx.req
75
+ const { name } = ctx.request.body
76
+
77
+ const file = readFile(localStorage, name)
78
+ const props = getProperty(localStorage, name) || {}
79
+
80
+ ctx.body = createSuccessResponse({
81
+ ...props,
82
+ name,
83
+ data: file,
84
+ })
85
+ } catch (error) {
86
+ ctx.body = createErrorResponse(error.message)
87
+ }
88
+ })
89
+
90
+ // 新增mock接口
91
+ router.post('/cgi-bin/mockbubu/create-api-data', (ctx) => {
92
+ try {
93
+ const { localStorage } = ctx.req
94
+ const { name, content, ruleValue } = ctx.request.body
95
+
96
+ writeFile({
97
+ storage: localStorage,
98
+ filename: name,
99
+ body: content,
100
+ properties: { ruleValue, mock: true },
101
+ })
102
+
103
+ ctx.body = createSuccessResponse(null, '创建成功')
104
+ } catch (error) {
105
+ ctx.body = createErrorResponse(error.message)
106
+ }
107
+ })
108
+
109
+ // 修改接口mock开关
110
+ router.post('/cgi-bin/mockbubu/update-api-mock', (ctx) => {
111
+ try {
112
+ const { localStorage } = ctx.req
113
+ const { name, mock } = ctx.request.body
114
+
115
+ setProperty(localStorage, name, { mock })
116
+ ctx.body = createSuccessResponse(null, '更新成功')
117
+ } catch (error) {
118
+ ctx.body = createErrorResponse(error.message)
119
+ }
120
+ })
121
+
122
+ // 修改接口lock开关
123
+ router.post('/cgi-bin/mockbubu/update-api-lock', (ctx) => {
124
+ try {
125
+ const { localStorage } = ctx.req
126
+ const { name, locked } = ctx.request.body
127
+
128
+ setProperty(localStorage, name, { locked })
129
+ ctx.body = createSuccessResponse(null, '更新成功')
130
+ } catch (error) {
131
+ ctx.body = createErrorResponse(error.message)
132
+ }
133
+ })
134
+
135
+ // 删除接口数据
136
+ router.post('/cgi-bin/mockbubu/delete-api', (ctx) => {
137
+ try {
138
+ const { localStorage } = ctx.req
139
+ const { name } = ctx.request.body
140
+
141
+ removeFile(localStorage, name)
142
+ ctx.body = createSuccessResponse(null, '删除成功')
143
+ } catch (error) {
144
+ ctx.body = createErrorResponse(error.message)
145
+ }
146
+ })
147
+
148
+ // 批量删除接口
149
+ router.post('/cgi-bin/mockbubu/batch-delete-api', (ctx) => {
150
+ try {
151
+ const { localStorage } = ctx.req
152
+
153
+ const filteredList = handleFilterList(getFullDataList(localStorage), {
154
+ ...ctx.request.body,
155
+ locked: 'unlocked', // 锁定的文件不可批量删除
156
+ })
157
+ // 批量删除
158
+ filteredList.forEach((item) => {
159
+ try {
160
+ removeFile(localStorage, item.name)
161
+ } catch (error) {
162
+ console.error(`删除文件 ${item.name} 失败:`, error.message)
163
+ }
164
+ })
165
+
166
+ ctx.body = createSuccessResponse(null, '删除成功')
167
+ } catch (error) {
168
+ ctx.body = createErrorResponse(error.message)
169
+ }
170
+ })
171
+ }
@@ -0,0 +1,127 @@
1
+ const {
2
+ addNewVersion,
3
+ updateVersionContent,
4
+ deleteVersion,
5
+ updateVersionName,
6
+ getVersions,
7
+ setProperty,
8
+ getPropertyAttr,
9
+ removePropertyAttr,
10
+ } = require('../../utils')
11
+ const { createErrorResponse, createSuccessResponse } = require('../util')
12
+
13
+
14
+ module.exports = (router) => {
15
+ // 新增版本
16
+ router.post('/cgi-bin/mockbubu/add-new-version', (ctx) => {
17
+ try {
18
+ const { localStorage } = ctx.req
19
+ const { versionName, content = {}, name } = ctx.request.body || {}
20
+
21
+ addNewVersion({
22
+ storage: localStorage,
23
+ filename: name,
24
+ versionName,
25
+ content,
26
+ })
27
+
28
+ ctx.body = createSuccessResponse({
29
+ filename: versionName,
30
+ content,
31
+ }, '版本创建成功')
32
+ } catch (error) {
33
+ ctx.body = createErrorResponse(error.message)
34
+ }
35
+ })
36
+
37
+ // 删除版本
38
+ router.post('/cgi-bin/mockbubu/delete-version', (ctx) => {
39
+ try {
40
+ const { localStorage } = ctx.req
41
+ const { versionName, name } = ctx.request.body || {}
42
+
43
+ const mockVersion = getPropertyAttr(localStorage, name, 'mockVersion')
44
+
45
+ // 如果删除的是当前mock版本,则清除mock版本设置
46
+ if (versionName === mockVersion) {
47
+ removePropertyAttr(localStorage, name, 'mockVersion')
48
+ }
49
+
50
+ deleteVersion({
51
+ storage: localStorage,
52
+ filename: name,
53
+ versionName,
54
+ })
55
+
56
+ ctx.body = createSuccessResponse(null, '版本删除成功')
57
+ } catch (error) {
58
+ ctx.body = createErrorResponse(error.message)
59
+ }
60
+ })
61
+
62
+ // 更新版本内容
63
+ router.post('/cgi-bin/mockbubu/update-version-content', (ctx) => {
64
+ try {
65
+ const { localStorage } = ctx.req
66
+ const { versionName, content, name } = ctx.request.body || {}
67
+
68
+ updateVersionContent({
69
+ storage: localStorage,
70
+ filename: name,
71
+ versionName,
72
+ content,
73
+ })
74
+
75
+ ctx.body = createSuccessResponse(null, '版本内容更新成功')
76
+ } catch (error) {
77
+ ctx.body = createErrorResponse(error.message)
78
+ }
79
+ })
80
+
81
+ // 更新版本名称
82
+ router.post('/cgi-bin/mockbubu/update-version-name', (ctx) => {
83
+ try {
84
+ const { localStorage } = ctx.req
85
+ const { versionName, name, newVersion } = ctx.request.body || {}
86
+
87
+ updateVersionName({
88
+ storage: localStorage,
89
+ filename: name,
90
+ versionName,
91
+ newVersion,
92
+ })
93
+
94
+ ctx.body = createSuccessResponse(null, '版本名称更新成功')
95
+ } catch (error) {
96
+ ctx.body = createErrorResponse(error.message)
97
+ }
98
+ })
99
+
100
+ // 获取版本列表
101
+ router.post('/cgi-bin/mockbubu/get-versions', (ctx) => {
102
+ try {
103
+ const { localStorage } = ctx.req
104
+ const { name } = ctx.request.body || {}
105
+
106
+ const list = getVersions({ storage: localStorage, filename: name }) || []
107
+
108
+ ctx.body = createSuccessResponse(list, '获取版本列表成功')
109
+ } catch (error) {
110
+ ctx.body = createErrorResponse(error.message)
111
+ }
112
+ })
113
+
114
+ // 设置mock的版本
115
+ router.post('/cgi-bin/mockbubu/set-mock-version', (ctx) => {
116
+ try {
117
+ const { localStorage } = ctx.req
118
+ const { name, versionName } = ctx.request.body || {}
119
+
120
+ setProperty(localStorage, name, { mockVersion: versionName })
121
+
122
+ ctx.body = createSuccessResponse(null, 'Mock版本设置成功')
123
+ } catch (error) {
124
+ ctx.body = createErrorResponse(error.message)
125
+ }
126
+ })
127
+ }