vite-plugin-deploy-ftp 3.2.0 → 3.4.0

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/README.md CHANGED
@@ -1,17 +1,6 @@
1
1
  # vite-plugin-deploy-ftp
2
2
 
3
- dist 目录上传到 FTP 服务器,支持单个或多个 FTP 服务器配置
4
-
5
- ## 介绍
6
-
7
- `vite-plugin-deploy-ftp` 是一个 Vite 插件,用于将打包后的文件上传到 FTP 服务器。插件支持:
8
-
9
- - 单个 FTP 服务器配置
10
- - 多个 FTP 服务器配置(可多选上传目标)
11
- - 自动备份远程文件
12
- - 连接重试机制
13
- - 选择性文件备份
14
- - 当前版本仅支持 ESM(`import`),不再提供 CommonJS(`require`)入口
3
+ Vite 打包后的目录上传到 FTP,适合不想手动打开 FTP 工具、重复拖文件发布的项目。
15
4
 
16
5
  ## 安装
17
6
 
@@ -19,72 +8,137 @@
19
8
  pnpm add vite-plugin-deploy-ftp -D
20
9
  ```
21
10
 
22
- ## 使用
11
+ ## 快速开始
23
12
 
24
- ### 单个 FTP 配置
13
+ 推荐用环境变量控制是否上传,默认本地普通打包不上传,只有明确开启时才发布。
14
+
15
+ ```bash
16
+ # .env
17
+ FTP_HOST=ftp.example.com
18
+ FTP_PORT=21
19
+ FTP_USER=username
20
+ FTP_PASSWORD=password
21
+ FTP_PATH=/public_html
22
+ FTP_ALIAS=https://example.com
23
+ DEPLOY_FTP=0
24
+ ```
25
25
 
26
26
  ```ts
27
27
  // vite.config.ts
28
+ import { defineConfig, loadEnv } from 'vite'
28
29
  import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
29
30
 
