whistle.mockbubu 2.0.0 → 2.1.0

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.
Files changed (56) hide show
  1. package/README.md +38 -0
  2. package/index.js +3 -3
  3. package/lib/config/const.js +138 -0
  4. package/lib/config/rule-collector.js +81 -0
  5. package/lib/constants.js +62 -0
  6. package/lib/core/memory-buffer/index.js +207 -0
  7. package/lib/core/memory-buffer/shared-instance.js +15 -0
  8. package/lib/core/plugin-mode-manager.js +74 -0
  9. package/lib/core/resRulesServer.js +14 -0
  10. package/lib/core/rulesServer.js +31 -0
  11. package/lib/core/server-entry/capture-handler.js +191 -0
  12. package/lib/core/server-entry/request-interceptor.js +82 -0
  13. package/lib/core/server-entry/response-handler.js +147 -0
  14. package/lib/core/server-entry/server.js +230 -0
  15. package/lib/storage/group-manager.js +627 -0
  16. package/lib/storage/storage-adapter.js +712 -0
  17. package/lib/storage/storage-v3.js +1418 -0
  18. package/lib/uiServer/index.js +61 -24
  19. package/lib/uiServer/router/export/import-export-router.js +459 -0
  20. package/lib/uiServer/router/files/api-list-router.js +150 -0
  21. package/lib/uiServer/router/files/batch-operations-router.js +185 -0
  22. package/lib/uiServer/router/files/file-config-router.js +118 -0
  23. package/lib/uiServer/router/files/file-crud-router.js +212 -0
  24. package/lib/uiServer/router/files/file-save-router.js +146 -0
  25. package/lib/uiServer/router/files/version-router.js +260 -0
  26. package/lib/uiServer/router/global/plugin-control.js +135 -0
  27. package/lib/uiServer/router/global/system-info-router.js +386 -0
  28. package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
  29. package/lib/uiServer/router/index.js +38 -1521
  30. package/lib/uiServer/utils/router-helpers.js +100 -0
  31. package/lib/uiServer/utils/util.js +172 -0
  32. package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
  33. package/lib/utils/archive-utils.js +788 -0
  34. package/lib/utils/error-handler.js +173 -0
  35. package/lib/utils/logger.js +79 -0
  36. package/lib/utils/path-utils.js +147 -0
  37. package/lib/utils/performance.js +265 -0
  38. package/lib/utils/utils.js +541 -0
  39. package/package.json +2 -2
  40. package/public/js/app.js +3707 -1922
  41. package/public/js/app.js.map +1 -1
  42. package/public/js/chunk-vendors.js +5098 -3965
  43. package/public/js/chunk-vendors.js.map +1 -1
  44. package/rules.txt +1 -1
  45. package/CHANGELOG_GROUP_FEATURE.md +0 -468
  46. package/CHANGELOG_P0_FIXES.md +0 -412
  47. package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
  48. package/CLAUDE.md +0 -436
  49. package/GROUP_FEATURE_DESIGN.md +0 -520
  50. package/lib/const.js +0 -47
  51. package/lib/group-manager.js +0 -491
  52. package/lib/resRulesServer.js +0 -9
  53. package/lib/server.js +0 -249
  54. package/lib/uiServer/router/version-router.js +0 -205
  55. package/lib/uiServer/util.js +0 -153
  56. package/lib/utils.js +0 -409
