ylyx-cli 1.0.14 → 1.0.16

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 (286) hide show
  1. package/README.md +4 -0
  2. package/bin/ylyx.js +168 -168
  3. package/lib/config.js +283 -299
  4. package/lib/deploy.js +529 -415
  5. package/lib/generator.js +192 -192
  6. package/lib/index.js +113 -113
  7. package/lib/interactive.js +87 -87
  8. package/lib/remote.js +179 -179
  9. package/lib/template.js +174 -174
  10. package/lib/utils.js +148 -148
  11. package/package.json +5 -5
  12. package/templates/react-app/files/README.md +23 -23
  13. package/templates/react-app/files/package.json +28 -28
  14. package/templates/react-app/files/public/index.html +11 -11
  15. package/templates/react-app/files/src/App.css +15 -15
  16. package/templates/react-app/files/src/App.js +15 -15
  17. package/templates/react-app/files/src/index.css +10 -10
  18. package/templates/react-app/files/src/index.js +11 -11
  19. package/templates/react-app/template.json +11 -11
  20. package/templates/react-component/files/Component.module.css +5 -5
  21. package/templates/react-component/files/Component.tsx +31 -31
  22. package/templates/react-component/files/index.ts +2 -2
  23. package/templates/react-component/template.json +48 -48
  24. package/templates/vue-app/files/.editorconfig +14 -14
  25. package/templates/vue-app/files/.env.development +21 -21
  26. package/templates/vue-app/files/.env.production +18 -18
  27. package/templates/vue-app/files/.eslintignore +5 -5
  28. package/templates/vue-app/files/.eslintrc.js +267 -267
  29. package/templates/vue-app/files/.prettierignore +11 -11
  30. package/templates/vue-app/files/.travis.yml +5 -5
  31. package/templates/vue-app/files/.vscode/extensions.json +7 -7
  32. package/templates/vue-app/files/.vscode/settings.json +39 -39
  33. package/templates/vue-app/files/LICENSE +191 -191
  34. package/templates/vue-app/files/README.md +87 -87
  35. package/templates/vue-app/files/babel.config.js +11 -11
  36. package/templates/vue-app/files/jest.config.js +24 -24
  37. package/templates/vue-app/files/package.json +136 -136
  38. package/templates/vue-app/files/plopfile.js +7 -7
  39. package/templates/vue-app/files/postcss.config.js +5 -5
  40. package/templates/vue-app/files/public/default.js +25 -25
  41. package/templates/vue-app/files/public/index.html +26 -26
  42. package/templates/vue-app/files/src/App.vue +54 -54
  43. package/templates/vue-app/files/src/api/bigDataV/index.js +198 -198
  44. package/templates/vue-app/files/src/api/data.js +42 -42
  45. package/templates/vue-app/files/src/api/dataArchive/index.js +186 -186
  46. package/templates/vue-app/files/src/api/generator/genConfig.js +16 -16
  47. package/templates/vue-app/files/src/api/generator/generator.js +32 -32
  48. package/templates/vue-app/files/src/api/indexManagement/index.js +39 -39
  49. package/templates/vue-app/files/src/api/institutionMonitor/index.js +396 -396
  50. package/templates/vue-app/files/src/api/institutionSet/index.js +120 -120
  51. package/templates/vue-app/files/src/api/login.js +44 -44
  52. package/templates/vue-app/files/src/api/selfStatistics/index.js +50 -50
  53. package/templates/vue-app/files/src/api/system/code.js +15 -15
  54. package/templates/vue-app/files/src/api/system/dict.js +34 -34
  55. package/templates/vue-app/files/src/api/system/dictDetail.js +52 -52
  56. package/templates/vue-app/files/src/api/system/equipment.js +35 -35
  57. package/templates/vue-app/files/src/api/system/examManage.js +49 -49
  58. package/templates/vue-app/files/src/api/system/examProject.js +48 -48
  59. package/templates/vue-app/files/src/api/system/hosModel.js +35 -35
  60. package/templates/vue-app/files/src/api/system/hospital.js +88 -88
  61. package/templates/vue-app/files/src/api/system/job.js +40 -40
  62. package/templates/vue-app/files/src/api/system/menu.js +67 -67
  63. package/templates/vue-app/files/src/api/system/model.js +42 -42
  64. package/templates/vue-app/files/src/api/system/risConfig.js +9 -9
  65. package/templates/vue-app/files/src/api/system/role.js +64 -64
  66. package/templates/vue-app/files/src/api/system/sysSet.js +19 -19
  67. package/templates/vue-app/files/src/api/system/timing.js +41 -41
  68. package/templates/vue-app/files/src/api/system/user.js +121 -121
  69. package/templates/vue-app/files/src/api/tools/alipay.js +25 -25
  70. package/templates/vue-app/files/src/api/tools/email.js +24 -24
  71. package/templates/vue-app/files/src/api/tools/localStorage.js +27 -27
  72. package/templates/vue-app/files/src/api/tools/qiniu.js +40 -40
  73. package/templates/vue-app/files/src/assets/css/font-awesome.css +2342 -2342
  74. package/templates/vue-app/files/src/assets/css/font-awesome.min.css +2336 -2336
  75. package/templates/vue-app/files/src/assets/fonts/fontawesome-webfont.svg +2671 -2671
  76. package/templates/vue-app/files/src/assets/icons/index.js +9 -9
  77. package/templates/vue-app/files/src/assets/icons/svg/hospital.svg +1 -1
  78. package/templates/vue-app/files/src/assets/icons/svg/hospital1.svg +1 -1
  79. package/templates/vue-app/files/src/assets/icons/svg/loginOne.svg +35 -35
  80. package/templates/vue-app/files/src/assets/icons/svg/loginOneWhite.svg +19 -19
  81. package/templates/vue-app/files/src/assets/icons/svg/loginOr.svg +93 -93
  82. package/templates/vue-app/files/src/assets/icons/svg/loginUp.svg +96 -96
  83. package/templates/vue-app/files/src/assets/icons/svg/monitor.svg +1 -1
  84. package/templates/vue-app/files/src/assets/icons/svg/offline.svg +1 -1
  85. package/templates/vue-app/files/src/assets/icons/svg/online.svg +1 -1
  86. package/templates/vue-app/files/src/assets/icons/svg/system.svg +1 -1
  87. package/templates/vue-app/files/src/assets/icons/svgo.yml +21 -21
  88. package/templates/vue-app/files/src/assets/styles/btn.scss +103 -103
  89. package/templates/vue-app/files/src/assets/styles/eladmin.scss +124 -124
  90. package/templates/vue-app/files/src/assets/styles/element-ui.scss +217 -217
  91. package/templates/vue-app/files/src/assets/styles/element-variables.scss +31 -31
  92. package/templates/vue-app/files/src/assets/styles/global.scss +73 -73
  93. package/templates/vue-app/files/src/assets/styles/index.scss +220 -220
  94. package/templates/vue-app/files/src/assets/styles/mixin.scss +60 -60
  95. package/templates/vue-app/files/src/assets/styles/publicStyle.scss +224 -224
  96. package/templates/vue-app/files/src/assets/styles/sidebar.scss +239 -239
  97. package/templates/vue-app/files/src/assets/styles/transition.scss +48 -48
  98. package/templates/vue-app/files/src/assets/styles/variables.scss +44 -44
  99. package/templates/vue-app/files/src/common/flexible.js +145 -145
  100. package/templates/vue-app/files/src/components/Breadcrumb/index.vue +87 -87
  101. package/templates/vue-app/files/src/components/Crud/CRUD.operation.vue +248 -248
  102. package/templates/vue-app/files/src/components/Crud/Pagination.vue +20 -20
  103. package/templates/vue-app/files/src/components/Crud/RR.operation.vue +39 -39
  104. package/templates/vue-app/files/src/components/Crud/UD.operation.vue +106 -106
  105. package/templates/vue-app/files/src/components/Crud/crud.js +907 -907
  106. package/templates/vue-app/files/src/components/Crud/user/UR.operation.vue +154 -154
  107. package/templates/vue-app/files/src/components/DateRangePicker/index.vue +42 -42
  108. package/templates/vue-app/files/src/components/DateRangePickerProhibitNext/index.vue +42 -42
  109. package/templates/vue-app/files/src/components/DateRangePickerProhibitTodayNext/index.vue +48 -48
  110. package/templates/vue-app/files/src/components/Dict/Dict.js +31 -31
  111. package/templates/vue-app/files/src/components/Dict/index.js +29 -29
  112. package/templates/vue-app/files/src/components/Doc/index.vue +16 -16
  113. package/templates/vue-app/files/src/components/Echarts/BarChart.vue +115 -115
  114. package/templates/vue-app/files/src/components/Echarts/Category.vue +768 -768
  115. package/templates/vue-app/files/src/components/Echarts/Funnel.vue +120 -120
  116. package/templates/vue-app/files/src/components/Echarts/Gauge.vue +74 -74
  117. package/templates/vue-app/files/src/components/Echarts/Graph.vue +101 -101
  118. package/templates/vue-app/files/src/components/Echarts/HeatMap.vue +301 -301
  119. package/templates/vue-app/files/src/components/Echarts/Line3D.vue +110 -110
  120. package/templates/vue-app/files/src/components/Echarts/PieChart.vue +84 -84
  121. package/templates/vue-app/files/src/components/Echarts/Point.vue +161 -161
  122. package/templates/vue-app/files/src/components/Echarts/RadarChart.vue +123 -123
  123. package/templates/vue-app/files/src/components/Echarts/Rich.vue +160 -160
  124. package/templates/vue-app/files/src/components/Echarts/Sankey.vue +114 -114
  125. package/templates/vue-app/files/src/components/Echarts/Scatter.vue +192 -192
  126. package/templates/vue-app/files/src/components/Echarts/Sunburst.vue +127 -127
  127. package/templates/vue-app/files/src/components/Echarts/ThemeRiver.vue +233 -233
  128. package/templates/vue-app/files/src/components/Echarts/WordCloud.vue +201 -201
  129. package/templates/vue-app/files/src/components/GithubCorner/index.vue +55 -55
  130. package/templates/vue-app/files/src/components/Hamburger/index.vue +44 -44
  131. package/templates/vue-app/files/src/components/HeaderSearch/index.vue +196 -196
  132. package/templates/vue-app/files/src/components/IconSelect/index.vue +74 -74
  133. package/templates/vue-app/files/src/components/IconSelect/requireIcons.js +10 -10
  134. package/templates/vue-app/files/src/components/Iframe/index.vue +30 -30
  135. package/templates/vue-app/files/src/components/JavaEdit/index.vue +78 -78
  136. package/templates/vue-app/files/src/components/Pagination/index.vue +100 -100
  137. package/templates/vue-app/files/src/components/PanThumb/index.vue +149 -149
  138. package/templates/vue-app/files/src/components/ParentView/index.vue +3 -3
  139. package/templates/vue-app/files/src/components/Permission/index.js +13 -13
  140. package/templates/vue-app/files/src/components/Permission/permission.js +21 -21
  141. package/templates/vue-app/files/src/components/RightPanel/index.vue +149 -149
  142. package/templates/vue-app/files/src/components/Screenfull/index.vue +60 -60
  143. package/templates/vue-app/files/src/components/SizeSelect/index.vue +60 -60
  144. package/templates/vue-app/files/src/components/SvgIcon/index.vue +66 -66
  145. package/templates/vue-app/files/src/components/ThemePicker/index.vue +177 -177
  146. package/templates/vue-app/files/src/components/UploadExcel/index.vue +149 -149
  147. package/templates/vue-app/files/src/components/WaterMark/setWaterMark.js +68 -68
  148. package/templates/vue-app/files/src/components/WaterMark/waterMark.vue +69 -69
  149. package/templates/vue-app/files/src/components/Xgplayer/index.vue +49 -49
  150. package/templates/vue-app/files/src/components/YamlEdit/index.vue +81 -81
  151. package/templates/vue-app/files/src/layout/components/AppMain.vue +67 -67
  152. package/templates/vue-app/files/src/layout/components/Navbar.vue +209 -209
  153. package/templates/vue-app/files/src/layout/components/Settings/index.vue +125 -125
  154. package/templates/vue-app/files/src/layout/components/Sidebar/FixiOSBug.js +26 -26
  155. package/templates/vue-app/files/src/layout/components/Sidebar/Item.vue +29 -29
  156. package/templates/vue-app/files/src/layout/components/Sidebar/Link.vue +35 -35
  157. package/templates/vue-app/files/src/layout/components/Sidebar/Logo.vue +93 -93
  158. package/templates/vue-app/files/src/layout/components/Sidebar/SidebarItem.vue +104 -104
  159. package/templates/vue-app/files/src/layout/components/Sidebar/index.vue +79 -79
  160. package/templates/vue-app/files/src/layout/components/TagsView/ScrollPane.vue +90 -90
  161. package/templates/vue-app/files/src/layout/components/TagsView/index.vue +299 -299
  162. package/templates/vue-app/files/src/layout/components/index.js +5 -5
  163. package/templates/vue-app/files/src/layout/index.vue +119 -119
  164. package/templates/vue-app/files/src/layout/mixin/ResizeHandler.js +45 -45
  165. package/templates/vue-app/files/src/main.js +89 -89
  166. package/templates/vue-app/files/src/mixins/crud.js +348 -348
  167. package/templates/vue-app/files/src/router/index.js +156 -156
  168. package/templates/vue-app/files/src/router/routers.js +203 -203
  169. package/templates/vue-app/files/src/settings.js +57 -57
  170. package/templates/vue-app/files/src/store/getters.js +29 -29
  171. package/templates/vue-app/files/src/store/index.js +25 -25
  172. package/templates/vue-app/files/src/store/modules/api.js +33 -33
  173. package/templates/vue-app/files/src/store/modules/app.js +55 -55
  174. package/templates/vue-app/files/src/store/modules/permission.js +79 -79
  175. package/templates/vue-app/files/src/store/modules/register.js +17 -17
  176. package/templates/vue-app/files/src/store/modules/settings.js +46 -46
  177. package/templates/vue-app/files/src/store/modules/tagsView.js +168 -168
  178. package/templates/vue-app/files/src/store/modules/user.js +119 -119
  179. package/templates/vue-app/files/src/utils/auth.js +18 -18
  180. package/templates/vue-app/files/src/utils/base64ToFile.js +27 -27
  181. package/templates/vue-app/files/src/utils/clipboard.js +36 -36
  182. package/templates/vue-app/files/src/utils/datetime.js +289 -289
  183. package/templates/vue-app/files/src/utils/getBlobFile.js +10 -10
  184. package/templates/vue-app/files/src/utils/getTime.js +29 -29
  185. package/templates/vue-app/files/src/utils/index.js +520 -520
  186. package/templates/vue-app/files/src/utils/permission.js +29 -29
  187. package/templates/vue-app/files/src/utils/pinyin.js +450 -450
  188. package/templates/vue-app/files/src/utils/request.js +139 -139
  189. package/templates/vue-app/files/src/utils/rsaEncrypt.js +37 -37
  190. package/templates/vue-app/files/src/utils/shortcuts.js +210 -210
  191. package/templates/vue-app/files/src/utils/upload.js +12 -12
  192. package/templates/vue-app/files/src/utils/validate.js +262 -262
  193. package/templates/vue-app/files/src/views/components/Echarts.vue +124 -124
  194. package/templates/vue-app/files/src/views/components/Editor.vue +75 -75
  195. package/templates/vue-app/files/src/views/components/MarkDown.vue +44 -44
  196. package/templates/vue-app/files/src/views/components/UVideo.vue +37 -37
  197. package/templates/vue-app/files/src/views/components/YamlEdit.vue +206 -206
  198. package/templates/vue-app/files/src/views/components/camera/camera.vue +115 -115
  199. package/templates/vue-app/files/src/views/components/camera/cameraTest.vue +158 -158
  200. package/templates/vue-app/files/src/views/components/excel/upload-excel.vue +41 -41
  201. package/templates/vue-app/files/src/views/components/icons/element-icons.js +74 -74
  202. package/templates/vue-app/files/src/views/components/icons/index.vue +106 -106
  203. package/templates/vue-app/files/src/views/components/icons/svg-icons.js +10 -10
  204. package/templates/vue-app/files/src/views/components/scroll/index.vue +107 -107
  205. package/templates/vue-app/files/src/views/components/scrollFull/index.vue +99 -99
  206. package/templates/vue-app/files/src/views/dashboard/LineChart.vue +138 -138
  207. package/templates/vue-app/files/src/views/dashboard/PanelGroup.vue +173 -173
  208. package/templates/vue-app/files/src/views/dashboard/mixins/resize.js +57 -57
  209. package/templates/vue-app/files/src/views/dataArchive/delTable.vue +354 -354
  210. package/templates/vue-app/files/src/views/dataArchive/exportDetail.vue +121 -121
  211. package/templates/vue-app/files/src/views/dataArchive/exportList.vue +160 -160
  212. package/templates/vue-app/files/src/views/dataArchive/index.vue +2104 -2104
  213. package/templates/vue-app/files/src/views/features/401.vue +83 -83
  214. package/templates/vue-app/files/src/views/features/404.vue +226 -226
  215. package/templates/vue-app/files/src/views/features/redirect.vue +12 -12
  216. package/templates/vue-app/files/src/views/generator/config.vue +348 -348
  217. package/templates/vue-app/files/src/views/generator/index.vue +143 -143
  218. package/templates/vue-app/files/src/views/generator/preview.vue +39 -39
  219. package/templates/vue-app/files/src/views/home.vue +80 -80
  220. package/templates/vue-app/files/src/views/indexManagement/index.vue +422 -422
  221. package/templates/vue-app/files/src/views/indexManagement/other.vue +286 -286
  222. package/templates/vue-app/files/src/views/institutionMonitor/components/LineChart.vue +191 -191
  223. package/templates/vue-app/files/src/views/institutionMonitor/index.scss +339 -339
  224. package/templates/vue-app/files/src/views/institutionMonitor/index.vue +1385 -1385
  225. package/templates/vue-app/files/src/views/institutionMonitor/mixins/resize.js +57 -57
  226. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/checkProjectCode.vue +267 -267
  227. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/imageAbnormalMonitor.vue +361 -361
  228. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/index.vue +686 -686
  229. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/leakageAnomaly.vue +270 -270
  230. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/moveMonitore.vue +114 -114
  231. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/qualityMonitore.vue +338 -338
  232. package/templates/vue-app/files/src/views/institutionSet/cloudSet.vue +534 -534
  233. package/templates/vue-app/files/src/views/institutionSet/index.vue +2550 -2550
  234. package/templates/vue-app/files/src/views/login.vue +243 -243
  235. package/templates/vue-app/files/src/views/qualityLogin.vue +280 -280
  236. package/templates/vue-app/files/src/views/selfStatistics/components/LineChart.vue +296 -296
  237. package/templates/vue-app/files/src/views/selfStatistics/hospitalDetail/index.vue +533 -533
  238. package/templates/vue-app/files/src/views/selfStatistics/index.scss +447 -447
  239. package/templates/vue-app/files/src/views/selfStatistics/index.vue +903 -903
  240. package/templates/vue-app/files/src/views/selfStatistics/mixins/resize.js +57 -57
  241. package/templates/vue-app/files/src/views/selfStatistics/monitorDetail/index.vue +285 -285
  242. package/templates/vue-app/files/src/views/selfStatistics/offlineDetail/index.vue +104 -104
  243. package/templates/vue-app/files/src/views/selfStatistics/selfActiveDetail/index.vue +104 -104
  244. package/templates/vue-app/files/src/views/selfStatistics/selfMonitorDetail/index.vue +420 -420
  245. package/templates/vue-app/files/src/views/selfStatistics/setWaterMark/setWaterMark.js +68 -68
  246. package/templates/vue-app/files/src/views/selfStatistics/setWaterMark/waterMark.vue +69 -69
  247. package/templates/vue-app/files/src/views/system/dept/index.vue +144 -144
  248. package/templates/vue-app/files/src/views/system/dict/dictDetail.vue +146 -146
  249. package/templates/vue-app/files/src/views/system/dict/index.vue +159 -159
  250. package/templates/vue-app/files/src/views/system/examModality/index.vue +174 -174
  251. package/templates/vue-app/files/src/views/system/examPart/index.vue +184 -184
  252. package/templates/vue-app/files/src/views/system/examProject/index.vue +321 -321
  253. package/templates/vue-app/files/src/views/system/examine/index.vue +66 -66
  254. package/templates/vue-app/files/src/views/system/job/index.vue +119 -119
  255. package/templates/vue-app/files/src/views/system/job/module/form.vue +72 -72
  256. package/templates/vue-app/files/src/views/system/job/module/header.vue +48 -48
  257. package/templates/vue-app/files/src/views/system/menu/index.vue +327 -327
  258. package/templates/vue-app/files/src/views/system/model/index.vue +285 -285
  259. package/templates/vue-app/files/src/views/system/role/index.vue +434 -434
  260. package/templates/vue-app/files/src/views/system/sysSet/index.vue +84 -84
  261. package/templates/vue-app/files/src/views/system/timing/index.vue +289 -289
  262. package/templates/vue-app/files/src/views/system/timing/log.vue +143 -143
  263. package/templates/vue-app/files/src/views/system/user/admin.vue +414 -414
  264. package/templates/vue-app/files/src/views/system/user/center/updateEmail.vue +146 -146
  265. package/templates/vue-app/files/src/views/system/user/center/updatePass.vue +168 -168
  266. package/templates/vue-app/files/src/views/system/user/center.vue +140 -140
  267. package/templates/vue-app/files/src/views/system/user/index.vue +972 -972
  268. package/templates/vue-app/files/src/views/systemSet/role/index.vue +314 -314
  269. package/templates/vue-app/files/src/views/systemSet/user/admin.vue +414 -414
  270. package/templates/vue-app/files/src/views/systemSet/user/center/updateEmail.vue +146 -146
  271. package/templates/vue-app/files/src/views/systemSet/user/center/updatePass.vue +115 -115
  272. package/templates/vue-app/files/src/views/systemSet/user/center.vue +351 -351
  273. package/templates/vue-app/files/src/views/systemSet/user/index.vue +928 -928
  274. package/templates/vue-app/files/src/views/tools/aliPay/config.vue +102 -102
  275. package/templates/vue-app/files/src/views/tools/aliPay/index.vue +48 -48
  276. package/templates/vue-app/files/src/views/tools/aliPay/toPay.vue +91 -91
  277. package/templates/vue-app/files/src/views/tools/email/config.vue +91 -91
  278. package/templates/vue-app/files/src/views/tools/email/index.vue +42 -42
  279. package/templates/vue-app/files/src/views/tools/email/send.vue +159 -159
  280. package/templates/vue-app/files/src/views/tools/storage/index.vue +35 -35
  281. package/templates/vue-app/files/src/views/tools/storage/local/index.vue +207 -207
  282. package/templates/vue-app/files/src/views/tools/storage/qiniu/form.vue +108 -108
  283. package/templates/vue-app/files/src/views/tools/storage/qiniu/index.vue +241 -241
  284. package/templates/vue-app/files/src/views/tools/swagger/index.vue +14 -14
  285. package/templates/vue-app/files/vue.config.js +173 -173
  286. package/templates/vue-app/template.json +29 -29