30
- export default {
31
- plugins: [
32
- vitePluginDeployFtp({
33
- open: true,
34
- host: 'ftp.example.com',
35
- port: 21,
36
- user: 'username',
37
- password: 'password',
38
- uploadPath: '/public_html',
39
- alias: 'https://example.com',
40
- singleBack: false,
41
- singleBackFiles: ['index.html'],
42
- maxRetries: 3,
43
- retryDelay: 1000,
44
- }),
45
- ],
46
- }
31
+ export default defineConfig(({ mode }) => {
32
+ const env = loadEnv(mode, process.cwd(), '')
33
+ const shouldDeploy = env.DEPLOY_FTP === '1'
34
+
35
+ return {
36
+ plugins: [
37
+ vitePluginDeployFtp({
38
+ open: shouldDeploy,
39
+ autoUpload: true,
40
+ failOnError: true,
41
+ host: env.FTP_HOST,
42
+ port: +(env.FTP_PORT || 21),
43
+ user: env.FTP_USER,
44
+ password: env.FTP_PASSWORD,
45
+ uploadPath: env.FTP_PATH?.split(',').map((path) => path.trim()) || '',
46
+ alias: env.FTP_ALIAS,
47
+ singleBack: true,
48
+ singleBackFiles: ['index.html'],
49
+ }),
50
+ ],
51
+ }
52
+ })
53
+ ```
54
+
55
+ 上传时再打开开关:
56
+
57
+ ```bash
58
+ # macOS / Linux
59
+ DEPLOY_FTP=1 pnpm build
60
+
61
+ # Windows PowerShell
62
+ $env:DEPLOY_FTP='1'; pnpm build
63
+ ```
64
+
65
+ `FTP_PATH` 可以写一个目录:
66
+
67
+ ```bash
68
+ FTP_PATH=/public_html
69
+ ```
70
+
71
+ 也可以写多个目录:
72
+
73
+ ```bash
74
+ FTP_PATH=/public_html,/backup_html
47
75
  ```
48
76
 
49
- ### 多个 FTP 配置
77
+ ## 常用配置说明
78
+
79
+ | 参数 | 说明 |
80
+ | ----------------- | -------------------------------------------------------------- |
81
+ | `open` | 是否启用上传。推荐用环境变量控制,避免普通打包时误上传。 |
82
+ | `autoUpload` | 是否跳过“是否上传”的确认。自动发布时建议设为 `true`。 |
83
+ | `failOnError` | 上传失败时是否让命令失败。发布流程里建议设为 `true`。 |
84
+ | `uploadPath` | 上传目录。支持字符串,也支持字符串数组,数组会上传到多个目录。 |
85
+ | `alias` | 访问域名。填写后,上传完成会输出可访问链接。 |
86
+ | `singleBack` | 是否只备份指定文件。通常备份 `index.html` 就够,速度更快。 |
87
+ | `singleBackFiles` | 单文件备份列表,支持子目录文件,例如 `assets/app.js`。 |
88
+ | `ftps` | 多个 FTP 服务器配置。需要发布到多个服务器时使用。 |
89
+ | `defaultFtp` | 多 FTP 时默认选中的服务器名称,可减少手动选择。 |
90
+ | `concurrency` | 同时上传的数量。服务器不稳定时保持默认值更稳。 |
91
+
92
+ ## 重要行为说明
93
+
94
+ - 插件只在 Vite 构建结束后上传。
95
+ - `open: false` 时不会上传,也不会检查 FTP 配置。
96
+ - `uploadPath` 传数组时,会把同一份文件依次上传到每个目录。
97
+ - 多 FTP 和多目录可以一起使用,会按“服务器 × 目录”的组合逐个上传。
98
+ - 上传前如果远端目录已有文件,会根据配置询问或执行备份。
99
+ - `singleBack: true` 时,只备份 `singleBackFiles` 里的文件。
100
+ - `autoUpload: false` 时,上传前会询问是否继续。
101
+ - 上传失败且 `failOnError: true` 时,构建命令会失败,方便发布系统拦截。
102
+ - 当前版本仅支持 ESM,也就是 `import`,不支持 `require`。
103
+
104
+ ## 风险提示
105
+
106
+ - 不建议把 FTP 用户名、密码直接写进 `vite.config.ts`,推荐放到环境变量里。
107
+ - 生产发布建议使用 `open` 环境变量开关,避免普通打包误传线上目录。
108
+ - `uploadPath` 写成数组时,会上传到多个目录,请确认每个目录都是预期目标。
109
+ - 完整备份会下载远端目录并重新上传压缩包,远端文件多时会比较慢。
110
+ - 如果 FTP 服务器不稳定,不建议把 `concurrency` 调太高。
111
+
112
+ ## 示例
113
+
114
+ ### 多个 FTP 服务器
50
115
 
51
116
  ```ts
52
- // vite.config.ts
53
117
  import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
54
118
 
