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.
- package/bin/ylyx.js +168 -168
- package/lib/config.js +283 -299
- package/lib/deploy.js +529 -495
- package/lib/generator.js +192 -192
- package/lib/index.js +113 -113
- package/lib/interactive.js +87 -87
- package/lib/remote.js +179 -179
- package/lib/template.js +174 -174
- package/lib/utils.js +148 -148
- package/package.json +5 -5
- package/templates/react-app/files/README.md +23 -23
- package/templates/react-app/files/package.json +28 -28
- package/templates/react-app/files/public/index.html +11 -11
- package/templates/react-app/files/src/App.css +15 -15
- package/templates/react-app/files/src/App.js +15 -15
- package/templates/react-app/files/src/index.css +10 -10
- package/templates/react-app/files/src/index.js +11 -11
- package/templates/react-app/template.json +11 -11
- package/templates/react-component/files/Component.module.css +5 -5
- package/templates/react-component/files/Component.tsx +31 -31
- package/templates/react-component/files/index.ts +2 -2
- package/templates/react-component/template.json +48 -48
- package/templates/vue-app/files/.editorconfig +14 -14
- package/templates/vue-app/files/.env.development +21 -21
- package/templates/vue-app/files/.env.production +18 -18
- package/templates/vue-app/files/.eslintignore +5 -5
- package/templates/vue-app/files/.eslintrc.js +267 -267
- package/templates/vue-app/files/.prettierignore +11 -11
- package/templates/vue-app/files/.travis.yml +5 -5
- package/templates/vue-app/files/.vscode/extensions.json +7 -7
- package/templates/vue-app/files/.vscode/settings.json +39 -39
- package/templates/vue-app/files/LICENSE +191 -191
- package/templates/vue-app/files/README.md +87 -87
- package/templates/vue-app/files/babel.config.js +11 -11
- package/templates/vue-app/files/jest.config.js +24 -24
- package/templates/vue-app/files/package.json +136 -136
- package/templates/vue-app/files/plopfile.js +7 -7
- package/templates/vue-app/files/postcss.config.js +5 -5
- package/templates/vue-app/files/public/default.js +25 -25
- package/templates/vue-app/files/public/index.html +26 -26
- package/templates/vue-app/files/src/App.vue +54 -54
- package/templates/vue-app/files/src/api/bigDataV/index.js +198 -198
- package/templates/vue-app/files/src/api/data.js +42 -42
- package/templates/vue-app/files/src/api/dataArchive/index.js +186 -186
- package/templates/vue-app/files/src/api/generator/genConfig.js +16 -16
- package/templates/vue-app/files/src/api/generator/generator.js +32 -32
- package/templates/vue-app/files/src/api/indexManagement/index.js +39 -39
- package/templates/vue-app/files/src/api/institutionMonitor/index.js +396 -396
- package/templates/vue-app/files/src/api/institutionSet/index.js +120 -120
- package/templates/vue-app/files/src/api/login.js +44 -44
- package/templates/vue-app/files/src/api/selfStatistics/index.js +50 -50
- package/templates/vue-app/files/src/api/system/code.js +15 -15
- package/templates/vue-app/files/src/api/system/dict.js +34 -34
- package/templates/vue-app/files/src/api/system/dictDetail.js +52 -52
- package/templates/vue-app/files/src/api/system/equipment.js +35 -35
- package/templates/vue-app/files/src/api/system/examManage.js +49 -49
- package/templates/vue-app/files/src/api/system/examProject.js +48 -48
- package/templates/vue-app/files/src/api/system/hosModel.js +35 -35
- package/templates/vue-app/files/src/api/system/hospital.js +88 -88
- package/templates/vue-app/files/src/api/system/job.js +40 -40
- package/templates/vue-app/files/src/api/system/menu.js +67 -67
- package/templates/vue-app/files/src/api/system/model.js +42 -42
- package/templates/vue-app/files/src/api/system/risConfig.js +9 -9
- package/templates/vue-app/files/src/api/system/role.js +64 -64
- package/templates/vue-app/files/src/api/system/sysSet.js +19 -19
- package/templates/vue-app/files/src/api/system/timing.js +41 -41
- package/templates/vue-app/files/src/api/system/user.js +121 -121
- package/templates/vue-app/files/src/api/tools/alipay.js +25 -25
- package/templates/vue-app/files/src/api/tools/email.js +24 -24
- package/templates/vue-app/files/src/api/tools/localStorage.js +27 -27
- package/templates/vue-app/files/src/api/tools/qiniu.js +40 -40
- package/templates/vue-app/files/src/assets/css/font-awesome.css +2342 -2342
- package/templates/vue-app/files/src/assets/css/font-awesome.min.css +2336 -2336
- package/templates/vue-app/files/src/assets/fonts/fontawesome-webfont.svg +2671 -2671
- package/templates/vue-app/files/src/assets/icons/index.js +9 -9
- package/templates/vue-app/files/src/assets/icons/svg/hospital.svg +1 -1
- package/templates/vue-app/files/src/assets/icons/svg/hospital1.svg +1 -1
- package/templates/vue-app/files/src/assets/icons/svg/loginOne.svg +35 -35
- package/templates/vue-app/files/src/assets/icons/svg/loginOneWhite.svg +19 -19
- package/templates/vue-app/files/src/assets/icons/svg/loginOr.svg +93 -93
- package/templates/vue-app/files/src/assets/icons/svg/loginUp.svg +96 -96
- package/templates/vue-app/files/src/assets/icons/svg/monitor.svg +1 -1
- package/templates/vue-app/files/src/assets/icons/svg/offline.svg +1 -1
- package/templates/vue-app/files/src/assets/icons/svg/online.svg +1 -1
- package/templates/vue-app/files/src/assets/icons/svg/system.svg +1 -1
- package/templates/vue-app/files/src/assets/icons/svgo.yml +21 -21
- package/templates/vue-app/files/src/assets/styles/btn.scss +103 -103
- package/templates/vue-app/files/src/assets/styles/eladmin.scss +124 -124
- package/templates/vue-app/files/src/assets/styles/element-ui.scss +217 -217
- package/templates/vue-app/files/src/assets/styles/element-variables.scss +31 -31
- package/templates/vue-app/files/src/assets/styles/global.scss +73 -73
- package/templates/vue-app/files/src/assets/styles/index.scss +220 -220
- package/templates/vue-app/files/src/assets/styles/mixin.scss +60 -60
- package/templates/vue-app/files/src/assets/styles/publicStyle.scss +224 -224
- package/templates/vue-app/files/src/assets/styles/sidebar.scss +239 -239
- package/templates/vue-app/files/src/assets/styles/transition.scss +48 -48
- package/templates/vue-app/files/src/assets/styles/variables.scss +44 -44
- package/templates/vue-app/files/src/common/flexible.js +145 -145
- package/templates/vue-app/files/src/components/Breadcrumb/index.vue +87 -87
- package/templates/vue-app/files/src/components/Crud/CRUD.operation.vue +248 -248
- package/templates/vue-app/files/src/components/Crud/Pagination.vue +20 -20
- package/templates/vue-app/files/src/components/Crud/RR.operation.vue +39 -39
- package/templates/vue-app/files/src/components/Crud/UD.operation.vue +106 -106
- package/templates/vue-app/files/src/components/Crud/crud.js +907 -907
- package/templates/vue-app/files/src/components/Crud/user/UR.operation.vue +154 -154
- package/templates/vue-app/files/src/components/DateRangePicker/index.vue +42 -42
- package/templates/vue-app/files/src/components/DateRangePickerProhibitNext/index.vue +42 -42
- package/templates/vue-app/files/src/components/DateRangePickerProhibitTodayNext/index.vue +48 -48
- package/templates/vue-app/files/src/components/Dict/Dict.js +31 -31
- package/templates/vue-app/files/src/components/Dict/index.js +29 -29
- package/templates/vue-app/files/src/components/Doc/index.vue +16 -16
- package/templates/vue-app/files/src/components/Echarts/BarChart.vue +115 -115
- package/templates/vue-app/files/src/components/Echarts/Category.vue +768 -768
- package/templates/vue-app/files/src/components/Echarts/Funnel.vue +120 -120
- package/templates/vue-app/files/src/components/Echarts/Gauge.vue +74 -74
- package/templates/vue-app/files/src/components/Echarts/Graph.vue +101 -101
- package/templates/vue-app/files/src/components/Echarts/HeatMap.vue +301 -301
- package/templates/vue-app/files/src/components/Echarts/Line3D.vue +110 -110
- package/templates/vue-app/files/src/components/Echarts/PieChart.vue +84 -84
- package/templates/vue-app/files/src/components/Echarts/Point.vue +161 -161
- package/templates/vue-app/files/src/components/Echarts/RadarChart.vue +123 -123
- package/templates/vue-app/files/src/components/Echarts/Rich.vue +160 -160
- package/templates/vue-app/files/src/components/Echarts/Sankey.vue +114 -114
- package/templates/vue-app/files/src/components/Echarts/Scatter.vue +192 -192
- package/templates/vue-app/files/src/components/Echarts/Sunburst.vue +127 -127
- package/templates/vue-app/files/src/components/Echarts/ThemeRiver.vue +233 -233
- package/templates/vue-app/files/src/components/Echarts/WordCloud.vue +201 -201
- package/templates/vue-app/files/src/components/GithubCorner/index.vue +55 -55
- package/templates/vue-app/files/src/components/Hamburger/index.vue +44 -44
- package/templates/vue-app/files/src/components/HeaderSearch/index.vue +196 -196
- package/templates/vue-app/files/src/components/IconSelect/index.vue +74 -74
- package/templates/vue-app/files/src/components/IconSelect/requireIcons.js +10 -10
- package/templates/vue-app/files/src/components/Iframe/index.vue +30 -30
- package/templates/vue-app/files/src/components/JavaEdit/index.vue +78 -78
- package/templates/vue-app/files/src/components/Pagination/index.vue +100 -100
- package/templates/vue-app/files/src/components/PanThumb/index.vue +149 -149
- package/templates/vue-app/files/src/components/ParentView/index.vue +3 -3
- package/templates/vue-app/files/src/components/Permission/index.js +13 -13
- package/templates/vue-app/files/src/components/Permission/permission.js +21 -21
- package/templates/vue-app/files/src/components/RightPanel/index.vue +149 -149
- package/templates/vue-app/files/src/components/Screenfull/index.vue +60 -60
- package/templates/vue-app/files/src/components/SizeSelect/index.vue +60 -60
- package/templates/vue-app/files/src/components/SvgIcon/index.vue +66 -66
- package/templates/vue-app/files/src/components/ThemePicker/index.vue +177 -177
- package/templates/vue-app/files/src/components/UploadExcel/index.vue +149 -149
- package/templates/vue-app/files/src/components/WaterMark/setWaterMark.js +68 -68
- package/templates/vue-app/files/src/components/WaterMark/waterMark.vue +69 -69
- package/templates/vue-app/files/src/components/Xgplayer/index.vue +49 -49
- package/templates/vue-app/files/src/components/YamlEdit/index.vue +81 -81
- package/templates/vue-app/files/src/layout/components/AppMain.vue +67 -67
- package/templates/vue-app/files/src/layout/components/Navbar.vue +209 -209
- package/templates/vue-app/files/src/layout/components/Settings/index.vue +125 -125
- package/templates/vue-app/files/src/layout/components/Sidebar/FixiOSBug.js +26 -26
- package/templates/vue-app/files/src/layout/components/Sidebar/Item.vue +29 -29
- package/templates/vue-app/files/src/layout/components/Sidebar/Link.vue +35 -35
- package/templates/vue-app/files/src/layout/components/Sidebar/Logo.vue +93 -93
- package/templates/vue-app/files/src/layout/components/Sidebar/SidebarItem.vue +104 -104
- package/templates/vue-app/files/src/layout/components/Sidebar/index.vue +79 -79
- package/templates/vue-app/files/src/layout/components/TagsView/ScrollPane.vue +90 -90
- package/templates/vue-app/files/src/layout/components/TagsView/index.vue +299 -299
- package/templates/vue-app/files/src/layout/components/index.js +5 -5
- package/templates/vue-app/files/src/layout/index.vue +119 -119
- package/templates/vue-app/files/src/layout/mixin/ResizeHandler.js +45 -45
- package/templates/vue-app/files/src/main.js +89 -89
- package/templates/vue-app/files/src/mixins/crud.js +348 -348
- package/templates/vue-app/files/src/router/index.js +156 -156
- package/templates/vue-app/files/src/router/routers.js +203 -203
- package/templates/vue-app/files/src/settings.js +57 -57
- package/templates/vue-app/files/src/store/getters.js +29 -29
- package/templates/vue-app/files/src/store/index.js +25 -25
- package/templates/vue-app/files/src/store/modules/api.js +33 -33
- package/templates/vue-app/files/src/store/modules/app.js +55 -55
- package/templates/vue-app/files/src/store/modules/permission.js +79 -79
- package/templates/vue-app/files/src/store/modules/register.js +17 -17
- package/templates/vue-app/files/src/store/modules/settings.js +46 -46
- package/templates/vue-app/files/src/store/modules/tagsView.js +168 -168
- package/templates/vue-app/files/src/store/modules/user.js +119 -119
- package/templates/vue-app/files/src/utils/auth.js +18 -18
- package/templates/vue-app/files/src/utils/base64ToFile.js +27 -27
- package/templates/vue-app/files/src/utils/clipboard.js +36 -36
- package/templates/vue-app/files/src/utils/datetime.js +289 -289
- package/templates/vue-app/files/src/utils/getBlobFile.js +10 -10
- package/templates/vue-app/files/src/utils/getTime.js +29 -29
- package/templates/vue-app/files/src/utils/index.js +520 -520
- package/templates/vue-app/files/src/utils/permission.js +29 -29
- package/templates/vue-app/files/src/utils/pinyin.js +450 -450
- package/templates/vue-app/files/src/utils/request.js +139 -139
- package/templates/vue-app/files/src/utils/rsaEncrypt.js +37 -37
- package/templates/vue-app/files/src/utils/shortcuts.js +210 -210
- package/templates/vue-app/files/src/utils/upload.js +12 -12
- package/templates/vue-app/files/src/utils/validate.js +262 -262
- package/templates/vue-app/files/src/views/components/Echarts.vue +124 -124
- package/templates/vue-app/files/src/views/components/Editor.vue +75 -75
- package/templates/vue-app/files/src/views/components/MarkDown.vue +44 -44
- package/templates/vue-app/files/src/views/components/UVideo.vue +37 -37
- package/templates/vue-app/files/src/views/components/YamlEdit.vue +206 -206
- package/templates/vue-app/files/src/views/components/camera/camera.vue +115 -115
- package/templates/vue-app/files/src/views/components/camera/cameraTest.vue +158 -158
- package/templates/vue-app/files/src/views/components/excel/upload-excel.vue +41 -41
- package/templates/vue-app/files/src/views/components/icons/element-icons.js +74 -74
- package/templates/vue-app/files/src/views/components/icons/index.vue +106 -106
- package/templates/vue-app/files/src/views/components/icons/svg-icons.js +10 -10
- package/templates/vue-app/files/src/views/components/scroll/index.vue +107 -107
- package/templates/vue-app/files/src/views/components/scrollFull/index.vue +99 -99
- package/templates/vue-app/files/src/views/dashboard/LineChart.vue +138 -138
- package/templates/vue-app/files/src/views/dashboard/PanelGroup.vue +173 -173
- package/templates/vue-app/files/src/views/dashboard/mixins/resize.js +57 -57
- package/templates/vue-app/files/src/views/dataArchive/delTable.vue +354 -354
- package/templates/vue-app/files/src/views/dataArchive/exportDetail.vue +121 -121
- package/templates/vue-app/files/src/views/dataArchive/exportList.vue +160 -160
- package/templates/vue-app/files/src/views/dataArchive/index.vue +2104 -2104
- package/templates/vue-app/files/src/views/features/401.vue +83 -83
- package/templates/vue-app/files/src/views/features/404.vue +226 -226
- package/templates/vue-app/files/src/views/features/redirect.vue +12 -12
- package/templates/vue-app/files/src/views/generator/config.vue +348 -348
- package/templates/vue-app/files/src/views/generator/index.vue +143 -143
- package/templates/vue-app/files/src/views/generator/preview.vue +39 -39
- package/templates/vue-app/files/src/views/home.vue +80 -80
- package/templates/vue-app/files/src/views/indexManagement/index.vue +422 -422
- package/templates/vue-app/files/src/views/indexManagement/other.vue +286 -286
- package/templates/vue-app/files/src/views/institutionMonitor/components/LineChart.vue +191 -191
- package/templates/vue-app/files/src/views/institutionMonitor/index.scss +339 -339
- package/templates/vue-app/files/src/views/institutionMonitor/index.vue +1385 -1385
- package/templates/vue-app/files/src/views/institutionMonitor/mixins/resize.js +57 -57
- package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/checkProjectCode.vue +267 -267
- package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/imageAbnormalMonitor.vue +361 -361
- package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/index.vue +686 -686
- package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/leakageAnomaly.vue +270 -270
- package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/moveMonitore.vue +114 -114
- package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/qualityMonitore.vue +338 -338
- package/templates/vue-app/files/src/views/institutionSet/cloudSet.vue +534 -534
- package/templates/vue-app/files/src/views/institutionSet/index.vue +2550 -2550
- package/templates/vue-app/files/src/views/login.vue +243 -243
- package/templates/vue-app/files/src/views/qualityLogin.vue +280 -280
- package/templates/vue-app/files/src/views/selfStatistics/components/LineChart.vue +296 -296
- package/templates/vue-app/files/src/views/selfStatistics/hospitalDetail/index.vue +533 -533
- package/templates/vue-app/files/src/views/selfStatistics/index.scss +447 -447
- package/templates/vue-app/files/src/views/selfStatistics/index.vue +903 -903
- package/templates/vue-app/files/src/views/selfStatistics/mixins/resize.js +57 -57
- package/templates/vue-app/files/src/views/selfStatistics/monitorDetail/index.vue +285 -285
- package/templates/vue-app/files/src/views/selfStatistics/offlineDetail/index.vue +104 -104
- package/templates/vue-app/files/src/views/selfStatistics/selfActiveDetail/index.vue +104 -104
- package/templates/vue-app/files/src/views/selfStatistics/selfMonitorDetail/index.vue +420 -420
- package/templates/vue-app/files/src/views/selfStatistics/setWaterMark/setWaterMark.js +68 -68
- package/templates/vue-app/files/src/views/selfStatistics/setWaterMark/waterMark.vue +69 -69
- package/templates/vue-app/files/src/views/system/dept/index.vue +144 -144
- package/templates/vue-app/files/src/views/system/dict/dictDetail.vue +146 -146
- package/templates/vue-app/files/src/views/system/dict/index.vue +159 -159
- package/templates/vue-app/files/src/views/system/examModality/index.vue +174 -174
- package/templates/vue-app/files/src/views/system/examPart/index.vue +184 -184
- package/templates/vue-app/files/src/views/system/examProject/index.vue +321 -321
- package/templates/vue-app/files/src/views/system/examine/index.vue +66 -66
- package/templates/vue-app/files/src/views/system/job/index.vue +119 -119
- package/templates/vue-app/files/src/views/system/job/module/form.vue +72 -72
- package/templates/vue-app/files/src/views/system/job/module/header.vue +48 -48
- package/templates/vue-app/files/src/views/system/menu/index.vue +327 -327
- package/templates/vue-app/files/src/views/system/model/index.vue +285 -285
- package/templates/vue-app/files/src/views/system/role/index.vue +434 -434
- package/templates/vue-app/files/src/views/system/sysSet/index.vue +84 -84
- package/templates/vue-app/files/src/views/system/timing/index.vue +289 -289
- package/templates/vue-app/files/src/views/system/timing/log.vue +143 -143
- package/templates/vue-app/files/src/views/system/user/admin.vue +414 -414
- package/templates/vue-app/files/src/views/system/user/center/updateEmail.vue +146 -146
- package/templates/vue-app/files/src/views/system/user/center/updatePass.vue +168 -168
- package/templates/vue-app/files/src/views/system/user/center.vue +140 -140
- package/templates/vue-app/files/src/views/system/user/index.vue +972 -972
- package/templates/vue-app/files/src/views/systemSet/role/index.vue +314 -314
- package/templates/vue-app/files/src/views/systemSet/user/admin.vue +414 -414
- package/templates/vue-app/files/src/views/systemSet/user/center/updateEmail.vue +146 -146
- package/templates/vue-app/files/src/views/systemSet/user/center/updatePass.vue +115 -115
- package/templates/vue-app/files/src/views/systemSet/user/center.vue +351 -351
- package/templates/vue-app/files/src/views/systemSet/user/index.vue +928 -928
- package/templates/vue-app/files/src/views/tools/aliPay/config.vue +102 -102
- package/templates/vue-app/files/src/views/tools/aliPay/index.vue +48 -48
- package/templates/vue-app/files/src/views/tools/aliPay/toPay.vue +91 -91
- package/templates/vue-app/files/src/views/tools/email/config.vue +91 -91
- package/templates/vue-app/files/src/views/tools/email/index.vue +42 -42
- package/templates/vue-app/files/src/views/tools/email/send.vue +159 -159
- package/templates/vue-app/files/src/views/tools/storage/index.vue +35 -35
- package/templates/vue-app/files/src/views/tools/storage/local/index.vue +207 -207
- package/templates/vue-app/files/src/views/tools/storage/qiniu/form.vue +108 -108
- package/templates/vue-app/files/src/views/tools/storage/qiniu/index.vue +241 -241
- package/templates/vue-app/files/src/views/tools/swagger/index.vue +14 -14
- package/templates/vue-app/files/vue.config.js +173 -173
- 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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
spinner
|
|
443
|
-
logger.logOp('
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
logger.logOp('
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|