package/lib/deploy.js CHANGED
@@ -1,415 +1,529 @@
1
- const path = require('path');
2
- const fs = require('fs-extra');
3
- const ora = require('ora');
4
- const inquirer = require('inquirer');
5
- const archiver = require('archiver');
6
- const { readProjectConfig } = require('./utils');
7
-
8
- let spinner = null; // 加载实例
9
-
10
- function formatTimestampCompact(date = new Date()) {
11
- const pad2 = (n) => String(n).padStart(2, '0');
12
- const yyyy = date.getFullYear();
13
- const MM = pad2(date.getMonth() + 1);
14
- const dd = pad2(date.getDate());
15
- const HH = pad2(date.getHours());
16
- const mm = pad2(date.getMinutes());
17
- const ss = pad2(date.getSeconds());
18
- return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
19
- }
20
-
21
- function formatBytes(bytes) {
22
- const n = Number(bytes) || 0;
23
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
24
- let v = n;
25
- let i = 0;
26
- while (v >= 1024 && i < units.length - 1) {
27
- v /= 1024;
28
- i++;
29
- }
30
- const fixed = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
31
- return `${v.toFixed(fixed)}${units[i]}`;
32
- }
33
-
34
- function listLocalFilesWithSize(rootDir) {
35
- const out = [];
36
- const stack = [rootDir];
37
- while (stack.length) {
38
- const current = stack.pop();
39
- const entries = fs.readdirSync(current);
40
- for (const e of entries) {
41
- const p = path.join(current, e);
42
- const st = fs.statSync(p);
43
- if (st.isDirectory()) stack.push(p);
44
- else if (st.isFile()) {
45
- out.push({
46
- localPath: p,
47
- relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
48
- size: st.size,
49
- mtimeMs: st.mtimeMs,
50
- });
51
- }
52
- }
53
- }
54
- out.sort((a, b) => a.relPosix.localeCompare(b.relPosix));
55
- return out;
56
- }
57
-
58
- function sanitizeBuildDirName(name) {
59
- // 用户要求:项目名称来源于 buildDir 字段;同时确保文件名安全
60
- return String(name || 'build')
61
- .replace(/[\\/]/g, '')
62
- .replace(/[^a-zA-Z0-9._-]/g, '')
63
- .slice(0, 80);
64
- }
65
-
66
- function getBuildDirBaseName({ buildDir, localDir }) {
67
- // buildDir 可能是路径:只取最后一级目录名
68
- if (buildDir != null && String(buildDir).trim()) {
69
- const normalized = String(buildDir).trim().replace(/\\/g, '/').replace(/\/+$/, '');
70
- const last = normalized.split('/').filter(Boolean).pop();
71
- if (last) return sanitizeBuildDirName(last);
72
- }
73
- // 兜底:使用 localDir 的最后一段目录名
74
- return sanitizeBuildDirName(path.basename(path.resolve(localDir || process.cwd())));
75
- }
76
-
77
- async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
78
- fs.ensureDirSync(path.dirname(zipPath));
79
- await new Promise((resolve, reject) => {
80
- const output = fs.createWriteStream(zipPath);
81
- const archive = archiver('zip', { zlib: { level: 9 } });
82
-
83
- output.on('close', resolve);
84
- output.on('error', reject);
85
- archive.on('warning', (err) => {
86
- if (err && err.code === 'ENOENT') return;
87
- reject(err);
88
- });
89
- archive.on('error', reject);
90
-
91
- archive.pipe(output);
92
- // 关键:zip 内顶层目录名强制为 buildDirName
93
- archive.directory(srcDir, topFolderName);
94
- archive.finalize();
95
- });
96
- }
97
-
98
- function hashLettersFromSha256(buf, lettersLen = 12) {
99
- const alphabet = 'abcdefghijklmnop'; // 16 letters for 0..15
100
- let out = '';
101
- for (let i = 0; i < buf.length && out.length < lettersLen; i++) {
102
- const b = buf[i];
103
- out += alphabet[(b >> 4) & 0xf];
104
- if (out.length >= lettersLen) break;
105
- out += alphabet[b & 0xf];
106
- }
107
- return out.slice(0, lettersLen);
108
- }
109
-
110
- function computeDirHashLetters(localDir) {
111
- const crypto = require('crypto');
112
- const h = crypto.createHash('sha256');
113
- const files = listLocalFilesWithSize(localDir);
114
- for (const f of files) {
115
- h.update(f.relPosix);
116
- h.update('\n');
117
- h.update(String(f.size || 0));
118
- h.update('\n');
119
- h.update(String(Math.floor(f.mtimeMs || 0)));
120
- h.update('\n');
121
- }
122
- const digest = h.digest();
123
- return hashLettersFromSha256(digest, 12);
124
- }
125
-
126
- async function chooseDeployEnv(env) {
127
- if (env === 'test' || env === 'prod') return env;
128
- const ans = await inquirer.prompt([
129
- {
130
- type: 'list',
131
- name: 'env',
132
- message: '请选择部署环境',
133
- choices: [
134
- { name: '部署测试服(上传到服务器)', value: 'test' },
135
- { name: '部署正式服(不上传,仅压缩)', value: 'prod' },
136
- ],
137
- default: 'test',
138
- },
139
- ]);
140
- return ans.env;
141
- }
142
-
143
- async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
144
- const files = listLocalFilesWithSize(localDir);
145
- const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
146
- if (files.length === 0) return { totalBytes: 0, uploadedBytes: 0, fileCount: 0 };
147
-
148
- // 先创建需要的目录(去重)
149
- const dirSet = new Set();
150
- for (const f of files) {
151
- const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
152
- dirSet.add(path.posix.dirname(remoteFile));
153
- }
154
- // 目录从浅到深创建,避免多余失败
155
- const dirs = Array.from(dirSet).sort((a, b) => a.length - b.length);
156
- for (const d of dirs) {
157
- await sftp.mkdir(d, true);
158
- }
159
-
160
- let uploadedBytes = 0;
161
- let lastRenderAt = 0;
162
- const render = (state) => {
163
- if (typeof onProgress !== 'function') return;
164
- const now = Date.now();
165
- // 轻微节流,避免刷屏
166
- if (now - lastRenderAt < 80 && state && state.force !== true) return;
167
- lastRenderAt = now;
168
- onProgress(state);
169
- };
170
-
171
- for (let i = 0; i < files.length; i++) {
172
- const f = files[i];
173
- const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
174
- let lastTransferred = 0;
175
-
176
- await sftp.fastPut(f.localPath, remoteFile, {
177
- step: (transferred) => {
178
- // transferred:当前文件已传输的总字节数(累计)
179
- const delta = Math.max(0, Number(transferred || 0) - lastTransferred);
180
- lastTransferred = Number(transferred || 0);
181
- uploadedBytes += delta;
182
- render({
183
- uploadedBytes,
184
- totalBytes,
185
- fileIndex: i + 1,
186
- fileCount: files.length,
187
- currentFile: f.relPosix,
188
- });
189
- },
190
- });
191
-
192
- // fastPut 有时不会触发 step(极小文件/实现差异),这里兜底补齐本文件剩余字节
193
- if (lastTransferred < f.size) {
194
- uploadedBytes += f.size - lastTransferred;
195
- }
196
- render({
197
- uploadedBytes,
198
- totalBytes,
199
- fileIndex: i + 1,
200
- fileCount: files.length,
201
- currentFile: f.relPosix,
202
- force: true,
203
- });
204
- }
205
-
206
- return { totalBytes, uploadedBytes, fileCount: files.length };
207
- }
208
-
209
- function resolveDeployConfig(overrides = {}) {
210
- const projectConfig = readProjectConfig();
211
- const deploy = (projectConfig && projectConfig.deploy) || {};
212
-
213
- function parseDotEnvFile(envPath) {
214
- if (!fs.existsSync(envPath)) return {};
215
- const content = fs.readFileSync(envPath, 'utf-8');
216
- const lines = content.split(/\r?\n/);
217
- const out = {};
218
- for (const raw of lines) {
219
- const line = raw.trim();
220
- if (!line || line.startsWith('#')) continue;
221
- const idx = line.indexOf('=');
222
- if (idx < 0) continue;
223
- const key = line.slice(0, idx).trim();
224
- let value = line.slice(idx + 1).trim();
225
- if (
226
- (value.startsWith('"') && value.endsWith('"')) ||
227
- (value.startsWith("'") && value.endsWith("'"))
228
- ) {
229
- value = value.slice(1, -1);
230
- }
231
- out[key] = value;
232
- }
233
- return out;
234
- }
235
-
236
- function normalizePublicUrlToDir(publicUrl) {
237
- // 与 config.js 规则保持一致:忽略完整 URL;去掉首尾 /;把 \ 转 /
238
- if (!publicUrl || typeof publicUrl !== 'string') return '';
239
- let v = publicUrl.trim();
240
- v = v.replace(/\\/g, '/');
241
- if (/^https?:\/\//i.test(v)) return '';
242
- v = v.replace(/^\/+/, '').replace(/\/+$/, '');
243
- return v;
244
- }
245
-
246
- // localDir 默认优先从项目 .env.production 推导(VUE_APP_PUBLIC_URL -> 目录名)
247
- const envProdPath = path.join(process.cwd(), '.env.production');
248
- const envProd = parseDotEnvFile(envProdPath);
249
- const publicUrlDir = normalizePublicUrlToDir(envProd.VUE_APP_PUBLIC_URL);
250
- const inferredFromEnvProd = publicUrlDir ? path.resolve(process.cwd(), publicUrlDir) : null;
251
-
252
- // localDir 默认使用项目 buildDir(打包输出目录)
253
- // 规则:优先 deploy.localDir;否则使用 .env.production 的 VUE_APP_PUBLIC_URL;否则使用 .ylyxrc.json 的 buildDir;最后才回退 ./EXTERNAL_DIGIC
254
- const inferredLocalDir =
255
- deploy.localDir ||
256
- inferredFromEnvProd ||
257
- (projectConfig && projectConfig.buildDir ? path.resolve(process.cwd(), projectConfig.buildDir) : null);
258
-
259
- if (!inferredLocalDir) {
260
- throw new Error(
261
- '无法确定 deploy.localDir(本地打包目录)。请设置以下任意一种:\n' +
262
- '- .ylyxrc.json 配置 deploy.localDir\n' +
263
- '- 在项目 .env.production 配置 VUE_APP_PUBLIC_URL(相对路径,如 /dist/app/)\n' +
264
- '- .ylyxrc.json 配置 buildDir(可相对/绝对路径)'
265
- );
266
- }
267
-
268
- const cfg = {
269
- env: overrides.env ?? deploy.env,
270
- buildDir: overrides.buildDir ?? deploy.buildDir ?? projectConfig.buildDir,
271
- host: overrides.host ?? deploy.host,
272
- port: String(overrides.port ?? deploy.port ?? '22'),
273
- username: overrides.username ?? deploy.username,
274
- password: overrides.password ?? deploy.password,
275
- localDir: overrides.localDir ?? inferredLocalDir,
276
- remoteDir: overrides.remoteDir ?? deploy.remoteDir,
277
- zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
278
- zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
279
- };
280
-
281
- return cfg;
282
- }
283
-
284
- /**
285
- * 执行部署(递归上传目录到远端)
286
- * 测试服:上传到服务器(可选部署后压缩)
287
- * 正式服:不上传,仅压缩
288
- * @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
289
- */
290
- async function deploy(overrides = {}) {
291
- const cfg = resolveDeployConfig(overrides);
292
- const localFolderPath = cfg.localDir;
293
- const remoteFolderPath = cfg.remoteDir;
294
-
295
- const env = await chooseDeployEnv(cfg.env);
296
-
297
- if (!fs.existsSync(localFolderPath)) {
298
- throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
299
- }
300
-
301
- // 正式服:仅压缩
302
- if (env === 'prod') {
303
- const buildDirName = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
304
- const ts = formatTimestampCompact(new Date());
305
- const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
306
- // 优化命名:更可读、更易分割(buildDir-时间-哈希)
307
- const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
308
- fs.ensureDirSync(cfg.zipOutDir);
309
- const zipName = `${zipBaseName}.zip`;
310
- const zipPath = path.join(cfg.zipOutDir, zipName);
311
-
312
- const zipSpinner = ora(`开始压缩:${zipName}`).start();
313
- try {
314
- await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
315
- zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
316
- } catch (e) {
317
- zipSpinner.fail('压缩失败');
318
- throw e;
319
- }
320
- return;
321
- }
322
-
323
- // 测试服:上传到服务器
324
- if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
325
- if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
326
- if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
327
- if (!remoteFolderPath) {
328
- throw new Error('缺少 deploy.remoteDir(测试服上传需要):请在 .ylyxrc.json 的 deploy.remoteDir 配置远端部署目录');
329
- }
330
-
331
- let Client;
332
- try {
333
- Client = require('ssh2-sftp-client');
334
- } catch (e) {
335
- throw new Error(
336
- "缺少依赖:ssh2-sftp-client。\n" +
337
- "如果你在 ylyx-cli 仓库本地跑:请先在 ylyx-cli 目录执行 `npm i`。\n" +
338
- "如果你用 npx:请确认你运行的是已发布且包含依赖的版本,或清理 npx 缓存后重试。"
339
- );
340
- }
341
-
342
- const sftp = new Client();
343
- try {
344
- spinner = ora('连接服务器...').start();
345
- await sftp.connect({
346
- host: cfg.host,
347
- port: cfg.port,
348
- username: cfg.username,
349
- password: cfg.password,
350
- });
351
-
352
- spinner.text = '服务器连接成功,准备清理远端目录...';
353
- try {
354
- await sftp.rmdir(remoteFolderPath, true);
355
- } catch (e) {
356
- // ignore
357
- }
358
- await sftp.mkdir(remoteFolderPath, true);
359
-
360
- const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
361
- spinner.text = '开始上传目录...';
362
- const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
363
- sftp,
364
- localDir: localFolderPath,
365
- remoteDirPosix,
366
- onProgress: ({ uploadedBytes: up, totalBytes: tot, fileIndex, fileCount: cnt, currentFile }) => {
367
- const percent = tot ? ((up / tot) * 100).toFixed(2) : '100.00';
368
- const shortFile = currentFile ? currentFile.slice(-60) : '';
369
- spinner.text = `上传进度: ${percent}% (${formatBytes(up)}/${formatBytes(tot)}) 文件:${fileIndex}/${cnt} ${shortFile}`;
370
- },
371
- });
372
- spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
373
- } catch (err) {
374
- if (spinner) spinner.fail('部署失败');
375
- throw err;
376
- } finally {
377
- try {
378
- await sftp.end();
379
- } catch (e) {}
380
- if (spinner) spinner.info('部署结束');
381
- }
382
-
383
- // 测试服可选:部署后压缩(延续之前的可选开关)
384
- if (cfg.zipAfter) {
385
- const buildDirName = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
386
- const ts = formatTimestampCompact(new Date());
387
- const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
388
- const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
389
- fs.ensureDirSync(cfg.zipOutDir);
390
- const zipName = `${zipBaseName}.zip`;
391
- const zipPath = path.join(cfg.zipOutDir, zipName);
392
-
393
- const zipSpinner = ora(`开始压缩:${zipName}`).start();
394
- try {
395
- await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
396
- zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
397
- } catch (e) {
398
- zipSpinner.fail('压缩失败');
399
- throw e;
400
- }
401
- }
402
- }
403
-
404
- module.exports = {
405
- deploy,
406
- resolveDeployConfig,
407
- };
408
-
409
- // 允许直接 node lib/deploy.js 运行(从 .ylyxrc.json 读取 deploy 配置)
410
- if (require.main === module) {
411
- deploy().catch((e) => {
412
- console.error(e);
413
- process.exit(1);
414
- });
415
- }
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const ora = require('ora');
4
+ const inquirer = require('inquirer');
5
+ const archiver = require('archiver');
6
+ const { readProjectConfig } = require('./utils');
7
+
8
+ let spinner = null; // 加载实例
9
+
10
+ function formatTimestampCompact(date = new Date()) {
11
+ const pad2 = (n) => String(n).padStart(2, '0');
12
+ const yyyy = date.getFullYear();
13
+ const MM = pad2(date.getMonth() + 1);
14
+ const dd = pad2(date.getDate());
15
+ const HH = pad2(date.getHours());
16
+ const mm = pad2(date.getMinutes());
17
+ const ss = pad2(date.getSeconds());
18
+ return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
19
+ }
20
+
21
+ function formatTimestampForLog(date = new Date()) {
22
+ const pad2 = (n) => String(n).padStart(2, '0');
23
+ const yyyy = date.getFullYear();
24
+ const MM = pad2(date.getMonth() + 1);
25
+ const dd = pad2(date.getDate());
26
+ const HH = pad2(date.getHours());
27
+ const mm = pad2(date.getMinutes());
28
+ const ss = pad2(date.getSeconds());
29
+ return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
30
+ }
31
+
32
+ function formatBytes(bytes) {
33
+ const n = Number(bytes) || 0;
34
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
35
+ let v = n;
36
+ let i = 0;
37
+ while (v >= 1024 && i < units.length - 1) {
38
+ v /= 1024;
39
+ i++;
40
+ }
41
+ const fixed = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
42
+ return `${v.toFixed(fixed)}${units[i]}`;
43
+ }
44
+
45
+ function createDeployLogger({ env, buildDirName, outDir }) {
46
+ const logDir = path.join(outDir || path.join(process.cwd(), '.ylyx-deploy'), 'logs');
47
+ fs.ensureDirSync(logDir);
48
+ const fileName = `deploy-${env || 'unknown'}-${buildDirName || 'build'}-${formatTimestampCompact(
49
+ new Date()
50
+ )}.log`;
51
+ const logPath = path.join(logDir, fileName);
52
+ const stream = fs.createWriteStream(logPath, { flags: 'a' });
53
+
54
+ const log = (msg) => {
55
+ const line = `[${formatTimestampForLog(new Date())}] ${msg}\n`;
56
+ try {
57
+ stream.write(line);
58
+ } catch (e) {
59
+ // ignore
60
+ }
61
+ };
62
+
63
+ // 只记录“操作/目录/时间”的精简日志
64
+ const logOp = (op, target, extra) => {
65
+ const parts = [`op=${op}`];
66
+ if (target) parts.push(`target=${target}`);
67
+ if (extra) parts.push(`extra=${extra}`);
68
+ log(parts.join(' '));
69
+ };
70
+
71
+ const close = () =>
72
+ new Promise((resolve) => {
73
+ try {
74
+ stream.end(resolve);
75
+ } catch (e) {
76
+ resolve();
77
+ }
78
+ });
79
+
80
+ return { logPath, log, logOp, close };
81
+ }
82
+
83
+ function listLocalFilesWithSize(rootDir) {
84
+ const out = [];
85
+ const stack = [rootDir];
86
+ while (stack.length) {
87
+ const current = stack.pop();
88
+ const entries = fs.readdirSync(current);
89
+ for (const e of entries) {
90
+ const p = path.join(current, e);
91
+ const st = fs.statSync(p);
92
+ if (st.isDirectory()) stack.push(p);
93
+ else if (st.isFile()) {
94
+ out.push({
95
+ localPath: p,
96
+ relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
97
+ size: st.size,
98
+ mtimeMs: st.mtimeMs,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ out.sort((a, b) => a.relPosix.localeCompare(b.relPosix));
104
+ return out;
105
+ }
106
+
107
+ function sanitizeBuildDirName(name) {
108
+ // 用户要求:项目名称来源于 buildDir 字段;同时确保文件名安全
109
+ return String(name || 'build')
110
+ .replace(/[\\/]/g, '')
111
+ .replace(/[^a-zA-Z0-9._-]/g, '')
112
+ .slice(0, 80);
113
+ }
114
+
115
+ function getBuildDirBaseName({ buildDir, localDir }) {
116
+ // buildDir 可能是路径:只取最后一级目录名
117
+ if (buildDir != null && String(buildDir).trim()) {
118
+ const normalized = String(buildDir).trim().replace(/\\/g, '/').replace(/\/+$/, '');
119
+ const last = normalized.split('/').filter(Boolean).pop();
120
+ if (last) return sanitizeBuildDirName(last);
121
+ }
122
+ // 兜底:使用 localDir 的最后一段目录名
123
+ return sanitizeBuildDirName(path.basename(path.resolve(localDir || process.cwd())));
124
+ }
125
+
126
+ function normalizeAndValidateRemoteDir(remoteDir) {
127
+ if (!remoteDir || typeof remoteDir !== 'string') return null;
128
+ // 统一成 Linux 风格
129
+ const raw = remoteDir.trim().replace(/\\/g, '/');
130
+ // 必须是绝对路径
131
+ if (!raw.startsWith('/')) {
132
+ throw new Error(
133
+ `deploy.remoteDir 必须是 Linux 绝对路径,并且只能在 /usr/local/nginx/html/* 下(当前: ${remoteDir})`
134
+ );
135
+ }
136
+
137
+ // 归一化,消除 .. 等
138
+ const resolved = path.posix.resolve(raw);
139
+ const base = '/usr/local/nginx/html';
140
+ const baseWithSlash = base + '/';
141
+
142
+ // 只允许 /usr/local/nginx/html/*(必须是子目录,不允许刚好等于 base)
143
+ if (!resolved.startsWith(baseWithSlash)) {
144
+ throw new Error(
145
+ `deploy.remoteDir 只能设置为 ${baseWithSlash}* 下的目录(当前: ${resolved})`
146
+ );
147
+ }
148
+ if (resolved === base) {
149
+ throw new Error(`deploy.remoteDir 不能是 ${base} 根目录,请设置为其子目录(例如 ${baseWithSlash}my-app)`);
150
+ }
151
+ // 再防一次:禁止路径段包含 ..(即便 resolve 已处理,这里保证输入约束更明确)
152
+ const rel = resolved.slice(baseWithSlash.length);
153
+ if (!rel || rel.split('/').some((seg) => seg === '..')) {
154
+ throw new Error(`deploy.remoteDir 非法:不允许包含 ..(当前: ${resolved})`);
155
+ }
156
+
157
+ return resolved;
158
+ }
159
+
160
+ async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
161
+ fs.ensureDirSync(path.dirname(zipPath));
162
+ await new Promise((resolve, reject) => {
163
+ const output = fs.createWriteStream(zipPath);
164
+ const archive = archiver('zip', { zlib: { level: 9 } });
165
+
166
+ output.on('close', resolve);
167
+ output.on('error', reject);
168
+ archive.on('warning', (err) => {
169
+ if (err && err.code === 'ENOENT') return;
170
+ reject(err);
171
+ });
172
+ archive.on('error', reject);
173
+
174
+ archive.pipe(output);
175
+ // 关键:zip 内顶层目录名强制为 buildDirName
176
+ archive.directory(srcDir, topFolderName);
177
+ archive.finalize();
178
+ });
179
+ }
180
+
181
+ function hashLettersFromSha256(buf, lettersLen = 12) {
182
+ const alphabet = 'abcdefghijklmnop'; // 16 letters for 0..15
183
+ let out = '';
184
+ for (let i = 0; i < buf.length && out.length < lettersLen; i++) {
185
+ const b = buf[i];
186
+ out += alphabet[(b >> 4) & 0xf];
187
+ if (out.length >= lettersLen) break;
188
+ out += alphabet[b & 0xf];
189
+ }
190
+ return out.slice(0, lettersLen);
191
+ }
192
+
193
+ function computeDirHashLetters(localDir) {
194
+ const crypto = require('crypto');
195
+ const h = crypto.createHash('sha256');
196
+ const files = listLocalFilesWithSize(localDir);
197
+ for (const f of files) {
198
+ h.update(f.relPosix);
199
+ h.update('\n');
200
+ h.update(String(f.size || 0));
201
+ h.update('\n');
202
+ h.update(String(Math.floor(f.mtimeMs || 0)));
203
+ h.update('\n');
204
+ }
205
+ const digest = h.digest();
206
+ return hashLettersFromSha256(digest, 12);
207
+ }
208
+
209
+ async function chooseDeployEnv(env) {
210
+ if (env === 'test' || env === 'prod') return env;
211
+ const ans = await inquirer.prompt([
212
+ {
213
+ type: 'list',
214
+ name: 'env',
215
+ message: '请选择部署环境',
216
+ choices: [
217
+ { name: '部署测试服(上传到服务器)', value: 'test' },
218
+ { name: '部署正式服(不上传,仅压缩)', value: 'prod' },
219
+ ],
220
+ default: 'test',
221
+ },
222
+ ]);
223
+ return ans.env;
224
+ }
225
+
226
+ async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
227
+ const files = listLocalFilesWithSize(localDir);
228
+ const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
229
+ if (files.length === 0) return { totalBytes: 0, uploadedBytes: 0, fileCount: 0 };
230
+
231
+ // 先创建需要的目录(去重)
232
+ const dirSet = new Set();
233
+ for (const f of files) {
234
+ const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
235
+ dirSet.add(path.posix.dirname(remoteFile));
236
+ }
237
+ // 目录从浅到深创建,避免多余失败
238
+ const dirs = Array.from(dirSet).sort((a, b) => a.length - b.length);
239
+ for (const d of dirs) {
240
+ await sftp.mkdir(d, true);
241
+ }
242
+
243
+ let uploadedBytes = 0;
244
+ let lastRenderAt = 0;
245
+ const render = (state) => {
246
+ if (typeof onProgress !== 'function') return;
247
+ const now = Date.now();
248
+ // 轻微节流,避免刷屏
249
+ if (now - lastRenderAt < 80 && state && state.force !== true) return;
250
+ lastRenderAt = now;
251
+ onProgress(state);
252
+ };
253
+
254
+ for (let i = 0; i < files.length; i++) {
255
+ const f = files[i];
256
+ const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
257
+ let lastTransferred = 0;
258
+
259
+ await sftp.fastPut(f.localPath, remoteFile, {
260
+ step: (transferred) => {
261
+ // transferred:当前文件已传输的总字节数(累计)
262
+ const delta = Math.max(0, Number(transferred || 0) - lastTransferred);
263
+ lastTransferred = Number(transferred || 0);
264
+ uploadedBytes += delta;
265
+ render({
266
+ uploadedBytes,
267
+ totalBytes,
268
+ fileIndex: i + 1,
269
+ fileCount: files.length,
270
+ currentFile: f.relPosix,
271
+ });
272
+ },
273
+ });
274
+
275
+ // fastPut 有时不会触发 step(极小文件/实现差异),这里兜底补齐本文件剩余字节
276
+ if (lastTransferred < f.size) {
277
+ uploadedBytes += f.size - lastTransferred;
278
+ }
279
+ render({
280
+ uploadedBytes,
281
+ totalBytes,
282
+ fileIndex: i + 1,
283
+ fileCount: files.length,
284
+ currentFile: f.relPosix,
285
+ force: true,
286
+ });
287
+ }
288
+
289
+ return { totalBytes, uploadedBytes, fileCount: files.length };
290
+ }
291
+
292
+ function resolveDeployConfig(overrides = {}) {
293
+ const projectConfig = readProjectConfig();
294
+ const deploy = (projectConfig && projectConfig.deploy) || {};
295
+
296
+ function parseDotEnvFile(envPath) {
297
+ if (!fs.existsSync(envPath)) return {};
298
+ const content = fs.readFileSync(envPath, 'utf-8');
299
+ const lines = content.split(/\r?\n/);
300
+ const out = {};
301
+ for (const raw of lines) {
302
+ const line = raw.trim();
303
+ if (!line || line.startsWith('#')) continue;
304
+ const idx = line.indexOf('=');
305
+ if (idx < 0) continue;
306
+ const key = line.slice(0, idx).trim();
307
+ let value = line.slice(idx + 1).trim();
308
+ if (
309
+ (value.startsWith('"') && value.endsWith('"')) ||
310
+ (value.startsWith("'") && value.endsWith("'"))
311
+ ) {
312
+ value = value.slice(1, -1);
313
+ }
314
+ out[key] = value;
315
+ }
316
+ return out;
317
+ }
318
+
319
+ function normalizePublicUrlToDir(publicUrl) {
320
+ // 与 config.js 规则保持一致:忽略完整 URL;去掉首尾 /;把 \ 转 /
321
+ if (!publicUrl || typeof publicUrl !== 'string') return '';
322
+ let v = publicUrl.trim();
323
+ v = v.replace(/\\/g, '/');
324
+ if (/^https?:\/\//i.test(v)) return '';
325
+ v = v.replace(/^\/+/, '').replace(/\/+$/, '');
326
+ return v;
327
+ }
328
+
329
+ // localDir 默认优先从项目 .env.production 推导(VUE_APP_PUBLIC_URL -> 目录名)
330
+ const envProdPath = path.join(process.cwd(), '.env.production');
331
+ const envProd = parseDotEnvFile(envProdPath);
332
+ const publicUrlDir = normalizePublicUrlToDir(envProd.VUE_APP_PUBLIC_URL);
333
+ const inferredFromEnvProd = publicUrlDir ? path.resolve(process.cwd(), publicUrlDir) : null;
334
+
335
+ // localDir 默认使用项目 buildDir(打包输出目录)
336
+ // 规则:优先 deploy.localDir;否则使用 .env.production 的 VUE_APP_PUBLIC_URL;否则使用 .ylyxrc.json 的 buildDir;最后才回退 ./EXTERNAL_DIGIC
337
+ const inferredLocalDir =
338
+ deploy.localDir ||
339
+ inferredFromEnvProd ||
340
+ (projectConfig && projectConfig.buildDir ? path.resolve(process.cwd(), projectConfig.buildDir) : null);
341
+
342
+ if (!inferredLocalDir) {
343
+ throw new Error(
344
+ '无法确定 deploy.localDir(本地打包目录)。请设置以下任意一种:\n' +
345
+ '- .ylyxrc.json 配置 deploy.localDir\n' +
346
+ '- 在项目 .env.production 配置 VUE_APP_PUBLIC_URL(相对路径,如 /dist/app/)\n' +
347
+ '- .ylyxrc.json 配置 buildDir(可相对/绝对路径)'
348
+ );
349
+ }
350
+
351
+ const cfg = {
352
+ env: overrides.env ?? deploy.env,
353
+ buildDir: overrides.buildDir ?? deploy.buildDir ?? projectConfig.buildDir,
354
+ host: overrides.host ?? deploy.host,
355
+ port: String(overrides.port ?? deploy.port ?? '22'),
356
+ username: overrides.username ?? deploy.username,
357
+ password: overrides.password ?? deploy.password,
358
+ localDir: overrides.localDir ?? inferredLocalDir,
359
+ remoteDir: normalizeAndValidateRemoteDir(overrides.remoteDir ?? deploy.remoteDir),
360
+ zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
361
+ zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
362
+ };
363
+
364
+ return cfg;
365
+ }
366
+
367
+ /**
368
+ * 执行部署(递归上传目录到远端)
369
+ * 测试服:上传到服务器(可选部署后压缩)
370
+ * 正式服:不上传,仅压缩
371
+ * @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
372
+ */
373
+ async function deploy(overrides = {}) {
374
+ const cfg = resolveDeployConfig(overrides);
375
+ const localFolderPath = cfg.localDir;
376
+ const remoteFolderPath = cfg.remoteDir;
377
+
378
+ const env = await chooseDeployEnv(cfg.env);
379
+ const buildDirNameForLog = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
380
+ const logger = createDeployLogger({ env, buildDirName: buildDirNameForLog, outDir: cfg.zipOutDir });
381
+ logger.logOp('DEPLOY_START', '', `env=${env}`);
382
+ logger.logOp('LOCAL_DIR', localFolderPath);
383
+ if (remoteFolderPath) logger.logOp('REMOTE_DIR', remoteFolderPath);
384
+
385
+ if (!fs.existsSync(localFolderPath)) {
386
+ logger.logOp('ERROR', localFolderPath, 'localDir_not_exists');
387
+ await logger.close();
388
+ throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
389
+ }
390
+
391
+ // 正式服:仅压缩
392
+ if (env === 'prod') {
393
+ const buildDirName = buildDirNameForLog;
394
+ const ts = formatTimestampCompact(new Date());
395
+ const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
396
+ // 优化命名:更可读、更易分割(buildDir-时间-哈希)
397
+ const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
398
+ fs.ensureDirSync(cfg.zipOutDir);
399
+ const zipName = `${zipBaseName}.zip`;
400
+ const zipPath = path.join(cfg.zipOutDir, zipName);
401
+ logger.logOp('ZIP_START', zipPath);
402
+
403
+ const zipSpinner = ora(`开始压缩:${zipName}`).start();
404
+ try {
405
+ await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
406
+ zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
407
+ logger.logOp('ZIP_DONE', zipPath);
408
+ } catch (e) {
409
+ zipSpinner.fail('压缩失败');
410
+ logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
411
+ await logger.close();
412
+ throw e;
413
+ }
414
+ logger.logOp('DEPLOY_END', '', 'prod');
415
+ await logger.close();
416
+ return;
417
+ }
418
+
419
+ // 测试服:上传到服务器
420
+ if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
421
+ if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
422
+ if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
423
+ if (!remoteFolderPath) {
424
+ logger.logOp('ERROR', '', 'missing_remoteDir');
425
+ await logger.close();
426
+ throw new Error('缺少 deploy.remoteDir(测试服上传需要):请在 .ylyxrc.json 的 deploy.remoteDir 配置远端部署目录');
427
+ }
428
+
429
+ let Client;
430
+ try {
431
+ Client = require('ssh2-sftp-client');
432
+ } catch (e) {
433
+ throw new Error(
434
+ "缺少依赖:ssh2-sftp-client。\n" +
435
+ "如果你在 ylyx-cli 仓库本地跑:请先在 ylyx-cli 目录执行 `npm i`。\n" +
436
+ "如果你用 npx:请确认你运行的是已发布且包含依赖的版本,或清理 npx 缓存后重试。"
437
+ );
438
+ }
439
+
440
+ const sftp = new Client();
441
+ try {
442
+ spinner = ora('连接服务器...').start();
443
+ logger.logOp('SFTP_CONNECT', cfg.host, `port=${cfg.port} username=${cfg.username}`);
444
+ await sftp.connect({
445
+ host: cfg.host,
446
+ port: cfg.port,
447
+ username: cfg.username,
448
+ password: cfg.password,
449
+ });
450
+
451
+ spinner.text = '服务器连接成功,准备清理远端目录...';
452
+ logger.logOp('REMOTE_DELETE_START', remoteFolderPath);
453
+ try {
454
+ await sftp.rmdir(remoteFolderPath, true);
455
+ logger.logOp('REMOTE_DELETE_DONE', remoteFolderPath);
456
+ } catch (e) {
457
+ // ignore
458
+ logger.logOp('REMOTE_DELETE_SKIP', remoteFolderPath, e && e.message ? e.message : 'ignored');
459
+ }
460
+ await sftp.mkdir(remoteFolderPath, true);
461
+ logger.logOp('REMOTE_MKDIR', remoteFolderPath);
462
+
463
+ const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
464
+ spinner.text = '开始上传目录...';
465
+ logger.logOp('UPLOAD_START', remoteDirPosix, `from=${localFolderPath}`);
466
+ const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
467
+ sftp,
468
+ localDir: localFolderPath,
469
+ remoteDirPosix,
470
+ onProgress: ({ uploadedBytes: up, totalBytes: tot, fileIndex, fileCount: cnt, currentFile }) => {
471
+ const percent = tot ? ((up / tot) * 100).toFixed(2) : '100.00';
472
+ const shortFile = currentFile ? currentFile.slice(-60) : '';
473
+ spinner.text = `上传进度: ${percent}% (${formatBytes(up)}/${formatBytes(tot)}) 文件:${fileIndex}/${cnt} ${shortFile}`;
474
+ },
475
+ });
476
+ spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
477
+ logger.logOp('UPLOAD_DONE', remoteDirPosix, `files=${fileCount} bytes=${uploadedBytes}/${totalBytes}`);
478
+ } catch (err) {
479
+ if (spinner) spinner.fail('部署失败');
480
+ logger.logOp('ERROR', '', err && err.message ? err.message : String(err));
481
+ await logger.close();
482
+ throw err;
483
+ } finally {
484
+ try {
485
+ await sftp.end();
486
+ } catch (e) {}
487
+ if (spinner) spinner.info('部署结束');
488
+ }
489
+
490
+ // 测试服可选:部署后压缩(延续之前的可选开关)
491
+ if (cfg.zipAfter) {
492
+ const buildDirName = buildDirNameForLog;
493
+ const ts = formatTimestampCompact(new Date());
494
+ const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
495
+ const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
496
+ fs.ensureDirSync(cfg.zipOutDir);
497
+ const zipName = `${zipBaseName}.zip`;
498
+ const zipPath = path.join(cfg.zipOutDir, zipName);
499
+ logger.logOp('ZIP_START', zipPath);
500
+
501
+ const zipSpinner = ora(`开始压缩:${zipName}`).start();
502
+ try {
503
+ await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
504
+ zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
505
+ logger.logOp('ZIP_DONE', zipPath);
506
+ } catch (e) {
507
+ zipSpinner.fail('压缩失败');
508
+ logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
509
+ await logger.close();
510
+ throw e;
511
+ }
512
+ }
513
+
514
+ logger.logOp('DEPLOY_END', '', 'test');
515
+ await logger.close();
516
+ }
517
+
518
+ module.exports = {
519
+ deploy,
520
+ resolveDeployConfig,
521
+ };
522
+
523
+ // 允许直接 node lib/deploy.js 运行(从 .ylyxrc.json 读取 deploy 配置)
524
+ if (require.main === module) {
525
+ deploy().catch((e) => {
526
+ console.error(e);
527
+ process.exit(1);
528
+ });
529
+ }