55
119
  export default {
56
120
  plugins: [
57
121
  vitePluginDeployFtp({
58
- open: true,
122
+ open: process.env.DEPLOY_FTP === '1',
123
+ autoUpload: true,
59
124
  uploadPath: '/public_html',
60
- singleBack: false,
61
- singleBackFiles: ['index.html'],
62
- maxRetries: 3,
63
- retryDelay: 1000,
125
+ defaultFtp: 'production',
64
126
  ftps: [
65
127
  {
66
- name: '生产环境',
67
- host: 'ftp.production.com',
68
- port: 21,
69
- user: 'prod_user',
70
- password: 'prod_password',
71
- alias: 'https://production.com',
72
- },
73
- {
74
- name: '测试环境',
75
- host: 'ftp.test.com',
128
+ name: 'production',
129
+ host: process.env.FTP_PROD_HOST,
76
130
  port: 21,
77
- user: 'test_user',
78
- password: 'test_password',
79
- alias: 'https://test.com',
131
+ user: process.env.FTP_PROD_USER,
132
+ password: process.env.FTP_PROD_PASSWORD,
133
+ alias: 'https://example.com',
80
134
  },
81
135
  {
82
- name: '开发环境',
83
- host: 'ftp.dev.com',
136
+ name: 'test',
137
+ host: process.env.FTP_TEST_HOST,
84
138
  port: 21,
85
- user: 'dev_user',
86
- password: 'dev_password',
87
- alias: 'https://dev.com',
139
+ user: process.env.FTP_TEST_USER,
140
+ password: process.env.FTP_TEST_PASSWORD,
141
+ alias: 'https://test.example.com',
88
142
  },
89
143
  ],
90
144
  }),
@@ -92,23 +146,50 @@ export default {
92
146
  }
93
147
  ```
94
148
 
95
- ## 配置参数
149
+ ### 多个上传目录
150
+
151
+ ```ts
152
+ import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
153
+
154
+ export default {
155
+ plugins: [
156
+ vitePluginDeployFtp({
157
+ open: process.env.DEPLOY_FTP === '1',
158
+ autoUpload: true,
159
+ host: process.env.FTP_HOST,
160
+ user: process.env.FTP_USER,
161
+ password: process.env.FTP_PASSWORD,
162
+ uploadPath: ['/public_html', '/backup_html'],
163
+ alias: 'https://example.com',
164
+ }),
165
+ ],
166
+ }
167
+ ```
168
+
169
+ ## 完整配置表
96
170
 
97
171
  ### 通用参数
98
172
 
99
- | 参数 | 类型 | 默认值 | 说明 |
100
- | ----------------- | ---------- | ---------------- | -------------------------------- |
101
- | `open` | `boolean` | `true` | 是否启用插件 |
102
- | `uploadPath` | `string` | - | FTP 服务器上的上传路径 |
103
- | `singleBack` | `boolean` | `false` | 是否使用单文件备份模式 |
104
- | `singleBackFiles` | `string[]` | `['index.html']` | 单文件备份模式下要备份的文件列表 |
105
- | `maxRetries` | `number` | `3` | 连接失败时的最大重试次数 |
106
- | `retryDelay` | `number` | `1000` | 重试延迟时间(毫秒) |
173
+ | 参数 | 类型 | 默认值 | 说明 |
174
+ | ----------------- | -------------------- | ---------------- | ------------------------------------------------ |
175
+ | `open` | `boolean` | `true` | 是否启用插件 |
176
+ | `uploadPath` | `string \| string[]` | - | FTP 服务器上的上传路径,传数组时会上传到多个目录 |
177
+ | `singleBack` | `boolean` | `false` | 是否使用单文件备份模式 |
178
+ | `singleBackFiles` | `string[]` | `['index.html']` | 单文件备份模式下要备份的文件列表 |
179
+ | `debug` | `boolean` | `false` | 是否输出调试耗时 |
180
+ | `maxRetries` | `number` | `3` | 连接或上传失败时的最大重试次数 |
181
+ | `retryDelay` | `number` | `1000` | 重试延迟时间,单位毫秒 |
182
+ | `showBackFile` | `boolean` | `false` | 是否显示备份文件列表 |
183
+ | `autoUpload` | `boolean` | `false` | 是否跳过上传确认 |
184
+ | `fancy` | `boolean` | `true` | 是否使用更丰富的终端输出 |
185
+ | `failOnError` | `boolean` | `true` | 上传失败时是否中断构建命令 |
186
+ | `concurrency` | `number` | `1` | 同时上传的任务数量 |
107
187
 
108
188
  ### 单个 FTP 配置参数
109
189
 
110
190
  | 参数 | 类型 | 默认值 | 说明 |
111
191
  | ---------- | -------- | ------ | -------------------------- |
192
+ | `name` | `string` | - | FTP 配置名称 |
112
193
  | `host` | `string` | - | FTP 服务器地址 |
113
194
  | `port` | `number` | `21` | FTP 服务器端口 |
114
195
  | `user` | `string` | - | FTP 用户名 |
@@ -117,11 +198,12 @@ export default {
117
198
 
118
199
  ### 多个 FTP 配置参数
119
200
 
120
- | 参数 | 类型 | 说明 |
121
- | ------ | ------------- | ------------------ |
122
- | `ftps` | `FtpConfig[]` | FTP 服务器配置数组 |
201
+ | 参数 | 类型 | 说明 |
202
+ | ------------ | ------------- | ----------------------- |
203
+ | `ftps` | `FtpConfig[]` | FTP 服务器配置数组 |
204
+ | `defaultFtp` | `string` | 默认使用的 FTP 配置名称 |
123
205
 
124
- #### FtpConfig 对象
206
+ ### FtpConfig 对象
125
207
 
126
208
  | 参数 | 类型 | 默认值 | 说明 |
127
209
  | ---------- | -------- | ------ | ---------------------------------- |
@@ -131,57 +213,3 @@ export default {
131
213
  | `user` | `string` | - | FTP 用户名 |
132
214
  | `password` | `string` | - | FTP 密码 |
133
215
  | `alias` | `string` | `''` | 网站别名,用于生成完整 URL |
134
-
135
- ## 功能特性
136
-
137
- ### 多服务器选择
138
-
139
- 当使用多个 FTP 配置时,插件会显示一个多选界面,让您选择要上传到哪些服务器:
140
-
141
- ```
142
- ? 选择要上传的FTP服务器(可多选)
143
- ❯ ◯ 生产环境
144
- ◯ 测试环境
145
- ◯ 开发环境
146
- ```
147
-
148
- ### 备份功能
149
-
150
- 插件提供两种备份模式:
151
-
152
- 1. **完整备份**: 将远程目录下的所有文件打包备份
153
- 2. **选择性备份**: 只备份指定的文件(通过 `singleBackFiles` 配置)
154
-
155
- ### 连接重试
156
-
157
- 当 FTP 连接失败时,插件会自动重试,您可以通过 `maxRetries` 和 `retryDelay` 参数控制重试行为。
158
-
159
- ## 环境变量示例
160
-
161
- 建议将敏感信息(如用户名和密码)放在环境变量中:
162
-
163
- ```bash
164
- # .env
165
- VITE_FTP_HOST=ftp.example.com
166
- VITE_FTP_PORT=21
167
- VITE_FTP_USER=username
168
- VITE_FTP_PASSWORD=password
169
- VITE_FTP_PATH=/public_html
170
- VITE_FTP_ALIAS=https://example.com
171
- ```
172
-
173
- ```ts
174
- // vite.config.ts
175
- export default {
176
- plugins: [
177
- vitePluginDeployFtp({
178
- host: process.env.VITE_FTP_HOST,
179
- port: +(process.env.VITE_FTP_PORT || 21),
180
- user: process.env.VITE_FTP_USER,
181
- password: process.env.VITE_FTP_PASSWORD,
182
- uploadPath: process.env.VITE_FTP_PATH,
183
- alias: process.env.VITE_FTP_ALIAS,
184
- }),
185
- ],
186
- }
187
- ```
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
3
  interface BaseOption {
4
- uploadPath: string;
4
+ uploadPath: string | string[];
5
5
  singleBackFiles?: string[];
6
6
  singleBack?: boolean;
7
7
  debug?: boolean;
package/dist/index.js CHANGED
@@ -6,7 +6,6 @@ import cliProgress from "cli-progress";
6
6
  import dayjs from "dayjs";
7
7
  import fs2 from "fs";
8
8
  import { stat } from "fs/promises";
9
- import os2 from "os";
10
9
  import path2 from "path";
11
10
  import ora from "ora";
12
11
  import { normalizePath as normalizePath2 } from "vite";
@@ -33,10 +32,12 @@ function getAllFiles(dirPath, arrayOfFiles = [], relativePath = "") {
33
32
  }
34
33
  function createTempDir(basePath) {
35
34
  const tempBaseDir = os.tmpdir();
36
- const tempPath = path.join(tempBaseDir, "vite-plugin-deploy-ftp", basePath);
37
- if (!fs.existsSync(tempPath)) {
38
- fs.mkdirSync(tempPath, { recursive: true });
35
+ const tempParentDir = path.join(tempBaseDir, "vite-plugin-deploy-ftp");
36
+ const safeBasePath = basePath.replace(/[\\/]+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "-") || "temp";
37
+ if (!fs.existsSync(tempParentDir)) {
38
+ fs.mkdirSync(tempParentDir, { recursive: true });
39
39
  }
40
+ const tempPath = fs.mkdtempSync(path.join(tempParentDir, `${safeBasePath}-`));
40
41
  return {
41
42
  path: tempPath,
42
43
  cleanup: () => {
@@ -134,7 +135,8 @@ var renderPanel = (title, rows, tone = "info", footer) => {
134
135
  const paddedLabel = padVisual(row.label, labelWidth);
135
136
  const prefix = `${paddedLabel} `;
136
137
  const availableValueWidth = Math.max(8, innerWidth - stringWidth(prefix));
137
- contentLines.push(`${chalk3.gray(prefix)}${fitVisual(row.value, availableValueWidth)}`);
138
+ const value = row.preserveValue ? row.value : fitVisual(row.value, availableValueWidth);
139
+ contentLines.push(`${chalk3.gray(prefix)}${value}`);
138
140
  }
139
141
  }
140
142
  if (footer) {
@@ -143,7 +145,10 @@ var renderPanel = (title, rows, tone = "info", footer) => {
143
145
  }
144
146
  const top = color(`\u256D${"\u2500".repeat(innerWidth + 2)}\u256E`);
145
147
  const bottom = color(`\u2570${"\u2500".repeat(innerWidth + 2)}\u256F`);
146
- const body = contentLines.map((line) => `${color("\u2502")} ${fitVisual(line, innerWidth)} ${color("\u2502")}`).join("\n");
148
+ const body = contentLines.map((line) => {
149
+ const fittedLine = stringWidth(line) > innerWidth ? line : fitVisual(line, innerWidth);
150
+ return `${color("\u2502")} ${fittedLine} ${color("\u2502")}`;
151
+ }).join("\n");
147
152
  return `${top}
148
153
  ${body}
149
154
  ${bottom}`;
@@ -271,7 +276,8 @@ var renderBackupPanel = (summary) => {
271
276
  { label: "\u7ED3\u679C:", value: chalk5.green(`${summary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6`) },
272
277
  ...previewItems.map((item, index) => ({
273
278
  label: `\u6587\u4EF6 ${index + 1}:`,
274
- value: chalk5.cyan(truncateTerminalText(item, 22))
279
+ value: chalk5.cyan(item),
280
+ preserveValue: true
275
281
  }))
276
282
  ];
277
283
  if (summary.items.length > previewItems.length) {
@@ -310,7 +316,12 @@ function vitePluginDeployFtp(option) {
310
316
  const isMultiFtp = "ftps" in safeOption;
311
317
  const ftpConfigs = isMultiFtp ? safeOption.ftps || [] : [{ ...safeOption, name: safeOption.name || safeOption.alias || safeOption.host }];
312
318
  const defaultFtp = isMultiFtp ? safeOption.defaultFtp : void 0;
313
- const normalizedUploadPath = normalizeFtpUploadPath(uploadPath);
319
+ const uploadPaths = Array.isArray(uploadPath) ? uploadPath : [uploadPath];
320
+ const normalizedUploadPaths = Array.from(
321
+ new Set(
322
+ uploadPaths.map((targetPath) => typeof targetPath === "string" ? normalizeFtpUploadPath(targetPath) : "/")
323
+ )
324
+ );
314
325
  let outDir = normalizePath2(path2.resolve("dist"));
315
326
  let upload = false;
316
327
  let buildFailed = false;
@@ -322,7 +333,14 @@ function vitePluginDeployFtp(option) {
322
333
  };
323
334
  const validateOptions = () => {
324
335
  const errors = [];
325
- if (!uploadPath) errors.push("uploadPath is required");
336
+ if (uploadPaths.length === 0) {
337
+ errors.push("uploadPath is required");
338
+ }
339
+ uploadPaths.forEach((targetPath, index) => {
340
+ if (typeof targetPath !== "string" || targetPath.trim() === "") {
341
+ errors.push(`uploadPath${uploadPaths.length > 1 ? `[${index}]` : ""} is required`);
342
+ }
343
+ });
326
344
  if (!Number.isInteger(maxRetries) || maxRetries < 1) errors.push("maxRetries must be >= 1");
327
345
  if (!Number.isFinite(retryDelay) || retryDelay < 0) errors.push("retryDelay must be >= 0");
328
346
  if (!Number.isInteger(concurrency) || concurrency < 1) errors.push("concurrency must be >= 1");
@@ -706,8 +724,10 @@ function vitePluginDeployFtp(option) {
706
724
  elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
707
725
  });
708
726
  progressBar.stop();
727
+ } else if (failed > 0) {
728
+ console.log(`${getLogSymbol("warning")} \u6587\u4EF6\u4E0A\u4F20\u7ED3\u675F\uFF0C\u6210\u529F ${completed - failed}/${totalFiles}\uFF0C\u5931\u8D25 ${failed}`);
709
729
  } else {
710
- console.log(`${getLogSymbol("success")} \u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
730
+ console.log(`${getLogSymbol("success")} \u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
711
731
  }
712
732
  debugEntries.push(
713
733
  {
@@ -734,7 +754,7 @@ function vitePluginDeployFtp(option) {
734
754
  );
735
755
  return { results, debugEntries };
736
756
  };
737
- const deploySingleTarget = async (ftpConfig) => {
757
+ const deploySingleTarget = async (ftpConfig, normalizedUploadPath) => {
738
758
  const { host, port = 21, user, password, alias = "", name } = ftpConfig;
739
759
  const normalizedAlias = alias ? normalizeUrlLikeBase(alias) : "";
740
760
  if (!host || !user || !password) {
@@ -754,10 +774,11 @@ function vitePluginDeployFtp(option) {
754
774
  });
755
775
  const totalFiles = allFiles.length;
756
776
  const displayName = name || host;
777
+ const resultName = normalizedUploadPaths.length > 1 ? `${displayName} ${normalizedUploadPath}` : displayName;
757
778
  const startTime = Date.now();
758
779
  if (allFiles.length === 0) {
759
780
  console.log(`${getLogSymbol("warning")} \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6`);
760
- return { name: displayName, totalFiles: 0, failedCount: 0 };
781
+ return { name: resultName, totalFiles: 0, failedCount: 0 };
761
782
  }
762
783
  clearScreen();
763
784
  console.log(
@@ -900,7 +921,7 @@ function vitePluginDeployFtp(option) {
900
921
  }
901
922
  ];
902
923
  if (accessUrl) {
903
- resultRows.push({ label: "\u8BBF\u95EE:", value: chalk5.cyan(truncateTerminalText(accessUrl, 20)) });
924
+ resultRows.push({ label: "\u8BBF\u95EE:", value: chalk5.cyan(accessUrl), preserveValue: true });
904
925
  }
905
926
  if (failedCount > 0) {
906
927
  const failedItems = results.filter((result) => !result.success).slice(0, 2);
@@ -933,7 +954,7 @@ function vitePluginDeployFtp(option) {
933
954
  });
934
955
  console.log(renderDebugPanel(debugEntries));
935
956
  }
936
- return { name: displayName, totalFiles: results.length, failedCount };
957
+ return { name: resultName, totalFiles: results.length, failedCount };
937
958
  } catch (error) {
938
959
  if (preflightSpinner) preflightSpinner.stop();
939
960
  console.log(`
@@ -947,7 +968,7 @@ ${getLogSymbol("danger")} \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF
947
968
  console.log(renderDebugPanel(debugEntries));
948
969
  }
949
970
  return {
950
- name: displayName,
971
+ name: resultName,
951
972
  totalFiles,
952
973
  failedCount: totalFiles > 0 ? totalFiles : 1,
953
974
  error: error instanceof Error ? error : new Error(String(error))
@@ -1018,8 +1039,10 @@ ${getLogSymbol("danger")} \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF
1018
1039
  }
1019
1040
  const deployResults = [];
1020
1041
  for (const ftpConfig of selectedConfigs) {
1021
- const targetResult = await deploySingleTarget(ftpConfig);
1022
- deployResults.push(targetResult);
1042
+ for (const normalizedUploadPath of normalizedUploadPaths) {
1043
+ const targetResult = await deploySingleTarget(ftpConfig, normalizedUploadPath);
1044
+ deployResults.push(targetResult);
1045
+ }
1023
1046
  }
1024
1047
  return deployResults;
1025
1048
  };
@@ -1035,9 +1058,8 @@ ${getLogSymbol("danger")} \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF
1035
1058
  clearScreen();
1036
1059
  const validationErrors = validateOptions();
1037
1060
  if (validationErrors.length > 0) {
1038
- console.log(`${chalk5.red("\u2717 \u914D\u7F6E\u9519\u8BEF:")}
1061
+ throw new Error(`\u914D\u7F6E\u9519\u8BEF:
1039
1062
  ${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
1040
- return;
1041
1063
  }
1042
1064
  upload = true;
1043
1065
  return config;
@@ -1101,13 +1123,10 @@ async function createBackupFile(client, dir, alias, showBackFile = false, useSpi
1101
1123
  const targetUrl = resolveDisplayUrl(alias, dir);
1102
1124
  const backupSpinner = useSpinner ? ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${targetUrl}`)}`).start() : null;
1103
1125
  const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
1104
- const tempDir = createTempDir("backup-zip");
1105
- const zipFilePath = path2.join(os2.tmpdir(), "vite-plugin-deploy-ftp", fileName);
1126
+ const tempDir = createTempDir("backup-download");
1127
+ const zipTempDir = createTempDir("backup-zip");
1128
+ const zipFilePath = path2.join(zipTempDir.path, fileName);
1106
1129
  try {
1107
- const zipDir = path2.dirname(zipFilePath);
1108
- if (!fs2.existsSync(zipDir)) {
1109
- fs2.mkdirSync(zipDir, { recursive: true });
1110
- }
1111
1130
  if (backupSpinner) {
1112
1131
  backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${targetUrl}`)}`;
1113
1132
  }
@@ -1147,6 +1166,7 @@ async function createBackupFile(client, dir, alias, showBackFile = false, useSpi
1147
1166
  throw error;
1148
1167
  } finally {
1149
1168
  tempDir.cleanup();
1169
+ zipTempDir.cleanup();
1150
1170
  try {
1151
1171
  if (fs2.existsSync(zipFilePath)) {
1152
1172
  fs2.rmSync(zipFilePath);
@@ -1162,12 +1182,10 @@ async function createSingleBackup(client, dir, alias, singleBackFiles, showBackF
1162
1182
  const tempDir = createTempDir("single-backup");
1163
1183
  let backupProgressSpinner;
1164
1184
  try {
1165
- const remoteFiles = await client.list(dir);
1166
- const normalizedSingleBackFiles = singleBackFiles.map((fileName) => normalizeSelectionPath(fileName)).filter(Boolean);
1167
- const backupTasks = normalizedSingleBackFiles.map((fileName) => {
1168
- const remoteFile = remoteFiles.find((file) => file.name === fileName);
1169
- return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
1170
- }).filter((task) => task.exists);
1185
+ const normalizedSingleBackFiles = singleBackFiles.map((fileName) => normalizeSelectionPath(fileName)).map(
1186
+ (fileName) => fileName.split("/").filter((segment) => segment && segment !== ".").join("/")
1187
+ ).filter((fileName) => !fileName.split("/").includes("..")).filter(Boolean);
1188
+ const backupTasks = normalizedSingleBackFiles.map((fileName) => ({ fileName }));
1171
1189
  if (backupTasks.length === 0) {
1172
1190
  if (backupSpinner) {
1173
1191
  backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
@@ -1185,31 +1203,31 @@ async function createSingleBackup(client, dir, alias, singleBackFiles, showBackF
1185
1203
  if (useSpinner) {
1186
1204
  backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
1187
1205
  }
1188
- const concurrencyLimit = 3;
1189
1206
  let backedUpCount = 0;
1190
1207
  const backedUpFiles = [];
1191
- for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
1192
- const batch = backupTasks.slice(i, i + concurrencyLimit);
1193
- const promises = batch.map(async ({ fileName }) => {
1194
- try {
1195
- const localTempPath = path2.join(tempDir.path, fileName);
1196
- const extIndex = fileName.lastIndexOf(".");
1197
- const name = extIndex > -1 ? fileName.slice(0, extIndex) : fileName;
1198
- const ext = extIndex > -1 ? fileName.slice(extIndex) : "";
1199
- const backupFileName = `${name}.${timestamp}${ext}`;
1200
- const sourceRemotePath = normalizeRemotePath(dir, fileName);
1201
- const backupRemotePath = normalizeRemotePath(dir, backupFileName);
1202
- await client.downloadTo(localTempPath, sourceRemotePath);
1203
- await client.uploadFrom(localTempPath, backupRemotePath);
1204
- backedUpFiles.push(resolveDisplayUrl(alias, backupRemotePath));
1205
- return true;
1206
- } catch (error) {
1207
- console.warn(chalk5.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
1208
- return false;
1208
+ for (const { fileName } of backupTasks) {
1209
+ try {
1210
+ const localTempPath = path2.join(tempDir.path, fileName);
1211
+ const localTempDir = path2.dirname(localTempPath);
1212
+ if (!fs2.existsSync(localTempDir)) {
1213
+ fs2.mkdirSync(localTempDir, { recursive: true });
1209
1214
  }
1210
- });
1211
- const results = await Promise.all(promises);
1212
- backedUpCount += results.filter(Boolean).length;
1215
+ const fileDir = path2.posix.dirname(fileName);
1216
+ const fileBaseName = path2.posix.basename(fileName);
1217
+ const extIndex = fileBaseName.lastIndexOf(".");
1218
+ const name = extIndex > -1 ? fileBaseName.slice(0, extIndex) : fileBaseName;
1219
+ const ext = extIndex > -1 ? fileBaseName.slice(extIndex) : "";
1220
+ const backupFileName = `${name}.${timestamp}${ext}`;
1221
+ const backupRelativePath = fileDir === "." ? backupFileName : normalizeRemotePath(fileDir, backupFileName);
1222
+ const sourceRemotePath = normalizeRemotePath(dir, fileName);
1223
+ const backupRemotePath = normalizeRemotePath(dir, backupRelativePath);
1224
+ await client.downloadTo(localTempPath, sourceRemotePath);
1225
+ await client.uploadFrom(localTempPath, backupRemotePath);
1226
+ backedUpFiles.push(resolveDisplayUrl(alias, backupRemotePath));
1227
+ backedUpCount++;
1228
+ } catch (error) {
1229
+ console.warn(chalk5.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
1230
+ }
1213
1231
  }
1214
1232
  if (backedUpCount > 0) {
1215
1233
  backupProgressSpinner?.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-deploy-ftp",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",
@@ -32,25 +32,25 @@
32
32
  "description": "将dist目录下的文件上传到ftp服务器",
33
33
  "devDependencies": {
34
34
  "@types/cli-progress": "^3.11.6",
35
- "@types/node": "^22.15.32",
36
- "@types/yazl": "^3.3.0",
37
- "prettier": "3.8.1",
38
- "tsup": "^8.5.0",
39
- "typescript": "^5.8.3"
35
+ "@types/node": "^25.7.0",
36
+ "@types/yazl": "^3.3.1",
37
+ "prettier": "3.8.3",
38
+ "tsup": "^8.5.1",
39
+ "typescript": "^5.9.3"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vite": "^6.0.3 || ^7 || ^8"
43
43
  },
44
44
  "dependencies": {
45
- "@inquirer/prompts": "^7.5.3",
46
- "basic-ftp": "^5.0.5",
47
- "chalk": "^5.4.1",
45
+ "@inquirer/prompts": "^8.4.3",
46
+ "basic-ftp": "^6.0.1",
47
+ "chalk": "^5.6.2",
48
48
  "cli-progress": "^3.12.0",
49
- "cli-truncate": "^5.2.0",
50
- "dayjs": "^1.11.13",
49
+ "cli-truncate": "^6.0.0",
50
+ "dayjs": "^1.11.20",
51
51
  "log-symbols": "^7.0.1",
52
- "ora": "^8.2.0",
53
- "string-width": "^8.2.0",
52
+ "ora": "^9.4.0",
53
+ "string-width": "^8.2.1",
54
54
  "yazl": "^3.3.1"
55
55
  },
56
56
  "scripts": {
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ esbuild: false