diff --git a/.eslintignore b/.eslintignore index 38702545..1902a282 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,5 +5,8 @@ /public/ /config/ /repo/ -!.vuepress + +!/docs/.vuepress/ +/docs/.vuepress/dist/* + barcode/ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0de9d04a..4bc45458 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,132 @@ # 更新日志 +### [1.3.143](https://github.com/novlan1/t-comm/compare/v1.3.142...v1.3.143) (2025-01-07) + + +### Features 🎉 + +* **lint:** stylelint 不处理vue文件 ([84c536f](https://github.com/novlan1/t-comm/commit/84c536f4b11072e6b6ad1dcdcc07964e36d362fc)) + +### [1.3.142](https://github.com/novlan1/t-comm/compare/v1.3.141...v1.3.142) (2025-01-07) + + +### Features 🎉 + +* **lint:** 优化全量模式 ([c22dfc0](https://github.com/novlan1/t-comm/commit/c22dfc02d481446817daf67aad4a951eb90ffa09)) + +### [1.3.141](https://github.com/novlan1/t-comm/compare/v1.3.140...v1.3.141) (2025-01-07) + + +### Features 🎉 + +* **lint:** 支持全量模式 ([d123dfe](https://github.com/novlan1/t-comm/commit/d123dfe4188ab287f3e7a787208c2073f029af4f)) + +### [1.3.140](https://github.com/novlan1/t-comm/compare/v1.3.139...v1.3.140) (2025-01-06) + + +### Features 🎉 + +* **lint:** add checkLint ([6fa2e55](https://github.com/novlan1/t-comm/commit/6fa2e5540354e94afe8e5e6e784a6332b21996d0)) + +### [1.3.139](https://github.com/novlan1/t-comm/compare/v1.3.138...v1.3.139) (2024-12-25) + + +### Features 🎉 + +* **csv:** add generateCSVData ([367cc49](https://github.com/novlan1/t-comm/commit/367cc4954bebc5ccdb7ecf841f183b32dae5c091)) + +### [1.3.138](https://github.com/novlan1/t-comm/compare/v1.3.137...v1.3.138) (2024-12-24) + + +### Features 🎉 + +* **csv:** add generateCSV ([d3d68e1](https://github.com/novlan1/t-comm/commit/d3d68e1612a224f7ef6614f5ae959643be4a731d)) + +### [1.3.137](https://github.com/novlan1/t-comm/compare/v1.3.136...v1.3.137) (2024-12-24) + + +### Documentation 📖 + +* update docs ([8956a30](https://github.com/novlan1/t-comm/commit/8956a309c5f2c42d065d0ae134fb9bc41da078ee)) + + +### Features 🎉 + +* **slice-object:** 边界处理 ([12d0585](https://github.com/novlan1/t-comm/commit/12d05853cd559b1dc7ee229e20261cffac4a380f)) + +### [1.3.136](https://github.com/novlan1/t-comm/compare/v1.3.135...v1.3.136) (2024-12-18) + + +### Features 🎉 + +* **debounce:** add debug ([f58f48c](https://github.com/novlan1/t-comm/commit/f58f48ccfe68d07260b7f07721f6658a8e7fc31e)) + +### [1.3.135](https://github.com/novlan1/t-comm/compare/v1.3.134...v1.3.135) (2024-12-18) + +### [1.3.134](https://github.com/novlan1/t-comm/compare/v1.3.133...v1.3.134) (2024-12-18) + + +### Features 🎉 + +* **debounce:** update type ([b3c4e63](https://github.com/novlan1/t-comm/commit/b3c4e631a727a44d6822b983177635f0bf8e668d)) + +### [1.3.133](https://github.com/novlan1/t-comm/compare/v1.3.132...v1.3.133) (2024-12-18) + + +### Features 🎉 + +* **debounce:** add debounceRun ([cfe2740](https://github.com/novlan1/t-comm/commit/cfe27400fc16d6e6e876d308d66fc082a7ac279f)) + +### [1.3.132](https://github.com/novlan1/t-comm/compare/v1.3.131...v1.3.132) (2024-12-18) + + +### Features 🎉 + +* **uni-hook:** add debug in invoke ([1118bfa](https://github.com/novlan1/t-comm/commit/1118bfa62be713a7a519e877fb148b5f5b01c625)) + +### [1.3.131](https://github.com/novlan1/t-comm/compare/v1.3.130...v1.3.131) (2024-12-18) + + +### Features 🎉 + +* **uni-hook:** use addInterceptor ([889df7d](https://github.com/novlan1/t-comm/commit/889df7d8dab31c1e99694e40b2486d764e54224b)) + +### [1.3.130](https://github.com/novlan1/t-comm/compare/v1.3.129...v1.3.130) (2024-12-18) + + +### Features 🎉 + +* update dotenv ([e570221](https://github.com/novlan1/t-comm/commit/e5702211621d2f142c72ce585d260be69b7d974a)) + +### [1.3.129](https://github.com/novlan1/t-comm/compare/v1.3.128...v1.3.129) (2024-12-18) + + +### Features 🎉 + +* **startUniProject:** 支持可选参数 ([383b8ec](https://github.com/novlan1/t-comm/commit/383b8ec12d44bbc8b6172e6a557aa85204883e06)) + +### [1.3.128](https://github.com/novlan1/t-comm/compare/v1.3.127...v1.3.128) (2024-12-18) + + +### Features 🎉 + +* **dotenv:** add loadDotenv ([e2c9fc6](https://github.com/novlan1/t-comm/commit/e2c9fc6ba317197a87a9afa48ae4c9d2f5058596)) + +### [1.3.127](https://github.com/novlan1/t-comm/compare/v1.3.126...v1.3.127) (2024-12-16) + + +### Documentation 📖 + +* update docs ([f25e150](https://github.com/novlan1/t-comm/commit/f25e1502738fb25909789ef5a8b54e617967d735)) +* update docs ([12b9f1d](https://github.com/novlan1/t-comm/commit/12b9f1d00300074e01975e282aa8d3e81ef101cd)) +* update docs ([ad95eff](https://github.com/novlan1/t-comm/commit/ad95eff2beb8da645999e1a4e02f37a06ad1590c)) +* update docs ([2ab440a](https://github.com/novlan1/t-comm/commit/2ab440a4cbd53c70861c672f2d8182582231dce3)) + + +### Features 🎉 + +* **node-command:** support more options ([ef67c06](https://github.com/novlan1/t-comm/commit/ef67c06dfe5bad65e3a8f946d15a351216074708)) + ### [1.3.126](https://github.com/novlan1/t-comm/compare/v1.3.125...v1.3.126) (2024-12-13) diff --git a/docs/zh/csv.md b/docs/zh/csv.md new file mode 100644 index 00000000..6d9324b7 --- /dev/null +++ b/docs/zh/csv.md @@ -0,0 +1,32 @@ +[[toc]] + +

引入

+ +```ts +import { generateCSV } from 't-comm'; + +// or +import { generateCSV} from 't-comm/lib/csv/index'; +``` + + +## `generateCSV(dataList)` + + +**描述**:

生成 CSV 文件内容,可以用于 fs.writeFileSync 输出

+

第一行为表头

+ +**参数**: + + +| 参数名 | 类型 | 描述 | +| --- | --- | --- | +| dataList | Array<Array<string>> |

二维数据列表

| + +**返回**:

生成的字符串

+ +**示例** + +```ts +generateCSV([['a','b'], ['1', '2']]); +``` diff --git a/docs/zh/daily-merge.md b/docs/zh/daily-merge.md index b8e7f334..6defcac5 100644 --- a/docs/zh/daily-merge.md +++ b/docs/zh/daily-merge.md @@ -14,6 +14,19 @@ import { dailyMerge} from 't-comm/lib/daily-merge/index'; **描述**:

每日合并

+
    +
  1. 获取昨天有活跃的分支
  2. +
  3. 对于每个分支,进行合并并推送 + +
  4. +
  5. 发送机器人消息
  6. +
**参数**: @@ -39,7 +52,6 @@ import { dailyMerge} from 't-comm/lib/daily-merge/index'; dailyMerge({ webhookUrl: 'xx', appName: 'xx', - projectId: 'xx', devRoot: 'xx', baseUrl: 'xx', diff --git a/docs/zh/debounce.md b/docs/zh/debounce.md index 5479b952..59ca01e1 100644 --- a/docs/zh/debounce.md +++ b/docs/zh/debounce.md @@ -3,14 +3,35 @@

引入

```ts -import { debounce } from 't-comm'; +import { debounceRun, debounce } from 't-comm'; // or -import { debounce} from 't-comm/lib/debounce/index'; +import { debounceRun, debounce} from 't-comm/lib/debounce/index'; ``` -## `debounce(fn, time)` +## `debounceRun` + + +**描述**:

不用生成中间函数的防抖

+ +**参数**: + + + +**示例** + +```ts +debounceRun(func, args, { + funcKey: 'funcKey', + wait: 500, // 默认 500 + throttle: false, // 是否是节流,默认 false + immediate: true, // 是否立即执行,默认 true +}) +`` + + +## `debounce(fn, time, immediate)` **描述**:

防抖,场景:搜索

@@ -24,6 +45,7 @@ import { debounce} from 't-comm/lib/debounce/index'; | --- | --- | --- | | fn | function |

主函数

| | time | number |

间隔时间,单位 ms

| +| immediate | boolean |

是否立即执行,默认 false

| **返回**:

闭包函数

@@ -34,4 +56,6 @@ function count() { console.log('xxxxx') } window.onscroll = debounce(count, 500) + +window.onscroll = debounce(count, 500, true) ``` diff --git a/docs/zh/dotenv.md b/docs/zh/dotenv.md new file mode 100644 index 00000000..65b0eeb7 --- /dev/null +++ b/docs/zh/dotenv.md @@ -0,0 +1,38 @@ +[[toc]] + +

引入

+ +```ts +import { loadDotenv } from 't-comm'; + +// or +import { loadDotenv} from 't-comm/lib/dotenv/index'; +``` + + +## `loadDotenv(file, param)` + + +**描述**:

用 dotenv-expand 加载环境变量

+ +**参数**: + + +| 参数名 | 描述 | +| --- | --- | +| file |

文件路径,默认 .env.local

| +| param |

参数

| + + + +**示例** + +```ts +loadEnv(); + +loadEnv('.env'); + +loadEnv('.env.local', { + debug: false, // 是否打印日志,默认 true +}); +``` diff --git a/docs/zh/node.md b/docs/zh/node.md index f5dba546..39fd14a9 100644 --- a/docs/zh/node.md +++ b/docs/zh/node.md @@ -179,7 +179,7 @@ import { | --- | --- | --- | | command | string |

命令

| | root | string |

执行命令的目录

| -| stdio | string |

结果输出,默认为 pipe

| +| stdio | string \| object |

结果输出,默认为 pipe

| **返回**: string
diff --git a/docs/zh/router.md b/docs/zh/router.md index e9afb2e7..8f1b2187 100644 --- a/docs/zh/router.md +++ b/docs/zh/router.md @@ -6,14 +6,16 @@ import { findRouteName, getRouterFuncPath, - getH5CurrentUrl + getH5CurrentUrl, + uniHookRouter } from 't-comm'; // or import { findRouteName, getRouterFuncPath, - getH5CurrentUrl + getH5CurrentUrl, + uniHookRouter } from 't-comm/lib/router/index'; ``` @@ -79,3 +81,30 @@ console.log('name', name); ```ts getH5CurrentUrl(this.$route); ``` + + +## `uniHookRouter()` + + +**描述**:

拦截路由

+ +**参数**: + + + +**示例** + +```ts +uniHookRouter({ + navigateToHooks: [ + () => console.log('1') + ], + navigateBackHooks: [ + () => console.log('2') + ], + redirectToHooks: [ + () => console.log('3') + ], + debug: true, +}) +``` diff --git a/docs/zh/uni-app.md b/docs/zh/uni-app.md index b3e2c1ab..f37647cc 100644 --- a/docs/zh/uni-app.md +++ b/docs/zh/uni-app.md @@ -3,10 +3,10 @@

引入

```ts -import { getRouteLeaveCache } from 't-comm'; +import { getRouteLeaveCache, startUniProject } from 't-comm'; // or -import { getRouteLeaveCache} from 't-comm/lib/uni-app/index'; +import { getRouteLeaveCache, startUniProject} from 't-comm/lib/uni-app/index'; ``` @@ -27,3 +27,28 @@ import { getRouteLeaveCache} from 't-comm/lib/uni-app/index'; **返回**:

返回对象,包含 beforeRouteLeave 和 activated 方法

+ + +## `startUniProject(options)` + + +**描述**:

启动 uni-app 项目

+ +**参数**: + + +| 参数名 | 描述 | +| --- | --- | +| options |

参数

| + + + +**示例** + +```ts +startUniProject(); + +startUniProject({ + debug: false, // 默认为 true,会打印参数 +}) +``` diff --git a/package.json b/package.json index adeb23b8..547700fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "t-comm", - "version": "1.3.126", + "version": "1.3.143", "description": "专业、稳定、纯粹的工具库", "main": "lib/index.js", "module": "lib/index.esm.js", @@ -146,4 +146,4 @@ "postchangelog": "node script/docs-jsdoc/change-log" } } -} \ No newline at end of file +} diff --git a/src/base/object/slice.ts b/src/base/object/slice.ts index 298e27c5..5ab53d6c 100644 --- a/src/base/object/slice.ts +++ b/src/base/object/slice.ts @@ -1,4 +1,7 @@ export function sliceObject(info: Record, max: number) { + if (!info) { + return {}; + } const keys = Object.keys(info); if (keys.length < max) { return info; diff --git a/src/base/regexp/regexp.ts b/src/base/regexp/regexp.ts index bf036c2e..d9d65fa2 100644 --- a/src/base/regexp/regexp.ts +++ b/src/base/regexp/regexp.ts @@ -38,6 +38,8 @@ const PRE_RELEASE_VERSION = /\d+\.\d+\.\d+-(\w+).\d+/; */ export function getPreReleaseTag(version: string) { const match = version.match(PRE_RELEASE_VERSION); - if (!match || !match[1]) return ''; + if (!match?.[1]) { + return ''; + } return match[1]; } diff --git a/src/csv/csv.ts b/src/csv/csv.ts new file mode 100644 index 00000000..84851fe0 --- /dev/null +++ b/src/csv/csv.ts @@ -0,0 +1,65 @@ +/** + * 生成 CSV 文件内容,可以用于 fs.writeFileSync 输出 + * + * 第一行为表头 + * @param {Array>} dataList 二维数据列表 + * @returns 生成的字符串 + * @example + * + * ```ts + * generateCSV([['a','b'], ['1', '2']]); + * ``` + */ +export function generateCSV(dataList: Array>) { + const result: string[] = []; + if (!dataList?.[0].length) { + return ''; + } + + dataList.forEach((line, lineIndex) => { + line.forEach((text, index) => { + if (lineIndex === 0 && index === 0) { + result.push(`\ufeff${text},`); + } else if (index === line.length - 1) { + result.push(`${text}\n`); + } else { + result.push(`${text},`); + } + }); + }); + + return result.join(''); +} + + +/** + * 生成 CSV 所需数据,可用于传递给 generateCSV 方法 + * + * @param {Array>} list 数据列表 + * @param {Record} headMap 数据项的 key 和表头标题的映射关系 + * @returns 二维数组,第一行是表头 + * + * @example + * ```ts + * generateCSVData([ + * { + * file: 'a.js', + * size: 88, + * }, + * { + * file: 'b.js', + * size: 66, + * } + * ], { file: '文件名称', size: '文件大小' }) + * + * + * // [['文件名称', '文件大小'], ['a.js', 88], ['b.js', 66]] + * ``` + */ +export function generateCSVData(list: Array>, headMap: Record) { + const dataList = list.map((item: Record) => Object.keys(headMap).map(key => item[key])); + return [ + Object.values(headMap), + ...dataList, + ]; +} diff --git a/src/csv/index.ts b/src/csv/index.ts new file mode 100644 index 00000000..f205b146 --- /dev/null +++ b/src/csv/index.ts @@ -0,0 +1 @@ +export { generateCSV, generateCSVData } from './csv'; diff --git a/src/daily-merge/daily-merge.ts b/src/daily-merge/daily-merge.ts index 05df7bea..712007c8 100644 --- a/src/daily-merge/daily-merge.ts +++ b/src/daily-merge/daily-merge.ts @@ -166,6 +166,15 @@ async function sendSendMsg(content: string, webhookUrl: string) { /** * 每日合并 + * 1. 获取昨天有活跃的分支 + * 2. 对于每个分支,进行合并并推送 + * - 清理 Git 环境 + * - 切到主分支,并拉最新代码 + * - 切到当前分支,拉最新代码 + * - 尝试执行 git merge + * - 对比 merge 前后的 commit 信息是否相同,作为判断 merge 是否成功的依据 + * 3. 发送机器人消息 + * * * @export * @async @@ -186,7 +195,6 @@ async function sendSendMsg(content: string, webhookUrl: string) { * dailyMerge({ * webhookUrl: 'xx', * appName: 'xx', - * projectId: 'xx', * devRoot: 'xx', * * baseUrl: 'xx', @@ -243,7 +251,7 @@ export async function dailyMerge({ whiteBranchReg, }); - if (!branches || !branches.length) { + if (!branches?.length) { console.log('[no branch to merge]'); sendSendMsg(`>【${appName || ''}自动合并】未找到需要合并的分支`, webhookUrl); return; diff --git a/src/debounce/debounce.ts b/src/debounce/debounce.ts index ae3083cf..3294d75a 100644 --- a/src/debounce/debounce.ts +++ b/src/debounce/debounce.ts @@ -1 +1,121 @@ -export { debounce } from './index'; +/** + * 防抖,场景:搜索 + * + * 触发事件后在 n 秒内函数只能执行一次,如果 + * 在 n 秒内又触发了事件,则会重新计算函数执行时间 + * + * @param {Function} fn 主函数 + * @param {number} time 间隔时间,单位 `ms` + * @param {boolean} immediate 是否立即执行,默认 `false` + * @returns 闭包函数 + * + * @example + * + * ```ts + * function count() { + * console.log('xxxxx') + * } + * window.onscroll = debounce(count, 500) + * + * window.onscroll = debounce(count, 500, true) + * ``` + */ +export function debounce(fn: Function, time: number, immediate = false) { + let timer: ReturnType; + let result: any; + + return function (...args: Array) { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + // const args = [...arguments]; + + if (immediate) { + result = fn.apply(that, args); + } + + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(() => { + result = fn.apply(that, args); + }, time); + + return result; + }; +} + + +/** + * 不用生成中间函数的防抖 + * + * @example + * ```ts + * debounceRun(func, args, { + * funcKey: 'funcKey', + * wait: 500, // 默认 500 + * throttle: false, // 是否是节流,默认 false + * immediate: true, // 是否立即执行,默认 true + * }) + * `` + */ +export const debounceRun = (() => { + // 储存方法的 timer 的 map + const timerMap = new Map(); + + return (func: Function, args: any[] = [], options: { + funcKey?: any; + wait?: number; + throttle?: boolean; + immediate?: boolean; + debug?: boolean; + } = {}) => { + const DEFAULT_OPTIONS = { + funcKey: null, + wait: 500, + throttle: false, + immediate: true, + debug: false, + }; + + // 如果没有 funcKey 那么直接使用 func 作为 map 的 key + const funcKey = options.funcKey || func; + const wait = options.wait ?? DEFAULT_OPTIONS.wait; + const throttle = options.throttle ?? DEFAULT_OPTIONS.throttle; + const immediate = options.immediate ?? DEFAULT_OPTIONS.immediate; + const debug = options.debug ?? DEFAULT_OPTIONS.debug; + + // 先看看 map 里面是否有 timer,有 timer 代表之前调用过 + let timer = timerMap.get(funcKey); + + if (immediate) { + func.apply(this, args); + } + + if (timer) { + if (debug) { + console.log('>>> debounceRun cached'); + } + if (throttle) { + return; + } + + clearTimeout(timer); + } + + timer = setTimeout(() => { + // 先把这个方法从 map 里面删掉 + timerMap.delete(funcKey); + + func.apply(this, args); + + if (debug) { + console.log('>>> debounceRun executing func'); + } + }, wait); + + // 将方法的 timer 存进 map, key 是 funcKey + timerMap.set(funcKey, timer); + }; +})(); diff --git a/src/debounce/index.ts b/src/debounce/index.ts index eaff390a..b8e63e18 100644 --- a/src/debounce/index.ts +++ b/src/debounce/index.ts @@ -1,35 +1 @@ -/** - * 防抖,场景:搜索 - * - * 触发事件后在 n 秒内函数只能执行一次,如果 - * 在 n 秒内又触发了事件,则会重新计算函数执行时间 - * - * @param {Function} fn 主函数 - * @param {number} time 间隔时间,单位 `ms` - * @returns 闭包函数 - * - * @example - * - * ```ts - * function count() { - * console.log('xxxxx') - * } - * window.onscroll = debounce(count, 500) - * ``` - */ -export function debounce(fn: Function, time: number) { - let timer: ReturnType; - - return function (...args: Array) { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-this-alias - const that = this; - // const args = [...arguments]; - if (timer) clearTimeout(timer); - - timer = setTimeout(() => { - fn.apply(that, args); - }, time); - }; -} - +export { debounce, debounceRun } from './debounce'; diff --git a/src/dotenv/dotenv.ts b/src/dotenv/dotenv.ts new file mode 100644 index 00000000..52fcb288 --- /dev/null +++ b/src/dotenv/dotenv.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +import fs from 'fs'; + + +/** + * 用 dotenv-expand 加载环境变量 + * @param file 文件路径,默认 .env.local + * @param param 参数 + * + * @example + * ```ts + * loadEnv(); + * + * loadEnv('.env'); + * + * loadEnv('.env.local', { + * debug: false, // 是否打印日志,默认 true + * }); + * ``` + */ +export function loadDotenv(file = '.env.local', options?: { + debug?: boolean +}) { + const dotenv = require('dotenv'); + const dotenvExpand = require('dotenv-expand'); + if (!fs.existsSync(file)) { + console.log(`>>> loadEnv ${file} 不存在`); + return; + } + + const myEnv = dotenv.config({ path: file }); + dotenvExpand.expand(myEnv); + + const debug = options?.debug ?? true; + + if (debug) { + console.log(`>>> loadEnv ${file} success`); + } +} diff --git a/src/dotenv/index.ts b/src/dotenv/index.ts new file mode 100644 index 00000000..f98d4dd7 --- /dev/null +++ b/src/dotenv/index.ts @@ -0,0 +1 @@ +export { loadDotenv } from './dotenv'; diff --git a/src/index.ts b/src/index.ts index 2e439eb6..8bd286f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export * from './constant'; export * from './cookie'; export * from './cron'; export * from './css'; +export * from './csv'; export * from './daily-merge'; export * from './date'; export * from './debounce'; @@ -22,6 +23,7 @@ export * from './devops'; export * from './dialog'; export * from './dom'; export * from './dom-to-image'; +export * from './dotenv'; export * from './e-bus'; export * from './e2e-test'; export * from './env'; @@ -36,6 +38,7 @@ export * from './ip'; export * from './jsdoc'; export * from './launch-app'; export * from './launch-game'; +export * from './lint'; export * from './loader'; export * from './lodash-mini'; export * from './log'; diff --git a/src/lint/index.ts b/src/lint/index.ts new file mode 100644 index 00000000..c431a16b --- /dev/null +++ b/src/lint/index.ts @@ -0,0 +1,2 @@ +export { checkLint } from './lint'; +export * from './types'; diff --git a/src/lint/lint.ts b/src/lint/lint.ts new file mode 100644 index 00000000..3ed9aebb --- /dev/null +++ b/src/lint/lint.ts @@ -0,0 +1,607 @@ +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; + +import { batchSendWxRobotMarkdown } from '../wecom-robot/batch-send'; +import { writeFileSync, readFileSync } from '../fs/fs'; +import { execCommand } from '../node/node-command'; + +import type { JSErrorFile, SCSSErrorFile } from './types'; + + +const ESLINT_CONFIG_FILE = '.eslintrc.js'; + + +function removeParserOptionsProject(workspace: string) { + const configFile = path.resolve(workspace, ESLINT_CONFIG_FILE); + if (!fs.existsSync(configFile)) { + console.log('不存在配置文件'); + return; + } + + const content = readFileSync(configFile); + + const reg = /\s+project:\s+'[^']+json',/; + const newContent = content.replace(reg, ''); + if (newContent && newContent !== content) { + console.log('已删除 parserOptions.project'); + writeFileSync(configFile, newContent); + } +} + + +async function createMRNote({ + privateToken, + gitApiPrefix, + + projectName, + mrId, + + body, + path, + line, + + lineType = 'new', + + // 严重程度 可选值 0、1、2、3 + // 0 : "default"(默认)1 : "slight"(轻微) + // 2 : "normal"(一般)3 : "serious"(严重 + risk = 3, + + // 需解决 可选值 0、1、2 + // 0 : "default"(默认) + // 1 : "unresolved"(未解决) + // 2 : "resolved"(已解决) + resolveState = 1, + + // 默认值为 true,发通知给相关用户 + notifyEnabled = true, + +}: { + privateToken: string; + gitApiPrefix: string; + + projectName: string; + mrId: string | number; + + body: any; + path: string; + line: number; + + lineType?: string; + risk?: 0 | 1 | 2 | 3; + + resolveState?: 0 | 1 | 2; + notifyEnabled?: boolean; + +}) { + return new Promise((resolve, reject) => { + axios({ + url: `${gitApiPrefix}/projects/${encodeURIComponent(projectName)}/merge_requests/${mrId}/notes?private_token=${privateToken}`, + method: 'POST', + data: { + body, + path, + line, + line_type: lineType, + risk, + resolve_state: resolveState, + notify_enabled: notifyEnabled, + }, + }).then((res) => { + resolve(res.data); + }) + .catch((err) => { + console.log('err', err); + reject(err); + }); + }); +} + + +function parseScssFiles(scssErrorFiles: SCSSErrorFile[], workspace: string): any[] { + if (!scssErrorFiles?.length) { + return []; + } + + const parsedFiles = scssErrorFiles.reduce((acc: any[], item) => { + item.warnings?.forEach((messageItem: any) => { + acc.push({ + filePath: item.source, + parsedFilePath: path.relative(workspace, item.source), + ...messageItem, + message: messageItem.text, + ruleId: messageItem.rule, + }); + }); + return acc; + }, []); + + return parsedFiles; +} + + +function parseJSFiles(jsErrorFiles: JSErrorFile[], workspace: string) { + if (!jsErrorFiles?.length) { + return []; + } + + const parsedFiles = jsErrorFiles.reduce((acc: any[], item) => { + item.messages?.forEach((messageItem: any) => { + acc.push({ + filePath: item.filePath, + parsedFilePath: path.relative(workspace, item.filePath), + ...messageItem, + }); + }); + return acc; + }, []); + + return parsedFiles; +} + + +async function tryCreateMRNote({ + privateToken, + gitApiPrefix, + + errorFiles, + projectName, + + mrId, + workspace, + + isScss, +}: { + privateToken: string; + gitApiPrefix: string; + + errorFiles: Array>; + projectName: string; + + mrId: string | number; + workspace: string; + + isScss?: boolean; +}) { + const parsedFiles: Array = isScss + ? parseScssFiles(errorFiles, workspace) + : parseJSFiles(errorFiles, workspace); + + console.log('[isScss]', isScss); + console.log('[parsedFiles]', parsedFiles); + + if (!parsedFiles?.length) return; + + + for (const file of parsedFiles) { + await createMRNote({ + projectName, + mrId, + + body: (`${file.message}\n\n [${file.ruleId}]`) || '格式错误', + line: file.line, + path: file.parsedFilePath, + privateToken, + gitApiPrefix, + }); + } +} + + +async function createMRComment({ + projectName, + mrId, + data, + privateToken, + gitApiPrefix, +}: { + projectName: string; + mrId: string | number; + data: any; + privateToken: string; + gitApiPrefix: string; +}): Promise { + return new Promise((resolve, reject) => { + axios({ + url: `${gitApiPrefix}/projects/${encodeURIComponent(projectName)}/merge_request/${mrId}/comments?private_token=${privateToken}`, + method: 'POST', + data: { + note: data, + }, + }).then((res) => { + resolve(res.data); + }) + .catch((err) => { + console.log('err', err); + reject(err); + }); + }); +} + +export async function checkLint({ + privateToken, + gitApiPrefix, + workspace, + + mrUrl, + mrId, + buildUrl, + + repo, + repoUrl, + + sourceBranch, + targetBranch, + + docLink, + webhookUrl, + + chatId = ['ALL'], + checkAll, +}: { + privateToken: string; + gitApiPrefix: string; + workspace: string; + + mrUrl?: string; + mrId?: string; + buildUrl: string; + + repo: string; + repoUrl?: string; + sourceBranch?: string; + targetBranch?: string; + + docLink: string + webhookUrl: string; + + chatId?: string[]; + checkAll?: boolean; +}) { + let jsKeyword = '--ext .js,.ts .'; + let vueKeyword = '--ext .vue .'; + let sassKeyword = '**/*.{css,scss}'; + + if (sourceBranch && targetBranch) { + const { + jsTsFiles, + vueFiles, + scssFiles, + } = getDiffFile({ + sourceBranch, + targetBranch, + workspace, + }); + + jsKeyword = jsTsFiles.join(' '); + vueKeyword = vueFiles.join(' '); + sassKeyword = scssFiles.join(' '); + } + + const outputJs = path.resolve(workspace, 'lint-js.json'); + const outputVue = path.resolve(workspace, 'lint-vue.json'); + const outputScss = path.resolve(workspace, 'lint-scss.json'); + + + console.log('正在执行 lint js/ts ...'); + execCommand(`npx eslint ${jsKeyword} --quiet -o ${outputJs} --format json || true`, workspace, 'inherit'); + + removeParserOptionsProject(workspace); + + console.log('正在执行 lint vue ...'); + + execCommand(`npx eslint ${vueKeyword} --quiet -o ${outputVue} --format json || true`, workspace, 'inherit'); + + execCommand(`npx stylelint ${sassKeyword} --quiet -o ${outputScss} --formatter json || true`, workspace, 'inherit'); + + const { + jsTotal, + jsErrorFiles, + + vueTotal, + vueErrorFiles, + + scssTotal, + scssErrorFiles, + } = parseResult({ + outputJs, + outputVue, + outputScss, + }); + + let commentSuccess = false; + if (mrId) { + const message = genRobotMessage({ + jsTotal, + jsErrorFiles, + + vueTotal, + vueErrorFiles, + + scssTotal, + scssErrorFiles, + + checkAll, + mrUrl, + sourceBranch, + targetBranch, + + buildUrl, + docLink, + + repo, + repoUrl, + }); + + try { + commentSuccess = await createMRComment({ + projectName: repo, + mrId, + data: message, + privateToken, + gitApiPrefix, + }); + } catch (err) { + } + } + + + console.log('[commentSuccess]', commentSuccess); + + const getPostFix = () => { + if (!mrId) return ''; + return commentSuccess ? '评论成功' : '评论失败'; + }; + + const robotMessage = genRobotMessage({ + jsTotal, + jsErrorFiles, + + vueTotal, + vueErrorFiles, + + scssTotal, + scssErrorFiles, + + postFix: getPostFix(), + mrUrl, + sourceBranch, + targetBranch, + + buildUrl, + docLink, + checkAll, + + repo, + repoUrl, + }); + + + await batchSendWxRobotMarkdown({ + content: robotMessage, + chatId, + webhookUrl, + }); + + if (mrId) { + await tryCreateMRNote({ + projectName: repo, + mrId, + errorFiles: jsErrorFiles, + workspace, + privateToken, + gitApiPrefix, + }); + await tryCreateMRNote({ + projectName: repo, + mrId, + errorFiles: vueErrorFiles, + workspace, + privateToken, + gitApiPrefix, + }); + await tryCreateMRNote({ + projectName: repo, + mrId, + errorFiles: scssErrorFiles, + isScss: true, + workspace, + privateToken, + gitApiPrefix, + }); + } +} + + +function getErrorInfo(result: Array<{ + errorCount?: number; +}>) { + const errorFiles = result.filter(item => item.errorCount); + const total = errorFiles.reduce((acc, file) => { + acc += file.errorCount || 0; + return acc; + }, 0); + + return { + total, + errorFiles, + }; +} + +function parseResult({ + outputJs, + outputVue, + outputScss, +}: { + outputJs: string; + outputVue: string; + outputScss: string; +}) { + const readyFile = (file: string) => { + if (!fs.existsSync(file)) { + writeFileSync(file, [], true); + return []; + } + return readFileSync(file, true); + }; + + const jsResult = readyFile(outputJs); + const vueResult = readyFile(outputVue); + const scssResult = readyFile(outputScss); + + console.log('\n'); + console.log('[jsResult] \n', JSON.stringify(jsResult, null, 2)); + console.log('[vueResult] \n', JSON.stringify(vueResult, null, 2)); + console.log('[scssResult] \n', JSON.stringify(scssResult, null, 2)); + console.log('\n'); + + const { + total: jsTotal, + errorFiles: jsErrorFiles, + } = getErrorInfo(jsResult); + + const { + total: vueTotal, + errorFiles: vueErrorFiles, + } = getErrorInfo(vueResult); + + const parsed = scssResult.filter((item: Record) => item.warnings?.length).map((item: any) => ({ + ...item, + errorCount: item.warnings.filter((warn: Record) => warn.severity === 'error').length, + })); + + const { + total: scssTotal, + errorFiles: scssErrorFiles, + } = getErrorInfo(parsed); + + + return { + jsTotal, + jsErrorFiles, + + vueTotal, + vueErrorFiles, + + scssTotal, + scssErrorFiles, + }; +} + +function genRobotMessage({ + jsTotal, + jsErrorFiles, + + vueTotal, + vueErrorFiles, + + scssTotal, + scssErrorFiles, + + mrUrl, + sourceBranch, + targetBranch, + + buildUrl, + docLink, + + repo, + repoUrl, + + postFix, + checkAll = false, +}: { + jsTotal: number; + jsErrorFiles: JSErrorFile[]; + + vueTotal: number; + vueErrorFiles: JSErrorFile[]; + + scssTotal: number; + scssErrorFiles: SCSSErrorFile[] + + mrUrl?: string; + sourceBranch?: string; + targetBranch?: string; + + buildUrl: string; + docLink: string; + + repo?: string; + repoUrl?: string; + + postFix?: string; + checkAll?: boolean; +}) { + const genTitle = (prefix: string) => (`${prefix}${checkAll ? '【LINT】全量模式' : '【LINT】增量模式'}`); + const postFixList = postFix ? [postFix] : []; + const mrInfo = [ + mrUrl ? `[${mrUrl}](${mrUrl})` : '', + (sourceBranch && targetBranch) ? `${sourceBranch} => ${targetBranch}` : '', + ].filter(item => item); + + const repoInfo = (checkAll && repo && repoUrl) ? [`[${repo}](${repoUrl})`] : []; + + if (!jsTotal && !vueTotal && !scssTotal) { + return [ + genTitle('✅'), + ...mrInfo, + ...repoInfo, + '未发现代码规范异常', + ...postFixList, + ].join(','); + } + + return [ + [ + genTitle('⚠️'), + // '遵守代码规范是防止项目腐化的第一步', + ...mrInfo, + ...repoInfo, + `可在[归档产物](${buildUrl})中查看详情,或本地运行 \`npx eslint\` 等命令`, + `[说明文档](${docLink})`, + '<@guowangyang>', + ...postFixList, + ].join(','), + + [`- **JS/TS 错误**:${jsTotal ? `${jsErrorFiles.length}个文件${jsTotal}个错误` : '无'}`], + [`- **Vue 错误**:${vueTotal ? `${vueErrorFiles.length}个文件${vueTotal}个错误` : '无'}`], + [`- **SCSS/CSS 错误**:${scssTotal ? `${scssErrorFiles.length}个文件${scssTotal}个错误` : '无'}`], + ].join('\n'); +} + + +function getDiffFile({ + sourceBranch, + targetBranch, + workspace, +}: { + sourceBranch: string; + targetBranch: string; + workspace: string; +}) { + execCommand(`git clean -df && git reset --hard HEAD && git checkout ${targetBranch} && git pull && git submodule update --init`, workspace, 'inherit'); + execCommand(`git checkout ${sourceBranch} && git reset --hard "origin/${sourceBranch}" && git pull`, workspace, 'inherit'); + + const list = execCommand(`git diff --name-only ${sourceBranch} ${targetBranch}`, workspace, { + stdio: 'pipe', + line: -1, + }).split('\n') + .map(item => item.trim()) + .filter(item => item) + .filter(item => fs.existsSync(path.resolve(workspace, item))); + + console.log('diff list: ', JSON.stringify(list, null, 2)); + + const jsTsFiles = list.filter(item => item.endsWith('.js') || item.endsWith('.ts')); + const vueFiles = list.filter(item => item.endsWith('.vue')); + const scssFiles = list.filter(item => item.endsWith('.scss') || item.endsWith('.css')); + + return { + jsTsFiles, + vueFiles, + scssFiles, + }; +} + diff --git a/src/lint/types.ts b/src/lint/types.ts new file mode 100644 index 00000000..561325a2 --- /dev/null +++ b/src/lint/types.ts @@ -0,0 +1,2 @@ +export type JSErrorFile = Record; +export type SCSSErrorFile = Record; diff --git a/src/mp-ci/mp-upload-and-report.ts b/src/mp-ci/mp-upload-and-report.ts index e018bd9f..1f8eb024 100644 --- a/src/mp-ci/mp-upload-and-report.ts +++ b/src/mp-ci/mp-upload-and-report.ts @@ -32,7 +32,7 @@ async function reportToRd({ bkBuildUrl, bkPipelineId, }: Record) { - if (!bundleInfo || !bundleInfo.__APP__) return; + if (!bundleInfo?.__APP__) return; const mainBundleSize = parseInt(`${(bundleInfo.__APP__?.size || 0) / 1024}`, 10); const totalBundleSize = parseInt(`${(bundleInfo.__FULL__?.size || 0) / 1024}`, 10); diff --git a/src/node/node-command.ts b/src/node/node-command.ts index 6f799c63..f5a97f94 100644 --- a/src/node/node-command.ts +++ b/src/node/node-command.ts @@ -6,23 +6,46 @@ * 这个方法会对输出结果截断,只返回第一行内容 * @param {string} command 命令 * @param {string} root 执行命令的目录 - * @param {string} stdio 结果输出,默认为 pipe + * @param {string | object} stdio 结果输出,默认为 pipe * @returns {string} 命令执行结果 */ -export function execCommand(command: string, root?: string, stdio?: string): string { +export function execCommand(command: string, root?: string, options?: string | { + stdio?: string; + line?: number; +}): string { if (!root) { root = process.cwd(); } const { execSync } = require('child_process'); - return ( - execSync(command, { - cwd: root || process.cwd(), - encoding: 'utf-8', - stdio: stdio || 'pipe', - }) - ?.split('\n')[0] - ?.trim() || '' - ); + + let innerOptions: { + stdio?: string; + line?: number; + } = {}; + + if (typeof options === 'string') { + innerOptions = { + stdio: options, + }; + } else if (typeof options === 'object') { + innerOptions = options; + } + + const stdio = innerOptions?.stdio ?? 'pipe'; + const line = innerOptions?.line ?? 0; + + const res = execSync(command, { + cwd: root || process.cwd(), + encoding: 'utf-8', + stdio: stdio || 'pipe', + }); + + if (line > -1) { + return res?.split('\n')[line] + ?.trim() || ''; + } + + return res; } diff --git a/src/router/uni-hook-router.ts b/src/router/uni-hook-router.ts index 078930fa..99fce6d4 100644 --- a/src/router/uni-hook-router.ts +++ b/src/router/uni-hook-router.ts @@ -1,16 +1,82 @@ +/** + * 拦截路由 + * + * @example + * ```ts + * uniHookRouter({ + * navigateToHooks: [ + * () => console.log('1') + * ], + * navigateBackHooks: [ + * () => console.log('2') + * ], + * redirectToHooks: [ + * () => console.log('3') + * ], + * debug: true, + * }) + * ``` + */ export function uniHookRouter({ navigateToHooks, navigateBackHooks, redirectToHooks, + + tryUniInterCeptor = true, + debug = false, }: { navigateToHooks?: Array; navigateBackHooks?: Array; redirectToHooks?: Array; + + tryUniInterCeptor?: boolean; + debug?: boolean; }) { const originNavigateTo = uni.navigateTo; const originNavigateBack = uni.navigateBack; const originReplaceTo = uni.redirectTo; + + const toDebug = (name: string, callbacks?: Array) => ({ + invoke(...args: Array) { + callbacks?.forEach(cb => cb?.(...args)); + if (debug) { + console.log(`>>> uniHookRouter ${name} invoke`); + } + }, + success() { + if (debug) { + console.log(`>>> uniHookRouter ${name} success`); + } + }, + fail() { + if (debug) { + console.log(`>>> uniHookRouter ${name} fail`); + } + }, + complete() { + if (debug) { + console.log(`>>> uniHookRouter ${name} complete`); + } + }, + }); + + if (tryUniInterCeptor && typeof uni.addInterceptor === 'function') { + uni.addInterceptor('navigateTo', { + ...toDebug('navigateTo', navigateToHooks), + }); + + uni.addInterceptor('navigateBack', { + ...toDebug('navigateBack', navigateBackHooks), + }); + + uni.addInterceptor('redirectTo', { + ...toDebug('redirectTo', redirectToHooks), + }); + + return; + } + if (originNavigateTo) { uni.navigateTo = (...args: Array) => { navigateToHooks?.forEach(cb => cb?.(...args)); diff --git a/src/tgit/project.ts b/src/tgit/project.ts index 4b9df536..0973f1c6 100644 --- a/src/tgit/project.ts +++ b/src/tgit/project.ts @@ -115,7 +115,7 @@ export async function getAllProjects(privateToken: string, search = ''): Promise }); res = res.concat(temp); page += 1; - if (!temp || !temp.length) { + if (!temp?.length) { break; } } diff --git a/src/uni-app/index.ts b/src/uni-app/index.ts index 5b32ee51..0e602a11 100644 --- a/src/uni-app/index.ts +++ b/src/uni-app/index.ts @@ -1 +1,2 @@ export { getRouteLeaveCache } from './route-leave-cache'; +export { startUniProject } from './start'; diff --git a/src/uni-app/start.ts b/src/uni-app/start.ts new file mode 100644 index 00000000..ce520a58 --- /dev/null +++ b/src/uni-app/start.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ + + +/** + * 启动 uni-app 项目 + * @param options 参数 + * + * @example + * ```ts + * startUniProject(); + * + * startUniProject({ + * debug: false, // 默认为 true,会打印参数 + * }) + * ``` + */ +export function startUniProject(options?: { + debug?: boolean +}) { + const { spawnSync } = require('child_process'); + const isWindows = require('os').platform() === 'win32'; + + let command = isWindows ? 'npm.cmd' : 'npm'; + const realArgv = process.argv.slice(2); + const debug = options?.debug ?? true; + + if (debug) { + console.log('>>> startUniProject realArgv: ', realArgv); + } + + let otherArgv = ['run', ...realArgv]; + + if (realArgv[0] === 'uni') { + command = isWindows ? 'npx.cmd' : 'npx'; + otherArgv = realArgv; + } + + spawnSync(command, otherArgv, { stdio: 'inherit', shell: true }); +} diff --git a/src/v-console/debug.ts b/src/v-console/debug.ts index 5182082a..e8cde2aa 100644 --- a/src/v-console/debug.ts +++ b/src/v-console/debug.ts @@ -44,8 +44,8 @@ export function genVConsole({ loadVConsole(vConsoleConfig); }) // 异常捕获,避免 TAM PROMISE_ERROR 错误上报 - .catch((error: any) => { - console.log('checkIsDevList', error); - }); + .catch((error: any) => { + console.log('checkIsDevList', error); + }); } } diff --git a/src/version-tip/gen-version-tip.ts b/src/version-tip/gen-version-tip.ts index 7507734f..ca819e72 100644 --- a/src/version-tip/gen-version-tip.ts +++ b/src/version-tip/gen-version-tip.ts @@ -27,7 +27,7 @@ export function parseChangeLog({ // 大版本 // 不是第一个版本 - if (!currentVersion || !currentVersion[0]) { + if (!currentVersion?.[0]) { currentVersion = changelogStr.match(new RegExp( `(?<=## \\[${targetVersion}\\].*\n).*?(?=\n##+ \\[?\\d+.\\d+.\\d+)`, 's', @@ -35,7 +35,7 @@ export function parseChangeLog({ } // changeLog 的另一种形式,lerna 生成的 - if (!currentVersion || !currentVersion[0]) { + if (!currentVersion?.[0]) { changelogStr = changelogStr.replace(/<\/?small>/g, ''); currentVersion = changelogStr.match(new RegExp( `(?<=## ${targetVersion}.*\n).*?(?=\n##+ ?\\d+.\\d+.\\d+)`, @@ -45,7 +45,7 @@ export function parseChangeLog({ // 非大版本 // 第一个版本 - if (!currentVersion || !currentVersion[0]) { + if (!currentVersion?.[0]) { currentVersion = changelogStr.match(new RegExp( `(?<=### ${targetVersion}.*\n).*`, 's', @@ -54,14 +54,14 @@ export function parseChangeLog({ // 大版本 // 第一个版本 - if (!currentVersion || !currentVersion[0]) { + if (!currentVersion?.[0]) { currentVersion = changelogStr.match(new RegExp( `(?<=## ${targetVersion}.*\n).*`, 's', )); } - if (!currentVersion || !currentVersion[0]) { + if (!currentVersion?.[0]) { console.log(`[GEN VERSION TIP] ERROR: NOT FOUND CHANGELOG INFO OF ${targetVersion} `); return ''; } diff --git a/test/base/object.test.ts b/test/base/object.test.ts index bc0f9f51..ed8d5c96 100644 --- a/test/base/object.test.ts +++ b/test/base/object.test.ts @@ -98,4 +98,11 @@ describe('sliceObject', () => { b: { b: 2 }, }); }); + + it('sliceObject empty', () => { + // @ts-expect-error + expect(sliceObject(null, 2)).toEqual({}); + // @ts-expect-error + expect(sliceObject(undefined, 2)).toEqual({}); + }); });