@@ -0,0 +1,541 @@
1
+ /**
2
+ * 文件名: utils.js
3
+ * 功能: Whistle.mockbubu 工具函数集
4
+ * 依赖: logger.js, const.js, path-utils.js
5
+ * 更新: 2025-12-02
6
+ *
7
+ * 职责:
8
+ * - 文件操作函数(读取、更新、列表查询)
9
+ * - URL 和请求处理(文件名生成、规则构造)
10
+ * - 响应数据处理(解压缩、类型转换)
11
+ * - API 列表更新标志管理
12
+ *
13
+ * 架构特点:
14
+ * - V3 架构简化版本,移除废弃的版本管理函数
15
+ * - 使用 StorageAdapter 适配层,屏蔽底层存储细节
16
+ * - 支持多种压缩格式(gzip、deflate、brotli)
17
+ *
18
+ * 使用示例:
19
+ * ```javascript
20
+ * const { getFilename, readSession } = require('./utils')
21
+ *
22
+ * // 获取文件名
23
+ * const filename = getFilename(originalReq)
24
+ *
25
+ * // 读取 session 数据
26
+ * const session = await readSession(storage, filename, groupId)
27
+ * ```
28
+ */
29
+
30
+ const zlib = require('zlib')
31
+ const { createLogger } = require('./logger')
32
+ const { RuleValueMap, SystemConstants } = require('../config/const')
33
+ const { buildPath } = require('./path-utils')
34
+
35
+ // 创建 logger 实例
36
+ const logger = createLogger('utils')
37
+
38
+ // ========================================
39
+ // 文件操作函数
40
+ // ========================================
41
+
42
+ /**
43
+ * 按组获取文件列表
44
+ * 从所有文件中过滤出属于指定组的文件
45
+ *
46
+ * 工作流程:
47
+ * 1. 调用 storage.getFileList() 获取所有文件
48
+ * 2. 过滤出以 "{groupId}/" 开头的文件
49
+ * 3. 去除路径前缀,返回纯文件名列表
50
+ *
51
+ * 文件名格式:
52
+ * - 输入(storage):groupId/filename(例如:default/https://api.test.com/users)
53
+ * - 输出:filename(例如:https://api.test.com/users)
54
+ *
55
+ * @param {StorageAdapter} storage - 存储适配器实例
56
+ * @param {string} groupId - 组ID
57
+ * @returns {Promise<Array>} 文件列表,每个文件包含 name 和 size
58
+ *
59
+ * @example
60
+ * const files = await getGroupFileList(storage, 'default')
61
+ * files.forEach(file => {
62
+ * console.log(file.name, file.size)
63
+ * })
64
+ * // 输出示例:
65
+ * // https://api.test.com/users 1024
66
+ * // https://api.test.com/posts 2048
67
+ */
68
+ const getGroupFileList = async (storage, groupId) => {
69
+ // 参数校验
70
+ if (!groupId) {
71
+ logger.warn('getGroupFileList: groupId 为空,返回空数组')
72
+ return []
73
+ }
74
+
75
+ try {
76
+ // 1. 获取所有文件列表
77
+ // StorageAdapter 返回的格式: groupId/filename(不需要解码)
78
+ const allFiles = await storage.getFileList() || []
79
+ const groupPrefix = `${groupId}/`
80
+ const result = []
81
+
82
+ // 2. 过滤出属于该组的文件
83
+ allFiles.forEach((item) => {
84
+ try {
85
+ // StorageAdapter 返回的文件名格式: groupId/filename
86
+ // 例如: default/https://m.shein.com/us/bff-api/...
87
+ // 不需要任何解码,直接使用
88
+ const name = item.name
89
+
90
+ // 检查是否以 "groupId/" 开头
91
+ if (name.startsWith(groupPrefix)) {
92
+ // 3. 提取纯文件名(去除 groupId/ 前缀)
93
+ const filename = name.substring(groupPrefix.length)
94
+
95
+ result.push({
96
+ name: filename, // 返回纯文件名,不包含 groupId 前缀
97
+ size: item.size || 0, // 文件大小(字节)
98
+ })
99
+ }
100
+ } catch (err) {
101
+ // 处理失败时跳过该文件,不中断整体流程
102
+ logger.warn(`处理文件失败: ${item.name}`, err.message)
103
+ }
104
+ })
105
+
106
+ return result
107
+ } catch (error) {
108
+ logger.error('getGroupFileList 执行失败:', error.message)
109
+
110
+ // 仅在开发环境输出堆栈
111
+ if (process.env.NODE_ENV === 'development') {
112
+ logger.error('错误堆栈:', error.stack)
113
+ }
114
+
115
+ return [] // 失败时返回空数组,避免中断业务
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 更新文件的响应体
121
+ * 读取现有 session,更新 res.body 字段,然后保存
122
+ *
123
+ * 注意:
124
+ * - 仅更新响应体,不修改其他字段
125
+ * - 如果文件不存在,不会创建新文件
126
+ *
127
+ * @param {StorageAdapter} storage - 存储适配器实例
128
+ * @param {string} filename - 文件名(URL)
129
+ * @param {Object} body - 新的响应体内容(会被 JSON.stringify)
130
+ * @param {string} groupId - 组ID
131
+ * @returns {Promise<void>}
132
+ *
133
+ * @example
134
+ * await updateFile(storage, 'https://api.test.com/users', { data: [...] }, 'default')
135
+ */
136
+ const updateFile = async (storage, filename, body, groupId) => {
137
+ if (!filename) {
138
+ throw new Error('文件名不能为空')
139
+ }
140
+
141
+ // 构造文件路径:groupId/filename
142
+ const filePath = groupId ? buildPath(groupId, filename) : filename
143
+
144
+ // 读取现有 session 数据
145
+ const sourceDataStr = await storage.readFile(filePath)
146
+ if (sourceDataStr) {
147
+ const session = JSON.parse(sourceDataStr)
148
+ // 更新响应体(JSON 序列化)
149
+ session.res.body = JSON.stringify(body)
150
+ // 保存更新后的 session 数据
151
+ await storage.writeFile(filePath, JSON.stringify(session))
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 读取文件的响应体
157
+ * 仅返回 res.body 字段,不返回完整 session
158
+ *
159
+ * @param {StorageAdapter} storage - 存储适配器实例
160
+ * @param {string} filename - 文件名(URL)
161
+ * @param {string} groupId - 组ID
162
+ * @returns {Promise<string|undefined>} 响应体内容,文件不存在返回 undefined
163
+ *
164
+ * @example
165
+ * const body = await readFile(storage, 'https://api.test.com/users', 'default')
166
+ * console.log(body) // '{"data": [...]}'
167
+ */
168
+ const readFile = async (storage, filename, groupId) => {
169
+ // 构造文件路径:groupId/filename
170
+ const filePath = groupId ? buildPath(groupId, filename) : filename
171
+
172
+ // 读取源数据 (storageAdapter.readFile 返回的是 session 对象的 JSON 字符串)
173
+ const sourceDataStr = await storage.readFile(filePath)
174
+ if (sourceDataStr) {
175
+ try {
176
+ // 解析后得到 session 对象 {req, res, ...}
177
+ const session = JSON.parse(sourceDataStr)
178
+ return session?.res?.body
179
+ } catch (err) {
180
+ console.error('[mockbubu] readFile parse error:', err.message)
181
+ return undefined
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * 读取完整的 session 数据(包含 req 和 res)
188
+ * 返回完整的 session 对象,包括请求和响应信息
189
+ *
190
+ * Session 结构:
191
+ * ```javascript
192
+ * {
193
+ * req: {
194
+ * method: 'GET',
195
+ * url: 'https://api.test.com/users',
196
+ * headers: {...},
197
+ * body: '...'
198
+ * },
199
+ * res: {
200
+ * statusCode: 200,
201
+ * headers: {...},
202
+ * body: '{"data": [...]}'
203
+ * }
204
+ * }
205
+ * ```
206
+ *
207
+ * @param {StorageAdapter} storage - 存储适配器实例
208
+ * @param {string} filename - 文件名(URL)
209
+ * @param {string} groupId - 组ID
210
+ * @returns {Promise<Object|null>} session 对象,文件不存在返回 null
211
+ *
212
+ * @example
213
+ * const session = await readSession(storage, 'https://api.test.com/users', 'default')
214
+ * if (session) {
215
+ * console.log('请求方法:', session.req.method)
216
+ * console.log('响应状态:', session.res.statusCode)
217
+ * console.log('响应内容:', session.res.body)
218
+ * }
219
+ */
220
+ const readSession = async (storage, filename, groupId) => {
221
+ // 构造文件路径:groupId/filename
222
+ const filePath = groupId ? buildPath(groupId, filename) : filename
223
+
224
+ // 读取源数据 (storageAdapter.readFile 返回的就是 session 对象的 JSON 字符串)
225
+ const sourceDataStr = await storage.readFile(filePath)
226
+ if (sourceDataStr) {
227
+ return JSON.parse(sourceDataStr)
228
+ }
229
+ return null
230
+ }
231
+
232
+ // ========================================
233
+ // URL 和请求处理函数
234
+ // ========================================
235
+
236
+ /**
237
+ * 获取 Mock 文件名
238
+ * 根据 Whistle 规则类型(ruleValue)从 URL 生成文件名
239
+ *
240
+ * 规则类型:
241
+ * - **href**: 完整 URL(包含 query 参数)
242
+ * - **pathname**: origin + pathname(默认,不含 query)
243
+ * - **pattern**: 使用 Whistle pattern 作为文件名
244
+ *
245
+ * 示例:
246
+ * - href: `https://api.test.com/users?id=1`
247
+ * - pathname: `https://api.test.com/users`
248
+ * - pattern: `*.test.com`
249
+ *
250
+ * @param {Object} originalReq - Whistle 原始请求对象
251
+ * @param {string} originalReq.url - 完整 URL
252
+ * @param {string} originalReq.ruleValue - 规则类型(href/pathname/pattern)
253
+ * @param {string} originalReq.pattern - Whistle pattern(仅 pattern 模式使用)
254
+ * @returns {string} 文件名
255
+ *
256
+ * @example
257
+ * const req = {
258
+ * url: 'https://api.test.com/users?id=1',
259
+ * ruleValue: 'pathname',
260
+ * pattern: '*.test.com'
261
+ * }
262
+ * const filename = getFilename(req)
263
+ * console.log(filename) // 'https://api.test.com/users'
264
+ */
265
+ const getFilename = (originalReq) => {
266
+ const { url, ruleValue, pattern } = originalReq
267
+ const u = new URL(url)
268
+ let filename = url
269
+
270
+ // 根据规则类型生成文件名
271
+ switch (ruleValue) {
272
+ case RuleValueMap.href:
273
+ // 完整 URL(包含 query 参数)
274
+ filename = u.href
275
+ break
276
+ case RuleValueMap.pathname:
277
+ // origin + pathname(不含 query)
278
+ filename = u.origin + u.pathname
279
+ break
280
+ case RuleValueMap.pattern:
281
+ // 使用 Whistle pattern 作为文件名
282
+ filename = pattern
283
+ break
284
+ default:
285
+ // 默认使用 pathname 模式
286
+ filename = u.origin + u.pathname
287
+ }
288
+
289
+ return filename
290
+ }
291
+
292
+ /**
293
+ * 获取 API 列表更新标志
294
+ * 检查是否有新的 API 被捕获(需要刷新列表)
295
+ *
296
+ * @param {StorageAdapter} storage - 存储适配器实例
297
+ * @returns {Promise<boolean>} true 表示有更新,false 表示无更新
298
+ *
299
+ * @example
300
+ * const updated = await getApiListUpdated(storage)
301
+ * if (updated) {
302
+ * console.log('API 列表有更新,需要刷新')
303
+ * }
304
+ */
305
+ const getApiListUpdated = async (storage) => {
306
+ const value = await storage.getProperty(SystemConstants.API_LIST_UPDATED_KEY)
307
+ return !!value
308
+ }
309
+
310
+ /**
311
+ * 设置 API 列表更新标志
312
+ * 当有新的 API 被捕获时,设置此标志为 true
313
+ *
314
+ * @param {StorageAdapter} storage - 存储适配器实例
315
+ * @param {boolean} updated - 是否有更新
316
+ * @returns {Promise<void>}
317
+ *
318
+ * @example
319
+ * await setApiListUpdated(storage, true)
320
+ */
321
+ const setApiListUpdated = async (storage, updated) => {
322
+ return await storage.setProperty(SystemConstants.API_LIST_UPDATED_KEY, updated)
323
+ }
324
+
325
+ /**
326
+ * 获取 Whistle 规则字符串
327
+ * 根据 pattern 和 ruleValue 构造完整的规则
328
+ *
329
+ * @param {Object} originalReq - Whistle 原始请求对象
330
+ * @param {string} originalReq.pattern - Whistle pattern
331
+ * @param {string} originalReq.ruleValue - 规则类型
332
+ * @returns {string} 规则字符串
333
+ *
334
+ * @example
335
+ * const req = { pattern: '*.test.com', ruleValue: 'pathname' }
336
+ * const rule = getRule(req)
337
+ * console.log(rule) // '*.test.com mockbubu://pathname'
338
+ */
339
+ const getRule = (originalReq) => {
340
+ const { pattern, ruleValue } = originalReq
341
+ return `${pattern} mockbubu://${ruleValue}`
342
+ }
343
+
344
+ /**
345
+ * 判断是否为 JSON 请求
346
+ * 通过请求头判断请求是否期望 JSON 响应
347
+ *
348
+ * 判断依据:
349
+ * 1. `Accept` 头包含 `application/json`
350
+ * 2. `sec-fetch-dest` 或 `Sec-Fetch-Dest` 为 `empty`
351
+ *
352
+ * 注意:
353
+ * - `sec-fetch-dest: empty` 通常表示 fetch/XHR 请求
354
+ * - 用于决定是否捕获并 mock 该请求
355
+ *
356
+ * @param {Object} headers - 请求头对象
357
+ * @param {string} headers.accept - Accept 头
358
+ * @param {string} headers['sec-fetch-dest'] - Fetch Destination(小写)
359
+ * @param {string} headers['Sec-Fetch-Dest'] - Fetch Destination(大写)
360
+ * @returns {boolean} true 表示是 JSON 请求
361
+ *
362
+ * @example
363
+ * const headers = {
364
+ * accept: 'application/json',
365
+ * 'sec-fetch-dest': 'empty'
366
+ * }
367
+ * const isJson = isJsonReq(headers)
368
+ * console.log(isJson) // true
369
+ */
370
+ const isJsonReq = (headers) => {
371
+ const result = (
372
+ (headers.accept && headers.accept.includes('application/json')) ||
373
+ headers['sec-fetch-dest'] === 'empty' ||
374
+ headers['Sec-Fetch-Dest'] === 'empty'
375
+ )
376
+
377
+ // 调试日志(仅用于特定域名)
378
+ // 可通过环境变量或配置开关控制
379
+ if (process.env.DEBUG_HOST && headers.host === process.env.DEBUG_HOST) {
380
+ const fullUrl = headers['x-whistle-full-url'] || 'unknown'
381
+ logger.log(`[isJsonReq] ${headers.host} 请求: ${decodeURIComponent(fullUrl)}`)
382
+ logger.log(' - Accept:', headers.accept)
383
+ logger.log(' - sec-fetch-dest:', headers['sec-fetch-dest'])
384
+ logger.log(' - Sec-Fetch-Dest:', headers['Sec-Fetch-Dest'])
385
+ logger.log(' - 判断结果:', result)
386
+ }
387
+
388
+ return result
389
+ }
390
+
391
+ // ========================================
392
+ // 工具函数
393
+ // ========================================
394
+
395
+ /**
396
+ * 为异步函数添加 try-catch 包装
397
+ * 捕获异常并在开发环境输出日志,然后重新抛出
398
+ *
399
+ * 使用场景:
400
+ * - 包装可能抛出异常的函数
401
+ * - 在开发环境提供更好的错误日志
402
+ * - 不改变原函数的返回值和异常行为
403
+ *
404
+ * 注意:
405
+ * - 异常仍会被抛出,只是在抛出前记录日志
406
+ * - 仅在开发环境输出日志,生产环境静默
407
+ *
408
+ * @param {Function} fn - 要包装的异步函数
409
+ * @returns {Function} 包装后的函数
410
+ *
411
+ * @example
412
+ * const safeReadFile = withTryCatch(async (filename) => {
413
+ * return await fs.readFile(filename, 'utf-8')
414
+ * })
415
+ *
416
+ * try {
417
+ * const content = await safeReadFile('test.json')
418
+ * } catch (err) {
419
+ * // 开发环境会先输出日志,再抛出异常
420
+ * console.error('读取失败:', err)
421
+ * }
422
+ */
423
+ const withTryCatch = (fn) => {
424
+ return async (...args) => {
425
+ try {
426
+ return await fn(...args)
427
+ } catch (e) {
428
+ // 仅在开发环境输出错误日志
429
+ if (process.env.NODE_ENV === 'development') {
430
+ logger.error('全局捕获异常:', e)
431
+ }
432
+ // 重新抛出异常,不改变原函数行为
433
+ throw e
434
+ }
435
+ }
436
+ }
437
+
438
+ // ========================================
439
+ // 导出 - V3 架构使用的函数
440
+ // ========================================
441
+
442
+ // 文件操作
443
+ exports.getGroupFileList = getGroupFileList
444
+ exports.updateFile = updateFile
445
+ exports.readFile = readFile
446
+ exports.readSession = readSession
447
+
448
+ // URL 和请求处理
449
+ exports.getFilename = getFilename
450
+ exports.getRule = getRule
451
+ exports.isJsonReq = isJsonReq
452
+
453
+ // API 列表更新标志
454
+ exports.getApiListUpdated = getApiListUpdated
455
+ exports.setApiListUpdated = setApiListUpdated
456
+
457
+ // 工具函数
458
+ exports.withTryCatch = withTryCatch
459
+
460
+ /**
461
+ * 将响应 Buffer 解压并转换为字符串
462
+ * 支持多种压缩格式和数据类型
463
+ *
464
+ * 支持的压缩格式:
465
+ * - **gzip**: 使用 zlib.gunzipSync() 解压
466
+ * - **deflate**: 使用 zlib.inflateSync() 解压
467
+ * - **br** (brotli): 使用 zlib.brotliDecompressSync() 解压
468
+ *
469
+ * 支持的数据类型:
470
+ * - **String**: 直接返回
471
+ * - **Object**: JSON.stringify() 序列化
472
+ * - **Uint8Array**: toString() 转换
473
+ * - **其他**: 原样返回
474
+ *
475
+ * 工作流程:
476
+ * 1. 根据 encoding 解压缩 body
477
+ * 2. 根据数据类型转换为字符串
478
+ * 3. 返回字符串内容
479
+ *
480
+ * @param {Object} params - 参数对象
481
+ * @param {Buffer|string|Object|Uint8Array} params.body - 响应体数据
482
+ * @param {string} params.encoding - 压缩编码类型(gzip/deflate/br)
483
+ * @returns {string} 转换后的字符串内容
484
+ *
485
+ * @example
486
+ * // 解压 gzip 数据
487
+ * const result = handleBuffer2String({
488
+ * body: gzipBuffer,
489
+ * encoding: 'gzip'
490
+ * })
491
+ * console.log(result) // '{"data": [...]}'
492
+ *
493
+ * @example
494
+ * // 处理 JSON 对象
495
+ * const result = handleBuffer2String({
496
+ * body: { data: [1, 2, 3] },
497
+ * encoding: undefined
498
+ * })
499
+ * console.log(result) // '{"data":[1,2,3]}'
500
+ */
501
+ exports.handleBuffer2String = withTryCatch(({ body, encoding }) => {
502
+ // 1. 根据编码类型解压缩
503
+ switch (encoding) {
504
+ case 'gzip':
505
+ body = zlib.gunzipSync(body)
506
+ break
507
+ case 'deflate':
508
+ body = zlib.inflateSync(body)
509
+ break
510
+ case 'br':
511
+ body = zlib.brotliDecompressSync(body)
512
+ break
513
+ default:
514
+ // 无压缩或未知编码,保持原样
515
+ break
516
+ }
517
+
518
+ // 2. 根据数据类型转换为字符串
519
+ const type = Object.prototype.toString.call(body)
520
+ let content
521
+
522
+ switch (type) {
523
+ case '[object String]':
524
+ // 已经是字符串,直接返回
525
+ content = body
526
+ break
527
+ case '[object Object]':
528
+ // 对象类型,JSON 序列化
529
+ content = JSON.stringify(body)
530
+ break
531
+ case '[object Uint8Array]':
532
+ // Uint8Array 类型,转换为字符串
533
+ content = body.toString()
534
+ break
535
+ default:
536
+ // 其他类型,原样返回
537
+ content = body
538
+ }
539
+
540
+ return content
541
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mockbubu",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "mock response data",
5
5
  "scripts": {
6
6
  "lint": "eslint . --ext .js",
@@ -25,7 +25,7 @@
25
25
  ".gitignore",
26
26
  "index.js",
27
27
  "package.json",
28
- "*.md",
28
+ "README.md",
29
29
  "step1.png",
30
30
  "step2.png"
31
31
  ],