├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelint.js ├── README.md ├── package.json ├── scripts └── release.ts ├── src ├── addTag │ └── index.ts ├── build │ └── index.ts ├── config │ ├── constans.tsx │ └── functions.ts ├── eslint │ └── index.ts ├── gitPush │ └── index.ts ├── index.ts ├── publishNpm │ └── index.ts ├── selectNextVersion │ └── index.ts ├── setChangelog │ └── index.ts └── stylelint │ └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /esm 3 | /lib 4 | /types 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins和extends的区别 3 | * 如果eslint里没有的规则需要plugin做拓展 4 | */ 5 | module.exports = { 6 | /** 7 | * node或者浏览器中的全局变量很多,如果我们一个个进行声明显得繁琐, 8 | * 因此就需要用到我们的env,这是对环境定义的一组全局变量的预设 9 | */ 10 | env: { 11 | browser: true, 12 | commonjs: true, 13 | es6: true, 14 | node: true, 15 | jest: true, 16 | }, 17 | parser: '@typescript-eslint/parser', 18 | /** 19 | * 插件是一个 npm 包,通常输出规则。一些插件也可以输出一个或多个命名的配置。要确保这个包安装在 ESLint 能请求到的目录下。 20 | * plugins属性值可以省略包名的前缀eslint-plugin-。extends属性值可以由以下组成: 21 | * plugin:包名 (省略了前缀,比如,react)配置名称 (比如recommended) 22 | * 插件一个主要的作用就是补充规则,比如eslint:recommended中没有有关react的规则,则需要另外导入规则插件eslint-plugin-react 23 | */ 24 | plugins: ['react', 'babel', '@typescript-eslint/eslint-plugin'], 25 | /** 26 | * eslint:开头的ESLint官方扩展,有两个:eslint:recommended(推荐规范)和eslint:all(所有规范)。 27 | * plugin:开头的扩展是插件类型扩展 28 | * eslint-config:开头的来自npm包,使用时可以省略eslint-config- 29 | * @:开头的扩展和eslint-config一样,是在npm包上面加了一层作用域scope 30 | * 需要注意的是:多个扩展中有相同的规则,以后面引入的扩展中规则为准。 31 | */ 32 | extends: ['airbnb', 'plugin:react/recommended', 'plugin:prettier/recommended', 'plugin:react-hooks/recommended'], 33 | parserOptions: { 34 | sourceType: 'module', 35 | ecmaFeatures: { 36 | experimentalObjectRestSpread: true, 37 | jsx: true, // 启用 JSX 38 | }, 39 | }, 40 | globals: { 41 | describe: false, 42 | it: false, 43 | expect: false, 44 | jest: false, 45 | afterEach: false, 46 | beforeEach: false, 47 | }, 48 | rules: { 49 | 'react/prop-types': 0, 50 | 'react/display-name': 0, 51 | 'react/jsx-no-target-blank': 0, // 允许 target 等于 blank 52 | 'react/jsx-key': 1, // jsx 中的遍历,需要加 key 属性,没有会提示警告 53 | 'react/no-find-dom-node': 0, 54 | indent: [2, 2, { SwitchCase: 1 }], // 缩进 2 格,jquery 项目可忽略。switch 和 case 之间缩进两个 55 | 'jsx-quotes': [2, 'prefer-double'], // jsx 属性统一使用双引号 56 | 'max-len': [1, { code: 140 }], // 渐进式调整,先设置最大长度为 140,同时只是警告 57 | 'no-mixed-spaces-and-tabs': 2, 58 | 'no-tabs': 2, 59 | 'no-trailing-spaces': 2, // 语句尾部不能出现空格 60 | quotes: [2, 'single'], // 统一使用单引号 61 | 'space-before-blocks': 2, // if 和函数等,大括号前需要空格 62 | 'space-in-parens': 2, // 括号内前后不加空格 63 | 'space-infix-ops': 2, // 中缀(二元)操作符前后加空格 64 | 'spaced-comment': 2, // 注释双斜杠后保留一个空格 65 | '@typescript-eslint/explicit-function-return-type': 0, 66 | '@typescript-eslint/no-explicit-any': 0, 67 | '@typescript-eslint/no-non-null-assertion': 0, 68 | '@typescript-eslint/ban-ts-ignore': 0, 69 | '@typescript-eslint/interface-name-prefix': 0, 70 | '@typescript-eslint/no-use-before-define': 0, 71 | 'react-hooks/rules-of-hooks': 'error', 72 | 'react-hooks/exhaustive-deps': 'warn', 73 | '@typescript-eslint/explicit-module-boundary-types': 0, 74 | '@typescript-eslint/ban-ts-comment': 0, 75 | 'consistent-return': 0, 76 | 'no-underscore-dangle': 0, 77 | 'import/prefer-default-export': 0, 78 | 'import/extensions': 0, 79 | 'import/no-unresolved': 0, 80 | camelcase: 0, 81 | 'no-plusplus': 0, 82 | 'no-param-reassign': 0, 83 | 'no-console': 0, 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom ignore 2 | /build 3 | /dist 4 | /lib 5 | /esm 6 | /types 7 | /coverage 8 | .vscode 9 | yarn.lock 10 | 11 | # General 12 | .DS_Store 13 | .AppleDouble 14 | .LSOverride 15 | 16 | # Thumbnails 17 | ._* 18 | 19 | # Files that might appear in the root of a volume 20 | .DocumentRevisions-V100 21 | .fseventsd 22 | .Spotlight-V100 23 | .TemporaryItems 24 | .Trashes 25 | .VolumeIcon.icns 26 | .com.apple.timemachine.donotpresent 27 | 28 | # Directories potentially created on remote AFP share 29 | .AppleDB 30 | .AppleDesktop 31 | Network Trash Folder 32 | Temporary Items 33 | .apdisk 34 | 35 | .vscode* 36 | !.vscodesettings.json 37 | !.vscodetasks.json 38 | !.vscodelaunch.json 39 | !.vscodeextensions.json 40 | *.code-workspace 41 | 42 | # Local History for Visual Studio Code 43 | .history 44 | 45 | # Logs 46 | logs 47 | *.log 48 | npm-debug.log* 49 | yarn-debug.log* 50 | yarn-error.log* 51 | lerna-debug.log* 52 | 53 | # Diagnostic reports (https:nodejs.orgapireport.html) 54 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 55 | 56 | # Runtime data 57 | pids 58 | *.pid 59 | *.seed 60 | *.pid.lock 61 | 62 | # Directory for instrumented libs generated by jscoverageJSCover 63 | lib-cov 64 | 65 | # Coverage directory used by tools like istanbul 66 | coverage 67 | *.lcov 68 | 69 | # nyc test coverage 70 | .nyc_output 71 | 72 | # Grunt intermediate storage (https:gruntjs.comcreating-plugins#storing-task-files) 73 | .grunt 74 | 75 | # Bower dependency directory (https:bower.io) 76 | bower_components 77 | 78 | # node-waf configuration 79 | .lock-wscript 80 | 81 | # Compiled binary addons (https:nodejs.orgapiaddons.html) 82 | buildRelease 83 | 84 | # Dependency directories 85 | node_modules 86 | jspm_packages 87 | 88 | # Snowpack dependency directory (https:snowpack.dev) 89 | web_modules 90 | 91 | # TypeScript cache 92 | *.tsbuildinfo 93 | 94 | # Optional npm cache directory 95 | .npm 96 | 97 | # Optional eslint cache 98 | .eslintcache 99 | 100 | # Microbundle cache 101 | .rpt2_cache 102 | .rts2_cache_cjs 103 | .rts2_cache_es 104 | .rts2_cache_umd 105 | 106 | # Optional REPL history 107 | .node_repl_history 108 | 109 | # Output of 'npm pack' 110 | *.tgz 111 | 112 | # Yarn Integrity file 113 | .yarn-integrity 114 | 115 | # dotenv environment variables file 116 | .env 117 | .env.test 118 | 119 | # parcel-bundler cache (https:parceljs.org) 120 | .cache 121 | .parcel-cache 122 | 123 | # Next.js build output 124 | .next 125 | out 126 | 127 | # Nuxt.js build generate output 128 | .nuxt 129 | dist 130 | 131 | # Gatsby files 132 | .cache 133 | # Comment in the public line in if your project uses Gatsby and not Next.js 134 | # https:nextjs.orgblognext-9-1#public-directory-support 135 | # public 136 | 137 | # vuepress build output 138 | .vuepressdist 139 | 140 | # Serverless directories 141 | .serverless 142 | 143 | # FuseBox cache 144 | .fusebox 145 | 146 | # DynamoDB Local files 147 | .dynamodb 148 | 149 | # TernJS port file 150 | .tern-port 151 | 152 | # Stores VSCode versions used for testing VSCode extensions 153 | .vscode-test 154 | 155 | # yarn v2 156 | .yarncache 157 | .yarnunplugged 158 | .yarnbuild-state.yml 159 | .yarninstall-state.gz 160 | .pnp.* 161 | 162 | # Windows thumbnail cache files 163 | Thumbs.db 164 | Thumbs.db:encryptable 165 | ehthumbs.db 166 | ehthumbs_vista.db 167 | 168 | # Dump file 169 | *.stackdump 170 | 171 | # Folder config file 172 | [Dd]esktop.ini 173 | 174 | # Recycle Bin used on file shares 175 | $RECYCLE.BIN 176 | 177 | # Windows Installer files 178 | *.cab 179 | *.msi 180 | *.msix 181 | *.msm 182 | *.msp 183 | 184 | # Windows shortcuts 185 | *.lnk 186 | 187 | *~ 188 | 189 | # temporary files which can be created if a process still has a handle open of a deleted file 190 | .fuse_hidden* 191 | 192 | # KDE directory preferences 193 | .directory 194 | 195 | # Linux trash folder which might appear on any partition or disk 196 | .Trash-* 197 | 198 | # .nfs files are created when an open file is removed but is still being accessed 199 | .nfs* 200 | 201 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 202 | # Reference: https:intellij-support.jetbrains.comhcen-usarticles206544839 203 | 204 | # User-specific stuff 205 | .idea**workspace.xml 206 | .idea**tasks.xml 207 | .idea**usage.statistics.xml 208 | .idea**dictionaries 209 | .idea**shelf 210 | 211 | # Generated files 212 | .idea**contentModel.xml 213 | 214 | # Sensitive or high-churn files 215 | .idea**dataSources 216 | .idea**dataSources.ids 217 | .idea**dataSources.local.xml 218 | .idea**sqlDataSources.xml 219 | .idea**dynamic.xml 220 | .idea**uiDesigner.xml 221 | .idea**dbnavigator.xml 222 | 223 | # Gradle 224 | .idea**gradle.xml 225 | .idea**libraries 226 | 227 | # Gradle and Maven with auto-import 228 | # When using Gradle or Maven with auto-import, you should exclude module files, 229 | # since they will be recreated, and may cause churn. Uncomment if using 230 | # auto-import. 231 | # .ideaartifacts 232 | # .ideacompiler.xml 233 | # .ideajarRepositories.xml 234 | # .ideamodules.xml 235 | # .idea*.iml 236 | # .ideamodules 237 | # *.iml 238 | # *.ipr 239 | 240 | # CMake 241 | cmake-build-* 242 | 243 | # Mongo Explorer plugin 244 | .idea**mongoSettings.xml 245 | 246 | # File-based project format 247 | *.iws 248 | 249 | # IntelliJ 250 | out 251 | 252 | # mpeltonensbt-idea plugin 253 | .idea_modules 254 | 255 | # JIRA plugin 256 | atlassian-ide-plugin.xml 257 | 258 | # Cursive Clojure plugin 259 | .ideareplstate.xml 260 | 261 | # Crashlytics plugin (for Android Studio and IntelliJ) 262 | com_crashlytics_export_strings.xml 263 | crashlytics.properties 264 | crashlytics-build.properties 265 | fabric.properties 266 | 267 | # Editor-based Rest Client 268 | .ideahttpRequests 269 | 270 | # Android studio 3.1+ serialized cache file 271 | .ideacachesbuild_file_checksums.ser 272 | 273 | # Cache files for Sublime Text 274 | *.tmlanguage.cache 275 | *.tmPreferences.cache 276 | *.stTheme.cache 277 | 278 | # Workspace files are user-specific 279 | *.sublime-workspace 280 | 281 | # Project files should be checked into the repository, unless a significant 282 | # proportion of contributors will probably not be using Sublime Text 283 | # *.sublime-project 284 | 285 | # SFTP configuration file 286 | sftp-config.json 287 | sftp-config-alt*.json 288 | 289 | # Package control specific files 290 | Package Control.last-run 291 | Package Control.ca-list 292 | Package Control.ca-bundle 293 | Package Control.system-ca-bundle 294 | Package Control.cache 295 | Package Control.ca-certs 296 | Package Control.merged-ca-bundle 297 | Package Control.user-ca-bundle 298 | oscrypto-ca-bundle.crt 299 | bh_unicode_properties.cache 300 | 301 | # Sublime-github package stores a github token in this file 302 | # https:packagecontrol.iopackagessublime-github 303 | GitHub.sublime-settings 304 | 305 | # Swap 306 | [._]*.s[a-v][a-z] 307 | !*.svg # comment out if you don't need vector files 308 | [._]*.sw[a-p] 309 | [._]s[a-rt-v][a-z] 310 | [._]ss[a-gi-z] 311 | [._]sw[a-p] 312 | 313 | # Session 314 | Session.vim 315 | Sessionx.vim 316 | 317 | # Temporary 318 | .netrwhist 319 | *~ 320 | # Auto-generated tag files 321 | tags 322 | # Persistent undo 323 | [._]*.un~ 324 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /esm 3 | /lib 4 | /types 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "jsxSingleQuote": true, 6 | "endOfLine": "lf", 7 | "printWidth": 120, 8 | "proseWrap": "never", 9 | "bracketSpacing": true, 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /.stylelint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-standard', 'stylelint-config-prettier'], 3 | customSyntax: 'postcss-less', 4 | rules: { 5 | 'no-descending-specificity': null, 6 | 'no-duplicate-selectors': null, 7 | 'font-family-no-missing-generic-family-keyword': null, 8 | 'block-opening-brace-space-before': 'always', 9 | 'declaration-block-trailing-semicolon': null, 10 | 'declaration-colon-newline-after': null, 11 | indentation: null, 12 | 'selector-descendant-combinator-no-non-space': null, 13 | 'selector-class-pattern': null, 14 | 'keyframes-name-pattern': null, 15 | 'no-invalid-position-at-import-rule': null, 16 | 'number-max-precision': 6, 17 | 'color-function-notation': null, 18 | 'selector-pseudo-class-no-unknown': [ 19 | true, 20 | { 21 | ignorePseudoClasses: ['global'], 22 | }, 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 文档如下 2 | 3 | ## 前言 4 | 兄弟们,这个东西是真的真的好用,为什么需要自动化发布脚本呢? 5 | 如果你们目前的项目是这样push代码的,就有大问题: 6 | ```bash 7 | git add -A 8 | git commit -m "xxx" 9 | git push 10 | ``` 11 | - 首先,你push版本前是不是要手动修改package.json里的版本号?是不是很麻烦? 12 | - 其次,代码如果要打tag,你是不是要手动git tag vXXX还要git push origin tag把tag单独推到git仓库? 13 | - 还有,你commit不符合angular规范咋办(比如修改bug是'fix:' 开头, 新增功能是'feat:' 开头) 14 | - 还有,修改了commit之后如何自动生成changelog? 15 | - 最重要的是你如果自己写bash 脚本,如何保证你代码的拓展性,比如我要加减一个流程,你必须面向过程编码去改bash脚本,而不能轻易的抽象出一个可配置化的流程脚本。 16 | 17 | ## 看看这个npm包咋样 18 | 19 | 这是我自己写的一个可配置化的node发布脚本,先看看效果: 20 | 21 | 我在自己项目的根目录建立一个scripts文件夹,下面建立一个release.ts 脚本 22 | ```typescript 23 | import defaultRelaseProcess from '@mx-design/release'; 24 | defaultRelaseProcess(); 25 | ``` 26 | package.json 27 | ```typescript 28 | "scripts": { 29 | // 不用担心ts-node依赖,@mx-design/release包会帮你安装 30 | "release": "ts-node ./scripts/release.ts" 31 | }, 32 | "devDependencies": { 33 | "@mx-design/release": "^1.0.0", 34 | }, 35 | ``` 36 | 然后命令行界面到你的项目根目录,输入: 37 | ``` 38 | npm run release 39 | ``` 40 | 然后你的命令行界面会显示如下: 41 | 1、询问你要升级的版本 42 | 43 | ![34182EEF-6E27-41B9-AE4F-41C6AFEFB6E7.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43a79c4a71324340a28d5d6efd63070c~tplv-k3u1fbpfcp-watermark.image?) 44 | 45 | 看到了吧,你可以自由选择是主版本升级,还是次版本,或者其他版本,都是符合semver规范的。 46 | 47 | 2、选择后会让你填写commit信息,如果你的commit的信息不符合angular提交规范,会报错提示你不符合规范,并且终止脚本。(整体交互很舒服,比如你git push的时候,我们会显示正在push loading,push成功,就会覆盖当时正在push的文字,颜色标绿并显示对钩的图标) 48 | 49 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6e6003e6342e4d7391c37c7842a575cb~tplv-k3u1fbpfcp-watermark.image?) 50 | 51 | 交互loading如下: 52 | 53 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce82b0c30baf48828c618bb65633375e~tplv-k3u1fbpfcp-watermark.image?) 54 | 55 | 如果命令执行错误,显示如下(国内github环境非常差,经常git push失败,用vpn还是一样的): 56 | 57 | ![109CAAC1-709A-4686-86A9-AF64D960E984.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9b7c615fa774426a802e6b816a5be4c~tplv-k3u1fbpfcp-watermark.image?) 58 | 59 | git push成功效果如下: 60 | 61 | ![C74EFAE8-C49A-447F-8E14-093B2ACBD947.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f312cf16ce94707b72390e311f0eaee~tplv-k3u1fbpfcp-watermark.image?) 62 | 63 | 3、这里release的主要流程完成如下: 64 | 65 | ![4E008797-7E12-4619-B39A-A3C8E131B4C1.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3aa5ef328d464e2ebf501d5931c3df87~tplv-k3u1fbpfcp-watermark.image?) 66 | 67 | 流程包括: 68 | - 选择升级版本 -> 69 | - 修改package.json的版本号 -> 70 | - 填写commit信息(校验格式,不通过终止) -> 71 | - 推送到代码仓库(推送失败会帮你把package.json里的版本回退) -> 72 | - 生成changeLog -> 73 | - **build打包(这里执行的是npm run build 所以你的scripts里必须有build命令,要不执行失败)** -> 74 | - 发布包(执行的 npm publish --access=public 命令) -> 75 | - 打tag并且推送到git仓库 76 | 77 | 78 | ## 自定义流程 79 | 上面几个流程是这个包自带流程顺序,你可以配自己的顺序, 80 | ```typescript 81 | import { getNextVersion, gitPush, setChangelog, build, publishNpm, addTag, compose } from '@mx-design/release'; 82 | 83 | // getNextVersion 获取下个版本号函数 84 | // updateVersion 修改版本号 返回值是回退版本号的函数 85 | // gitPush push代码函数 86 | // setChangelog 设置changeLog函数 87 | // build 执行npm run build函数 88 | // publish 执行npm publish函数 89 | // addTag 打tag函数 90 | // compose函数组合器 91 | 92 | // 使用方法,这些函数顺序可以自己换位置,或者增删减 93 | const middle = [getNextVersion, updateVersion, gitPush, setChangelog, build, publishNpm, addTag]; 94 | 95 | function execRelease() { 96 | compose(middle); 97 | } 98 | ``` 99 | 100 | ## 寻求共建 + 源码分析 101 | 102 | 本项目随时欢迎任何有兴趣的同学加入,评论区写自己github账号,或者issue上留言,我会第一时间加你到核心开发成员列表。 103 | 104 | 这个项目还缺少一些东西,比如eslint、stylelint校验,自定义changelog位置,增加单元测试,加入git workflow 等等,我没那么多时间做,因为今年还有很多任务排着呢。。。 105 | 106 | ## 源码分析 107 | 108 | 目录含义: 109 | ``` 110 | - 根目录 111 | - src 112 | - addTag 113 | - index.ts // 打tag函数 114 | - build 115 | - build.ts // build项目函数 116 | - config // 配置文件夹 117 | - functions 工具函数 118 | - contstants 常量 119 | - gitPush 120 | - index.ts // push到git函数 121 | - publishNpm 122 | - index.ts // publish 整个项目的函数 123 | - selectNextVersion 124 | - index.ts 选择下个版本号函数 125 | - setChangeLog 126 | - 打changeLog函数 127 | package.json 128 | tsconfig.json 129 | ... 普通文件没啥好讲的 130 | ``` 131 | 核心中间件执行器compose,这个函数的灵感来源于koa框架,说白了就是一个中间件组合函数,并且能共享数据,代码如下,后面我们会举个例子,来看它的执行机制: 132 | ```javascript 133 | export function compose(middleware) { 134 | // 共享数据仓库,每个函数都可以往里面加数据,所有函数都可以访问到里面的数据 135 | const otherOptions = {}; 136 | // 函数发射器,一个函数执行完毕调用next(),就执行下一个函数 137 | function dispatch(index, otherOptions) { 138 | // 每次从middleware中间件里面选取一个函数执行,直到执行了全部函数 139 | if (index == middleware.length) return; 140 | const currMiddleware = middleware[index]; 141 | // 执行函数,传入了一下一个函数的dispatch,已经更新共享数据 142 | currMiddleware((addOptions) => { 143 | dispatch(++index, { ...otherOptions, ...addOptions }); 144 | }, otherOptions).catch((error) => { 145 | console.log('💣 发布失败,失败原因:', error); 146 | }); 147 | } 148 | dispatch(0, otherOptions); 149 | } 150 | ``` 151 | middleware写法 152 | ```javascript 153 | // 获取版本号函数,固定参数第一个是next,表示调用middleware里下一个函数 154 | const getNextVersion = async (next) => { 155 | // _selectNextVersion会查看你package.json里的version参数是啥版本 156 | const nextVersion = await _selectNextVersion(); 157 | // 缓存老的package.json数据,为了后面可以回退版本有老版本数据做准备 158 | const originPackageJson = getOriginPackageJson(); 159 | // next传入的数据都会放到共享数据仓库,下面的函数都可以拿到这些数据 160 | next({ 161 | nextVersion, 162 | originVersion: originPackageJson.version, 163 | originPackageJson, 164 | }); 165 | }; 166 | 167 | // 更新版本号方法 168 | const updateVersion = async (next, otherOptions) => { 169 | if (!otherOptions?.nextVersion) { 170 | throw new Error('请传入package.json新版本号'); 171 | } 172 | // 返回一个回退版本号的方法,上面的函数共享的originPackageJson数据就是给他用的 173 | // basicCatchError如果命令执行失败,就打印失败原因,并且返回false 174 | const backVersionFn = await _updateVersion(otherOptions.nextVersion, otherOptions.originPackageJson).catch( 175 | basicCatchError, 176 | ); 177 | // 把回退版本的函数共享出来,给下面需要用的地方自己调用 178 | next({ backVersionFn }); 179 | }; 180 | ``` 181 | 案例说明 182 | ``` 183 | compose([getNextVersion, updateVersion]) 184 | ``` 185 | 首先compose会调用 186 | - dispatch(0, otherOptions) 187 | - 0代表compose传参的middleware数组里的第一项 188 | - otherOptions代表共享数据,目前是空对象 189 | 190 | 接着, 第一个函数调用 191 | - getNextVersion((addOptions) => { 192 | dispatch(++index, { ...otherOptions, ...addOptions }); 193 | }, otherOptions); 194 | - 看第一个参数,我们是这么写的 195 | - const getNextVersion = async (next) => { 196 | - 也就是说第一个参数next调用后,实际上是调用dispatch函数,dispatch函数的参数index + 1,就调用了middleware数组里,第二个函数了 197 | - dispatch第二个参数就是把next想共享的数据发下去 198 | 199 | 好了源码分析完了,我今年主要是任务是写react pc和移动端组件库,之前写了cli打包和开发的脚手架,文章如下: 200 | https://juejin.cn/post/7075187294096850951 201 | 202 | 这个项目也是,有兴趣加入核心贡献者,直接留言,把github账号说下,第一时间加你进来,多谢点个star哦 203 | 现在完成了发布脚本,接着就是搭建组件库展示网站了,也是自己写,没有用现成的dumi或者storybook。 204 | 205 | 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mx-design/release", 3 | "version": "2.2.29", 4 | "description": "Lightweight release script for Front-end project", 5 | "keywords": [ 6 | "mx-release", 7 | "mx release", 8 | "release script", 9 | "automated release", 10 | "js release script" 11 | ], 12 | "main": "lib/index.js", 13 | "module": "esm/index.js", 14 | "license": "MIT", 15 | "author": "1334196450@qq.com", 16 | "typings": "types/index.d.ts", 17 | "files": [ 18 | "lib", 19 | "bin", 20 | "esm", 21 | "types" 22 | ], 23 | "scripts": { 24 | "build:types": "rimraf types && tsc --outDir types -d --emitDeclarationOnly", 25 | "build:es": "rimraf esm && mx buildLib --mode esm", 26 | "build:cjs": "rimraf lib && mx buildLib --mode cjs", 27 | "build": "yarn build:types && yarn build:cjs && yarn build:es", 28 | "clean": "rimraf esm lib types", 29 | "release": "ts-node ./scripts/release.ts" 30 | }, 31 | "dependencies": { 32 | "@mx-design/cli": "1.0.13", 33 | "chalk": "^4.1.1", 34 | "conventional-changelog-cli": "^2.2.2", 35 | "inquirer": "8.2.0", 36 | "ora": "^5.1.0", 37 | "rimraf": "^3.0.2", 38 | "semver": "7.3.5", 39 | "typescript": "^4.5.4", 40 | "eslint": "^8.9.0", 41 | "eslint-config-airbnb": "^17.1.0", 42 | "eslint-config-prettier": "^6.5.0", 43 | "eslint-plugin-babel": "^5.3.0", 44 | "eslint-plugin-import": "^2.14.0", 45 | "eslint-plugin-prettier": "^3.1.1", 46 | "eslint-plugin-react": "^7.26.0", 47 | "eslint-plugin-jsx-a11y": "^6.1.2", 48 | "eslint-plugin-react-hooks": "^4.0.8", 49 | "stylelint": "^13.13.1", 50 | "stylelint-config-prettier": "^8.0.2", 51 | "stylelint-config-standard": "^22.0.0", 52 | "stylelint-order": "^4.1.0", 53 | "@typescript-eslint/eslint-plugin": "^5.4.0", 54 | "@typescript-eslint/parser": "^5.4.0", 55 | "ts-node": "10.7.0", 56 | "postcss-less": "4.0.1", 57 | "prettier": "^2.5.0", 58 | "pretty-quick": "^3.1.2" 59 | }, 60 | "devDependencies": { 61 | "@types/node": "^15.3.0" 62 | }, 63 | "browserslist": [ 64 | "chrome 49", 65 | "Firefox 45", 66 | "safari 10" 67 | ], 68 | "publishConfig": { 69 | "registry": "https://registry.npmjs.org/" 70 | } 71 | } -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { getNextVersion, gitPush, build, publishNpm, updateVersion, compose, eslint } from '../src/index'; 2 | 3 | const middle = [eslint(), getNextVersion, updateVersion, gitPush, build, publishNpm]; 4 | 5 | compose(middle); 6 | -------------------------------------------------------------------------------- /src/addTag/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLogger, runAsync, taskPre } from '../config/functions'; 2 | 3 | /** 4 | * 打tag提交至git 5 | */ 6 | export async function _addTag(nextVersion: string) { 7 | const spinner = new DefaultLogger(taskPre('打tag并推送至git', 'start')); 8 | await runAsync(`git tag v${nextVersion} && git push origin tag v${nextVersion}`, spinner); 9 | spinner.succeed(taskPre('打tag并推送至git', 'end')); 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /src/build/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLogger, taskPre, runAsync } from '../config/functions'; 2 | 3 | /** 4 | * 组件库打包 5 | */ 6 | export async function _build() { 7 | const spinner = new DefaultLogger(taskPre('准备打包', 'start')); 8 | await runAsync('npm run build', spinner); 9 | spinner.succeed(taskPre('打包完成', 'end')); 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /src/config/constans.tsx: -------------------------------------------------------------------------------- 1 | export const COMMIT_REEOR_MESSAGE = 'commit format error(提交格式不符合angular提交规范)'; 2 | 3 | export const GIT_ADD = 'git add'; 4 | export const GIT_COMMIT = 'git commit'; 5 | export const GIT_PUSH = 'git push'; 6 | 7 | export const CHANGELOG_NAME = 'CHANGELOG.md'; 8 | -------------------------------------------------------------------------------- /src/config/functions.ts: -------------------------------------------------------------------------------- 1 | import child_process from 'child_process'; 2 | import chalk from 'chalk'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import ora, { Color } from 'ora'; 6 | import util from 'util'; 7 | 8 | const { execSync } = child_process; 9 | const exec = util.promisify(child_process.exec); 10 | 11 | export class DefaultLogger { 12 | private _spinner: ora.Ora; 13 | 14 | public constructor(startText: string) { 15 | this._spinner = ora(startText).start(); 16 | } 17 | 18 | public log(text: string, color?: Color): void { 19 | if (color) { 20 | this._spinner.color = color; 21 | } 22 | this._spinner.text = text; 23 | } 24 | 25 | public succeed(text: string): void { 26 | this._spinner.succeed(text); 27 | } 28 | 29 | public fail(text: string): void { 30 | this._spinner.fail(text); 31 | } 32 | } 33 | 34 | export const runSync = (command: string, spinner?: DefaultLogger) => { 35 | try { 36 | return execSync(command, { cwd: process.cwd(), encoding: 'utf8' }); 37 | } catch (error) { 38 | spinner?.fail?.('task fail'); 39 | process.exit(1); 40 | } 41 | }; 42 | 43 | export const runAsync = async (command: string, spinner?: DefaultLogger, hasError?: boolean) => { 44 | try { 45 | await exec(command, { cwd: process.cwd(), encoding: 'utf8' }); 46 | } catch (error) { 47 | if (hasError) console.log(error); 48 | spinner?.fail?.('task fail'); 49 | process.exit(1); 50 | } 51 | }; 52 | 53 | export const taskPre = (logInfo: string, type: 'start' | 'end') => { 54 | if (type === 'start') { 55 | return `task start(开始任务): ${logInfo} \r\n`; 56 | } 57 | return `task end(任务结束): ${logInfo} \r\n`; 58 | }; 59 | 60 | // 获取项目文件 61 | export const getProjectPath = (dir = './'): string => { 62 | return path.join(process.cwd(), dir); 63 | }; 64 | 65 | export function compose(middleware) { 66 | const _otherOptions = {}; 67 | function dispatch(index, otherOptions) { 68 | if (index === middleware.length) return; 69 | const currMiddleware = middleware[index]; 70 | currMiddleware((addOptions) => { 71 | dispatch(++index, { ...otherOptions, ...addOptions }); 72 | }, otherOptions).catch((error) => { 73 | console.log('💣 发布失败,失败原因:', error); 74 | }); 75 | } 76 | dispatch(0, _otherOptions); 77 | } 78 | 79 | /** 80 | * 获取当前package.json的版本号 81 | */ 82 | export const getOriginPackageJson = (): Record => { 83 | const packageJson = JSON.parse(fs.readFileSync(getProjectPath('package.json'), 'utf-8')); 84 | return packageJson; 85 | }; 86 | 87 | /** 88 | * 工具函数,用来捕获并打印错误,返回false 89 | */ 90 | export const basicCatchError = (err: Error) => { 91 | console.log(`\r\n ${chalk.red(err)}`); 92 | return false; 93 | }; 94 | -------------------------------------------------------------------------------- /src/eslint/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLogger, taskPre, runAsync } from '../config/functions'; 2 | 3 | /** 4 | * eslint校验 5 | */ 6 | export async function _eslint(srcDir, configDir) { 7 | const spinner = new DefaultLogger(taskPre('eslint校验中...', 'start')); 8 | await runAsync(`eslint ${srcDir} --ext .ts,.tsx,.js,.jsx --fix --cache --config ${configDir}`, spinner, true); 9 | spinner.succeed(taskPre('eslint检测通过', 'end')); 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /src/gitPush/index.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import { DefaultLogger, taskPre, runAsync, runSync } from '../config/functions'; 3 | import { COMMIT_REEOR_MESSAGE, GIT_ADD, GIT_COMMIT, GIT_PUSH } from '../config/constans'; 4 | 5 | /** 6 | * 交互式选择下一个版本号 7 | * @export prompt 8 | * @return {*} {Promise} 9 | */ 10 | async function checkCommit(): Promise { 11 | const { commitMsg } = await inquirer.prompt([ 12 | { 13 | type: 'string', 14 | name: 'commitMsg', 15 | message: '请输入git commit的信息,需要符合angular commit规范', 16 | }, 17 | ]); 18 | return commitMsg; 19 | } 20 | 21 | /** 22 | * 校验commit信息是否正确 23 | */ 24 | function isMathCommit(commitMsg) { 25 | const isMath = /^(feat|fix|docs|style|refactor|test|chore|perf)(\(.+\))?:\s.+/.test(commitMsg); 26 | if (!isMath) { 27 | throw new Error(COMMIT_REEOR_MESSAGE); 28 | } 29 | } 30 | 31 | /** 32 | * 将代码提交至git 33 | */ 34 | export async function _gitPush() { 35 | const commitMsg = await checkCommit(); 36 | isMathCommit(commitMsg); 37 | 38 | const spinner = new DefaultLogger(taskPre('准备推送代码至git仓库', 'start')); 39 | const curBranchName = runSync('git symbolic-ref --short HEAD'); 40 | const isExistCurBranch = runSync(`git branch -r | grep -w "origin/${curBranchName}"`); 41 | 42 | await runAsync(`${GIT_ADD} .`, spinner, true); 43 | await runAsync(`${GIT_COMMIT} -m "${commitMsg}"`, spinner, true); 44 | if (!isExistCurBranch) { 45 | await runAsync(`git push --set-upstream origin ${curBranchName}`, spinner, true); 46 | } else { 47 | await runAsync(`${GIT_PUSH}`, spinner, true); 48 | } 49 | 50 | spinner.succeed(taskPre('已推送代码至git仓库', 'end')); 51 | return true; 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 部署函数,提供有如下功能 3 | * 版本更新 4 | * 推送至 git 仓库 5 | * 组件库打包 6 | * 发布至 npm 7 | * 生成 CHANGELOG 8 | * 打 tag 并推送至 git 9 | */ 10 | import { _updateVersion, _selectNextVersion } from './selectNextVersion'; 11 | import { _gitPush } from './gitPush'; 12 | import { getOldLog, _setChangelog } from './setChangelog'; 13 | import { _build } from './build'; 14 | import { _publishNpm } from './publishNpm'; 15 | import { _addTag } from './addTag'; 16 | import { _eslint } from './eslint'; 17 | import { compose, getOriginPackageJson, basicCatchError, getProjectPath } from './config/functions'; 18 | import { _stylelint } from './stylelint'; 19 | 20 | const getNextVersion = async (next) => { 21 | const nextVersion = await _selectNextVersion(); 22 | const originPackageJson = getOriginPackageJson(); 23 | next({ 24 | nextVersion, 25 | originVersion: originPackageJson.version, 26 | originPackageJson, 27 | }); 28 | }; 29 | 30 | const updateVersion = async (next, otherOptions) => { 31 | if (!otherOptions?.nextVersion) { 32 | throw new Error('请传入package.json新版本号'); 33 | } 34 | const backVersionFn = await _updateVersion(otherOptions.nextVersion, otherOptions.originPackageJson).catch( 35 | basicCatchError, 36 | ); 37 | next({ backVersionFn }); 38 | }; 39 | 40 | const gitPush = async (next, otherOptions) => { 41 | const pushResult = await _gitPush().catch(basicCatchError); 42 | if (!pushResult) { 43 | return otherOptions?.backVersionFn?.(); 44 | } 45 | next(); 46 | }; 47 | 48 | const setChangelog = async (next, otherOptions) => { 49 | const backChangelog = getOldLog(); 50 | const setLogResult = await _setChangelog().catch(basicCatchError); 51 | if (!setLogResult) { 52 | backChangelog(); 53 | return otherOptions?.backVersionFn(); 54 | } 55 | next({ backChangelog }); 56 | }; 57 | 58 | const build = async (next, otherOptions) => { 59 | const buildResult = await _build().catch(basicCatchError); 60 | if (!buildResult) { 61 | return otherOptions?.backVersionFn(); 62 | } 63 | next(); 64 | }; 65 | 66 | const eslint = function eslint(srcDir = getProjectPath('./src/'), configDir = getProjectPath('./.eslintrc.js')) { 67 | return async (next, otherOptions) => { 68 | const buildResult = await _eslint(srcDir, configDir).catch(basicCatchError); 69 | if (!buildResult) { 70 | return otherOptions?.backVersionFn(); 71 | } 72 | next(); 73 | }; 74 | }; 75 | 76 | const stylelint = function stylelint(srcDir = getProjectPath('./src/'), configDir = getProjectPath('./.stylelint.js')) { 77 | return async (next, otherOptions) => { 78 | const buildResult = await _stylelint(srcDir, configDir).catch(basicCatchError); 79 | if (!buildResult) { 80 | return otherOptions?.backVersionFn(); 81 | } 82 | next(); 83 | }; 84 | }; 85 | 86 | const publishNpm = async (next) => { 87 | const publishResult = await _publishNpm().catch(basicCatchError); 88 | if (!publishResult) { 89 | return; 90 | } 91 | next(); 92 | }; 93 | 94 | const addTag = async (next, otherOptions) => { 95 | const addTagResult = await _addTag(otherOptions?.nextVersion).catch(basicCatchError); 96 | if (!addTagResult) { 97 | return; 98 | } 99 | next(); 100 | }; 101 | 102 | const middle = [getNextVersion, updateVersion, gitPush, setChangelog, build, publishNpm, addTag]; 103 | 104 | async function defaultMain() { 105 | compose(middle); 106 | } 107 | 108 | export { 109 | getNextVersion, 110 | gitPush, 111 | setChangelog, 112 | build, 113 | publishNpm, 114 | addTag, 115 | updateVersion, 116 | compose, 117 | eslint, 118 | stylelint, 119 | getProjectPath, 120 | }; 121 | export default defaultMain; 122 | -------------------------------------------------------------------------------- /src/publishNpm/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLogger, taskPre, runAsync } from '../config/functions'; 2 | 3 | /** 4 | * 发布至npm 5 | */ 6 | export async function _publishNpm() { 7 | const spinner = new DefaultLogger(taskPre('发布', 'start')); 8 | await runAsync('npm publish --access public', spinner, true); 9 | spinner.succeed(taskPre('发布完成', 'end')); 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /src/selectNextVersion/index.ts: -------------------------------------------------------------------------------- 1 | import semverInc from 'semver/functions/inc'; 2 | import { ReleaseType } from 'semver'; 3 | import inquirer from 'inquirer'; 4 | import fs from 'fs'; 5 | import { writeFile } from 'fs/promises'; 6 | import { getOriginPackageJson, getProjectPath, taskPre, DefaultLogger } from '../config/functions'; 7 | 8 | const currentVersion = getOriginPackageJson()?.version; 9 | /** 10 | * 列出所有下一个版本的列表 11 | * @return {*} {({ [key in ReleaseType]: string | null })} 12 | */ 13 | // eslint-disable-next-line no-unused-vars 14 | const getNextVersions = (): { [key in ReleaseType]: string | null } => { 15 | return { 16 | major: semverInc(currentVersion, 'major'), 17 | minor: semverInc(currentVersion, 'minor'), 18 | patch: semverInc(currentVersion, 'patch'), 19 | premajor: semverInc(currentVersion, 'premajor'), 20 | preminor: semverInc(currentVersion, 'preminor'), 21 | prepatch: semverInc(currentVersion, 'prepatch'), 22 | prerelease: semverInc(currentVersion, 'prerelease'), 23 | }; 24 | }; 25 | 26 | /** 27 | * 交互式选择下一个版本号 28 | * @export prompt 29 | * @return {*} {Promise} 30 | */ 31 | export async function _selectNextVersion(): Promise { 32 | const nextVersions = getNextVersions(); 33 | const { nextVersion } = await inquirer.prompt([ 34 | { 35 | type: 'list', 36 | name: 'nextVersion', 37 | message: `请选择将要发布的版本 (当前版本 ${currentVersion})`, 38 | choices: (Object.keys(nextVersions) as Array).map((level) => ({ 39 | name: `${level} => ${nextVersions[level]}`, 40 | value: nextVersions[level], 41 | })), 42 | }, 43 | ]); 44 | return nextVersion; 45 | } 46 | 47 | /** 48 | * 更新版本号 49 | * @param nextVersion 新版本号 50 | */ 51 | export async function _updateVersion(nextVersion: string, originPackageJson) { 52 | const spinner = new DefaultLogger(taskPre('开始修改package.json版本号', 'start')); 53 | await writeFile( 54 | getProjectPath('package.json'), 55 | JSON.stringify({ ...originPackageJson, version: nextVersion }, null, 2), 56 | ); 57 | spinner.succeed(taskPre('已经完成修改package.json版本号', 'end')); 58 | return async () => { 59 | fs.writeFileSync(getProjectPath('package.json'), JSON.stringify(originPackageJson, null, 2)); 60 | console.log('There was an error and version is being rolled back.(流程出现错误,正在回退版本)'); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/setChangelog/index.ts: -------------------------------------------------------------------------------- 1 | import fs, { existsSync } from 'fs'; 2 | import { taskPre, getProjectPath, DefaultLogger, runAsync } from '../config/functions'; 3 | import { CHANGELOG_NAME } from '../config/constans'; 4 | 5 | export const getOldLog = () => { 6 | const logPath = getProjectPath(CHANGELOG_NAME); 7 | if (!existsSync(logPath)) { 8 | fs.writeFileSync(logPath, ''); 9 | } 10 | const oldFile = fs.readFileSync(logPath); 11 | return () => { 12 | fs.writeFileSync(logPath, oldFile); 13 | }; 14 | }; 15 | 16 | /** 17 | * 生成CHANGELOG 18 | */ 19 | export async function _setChangelog() { 20 | const spinner = new DefaultLogger(taskPre('生成CHANGELOG.md', 'start')); 21 | await runAsync(`conventional-changelog -p angular -i ${CHANGELOG_NAME} -s`); 22 | spinner.succeed(taskPre('生成CHANGELOG.md', 'end')); 23 | return true; 24 | } 25 | -------------------------------------------------------------------------------- /src/stylelint/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLogger, taskPre, runAsync } from '../config/functions'; 2 | 3 | /** 4 | * eslint校验 5 | */ 6 | export async function _stylelint(srcDir, configDir) { 7 | const spinner = new DefaultLogger(taskPre('stylelint校验中...', 'start')); 8 | await runAsync( 9 | `stylelint '${srcDir}**/*.less' '${srcDir}**/*.css' --fix --cache --config ${configDir}`, 10 | spinner, 11 | true, 12 | ); 13 | spinner.succeed(taskPre('stylelint检测通过', 'end')); 14 | return true; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ES2019", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "sourceMap": true, 12 | "allowSyntheticDefaultImports": true, 13 | "skipLibCheck": true, 14 | "types": ["webpack", "node", "jest"] 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "lib", "es", "**/__tests__/**", "tests"] 18 | } 19 | --------------------------------------------------------------------------------