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