vite-plugin-deploy-ftp 0.0.10 → 1.0.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,10 +1,16 @@
1
1
  # vite-plugin-deploy-ftp
2
2
 
3
- 将 dist 目录上传到 FTP 服务器
3
+ 将 dist 目录上传到 FTP 服务器,支持单个或多个 FTP 服务器配置
4
4
 
5
5
  ## 介绍
6
6
 
7
- `vite-plugin-deploy-ftp` 是一个 Vite 插件,用于将打包后的文件上传到 FTP 服务器。
7
+ `vite-plugin-deploy-ftp` 是一个 Vite 插件,用于将打包后的文件上传到 FTP 服务器。插件支持:
8
+
9
+ - 单个 FTP 服务器配置
10
+ - 多个 FTP 服务器配置(可多选上传目标)
11
+ - 自动备份远程文件
12
+ - 连接重试机制
13
+ - 选择性文件备份
8
14
 
9
15
  ## 安装
10
16
 
@@ -14,23 +20,166 @@ pnpm add vite-plugin-deploy-ftp -D
14
20
 
15
21
  ## 使用
16
22
 
23
+ ### 单个 FTP 配置
24
+
25
+ ```ts
26
+ // vite.config.ts
27
+ import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
28
+
29
+ export default {
30
+ plugins: [
31
+ vitePluginDeployFtp({
32
+ open: true,
33
+ host: 'ftp.example.com',
34
+ port: 21,
35
+ user: 'username',
36
+ password: 'password',
37
+ uploadPath: '/public_html',
38
+ alias: 'https://example.com',
39
+ singleBack: false,
40
+ singleBackFiles: ['index.html'],
41
+ maxRetries: 3,
42
+ retryDelay: 1000,
43
+ }),
44
+ ],
45
+ }
46
+ ```
47
+
48
+ ### 多个 FTP 配置
49
+
17
50
  ```ts
18
51
  // vite.config.ts
19
52
  import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
20
53
 
21
- // ...existing code...
22
54
  export default {
23
- // ...existing code...
24
55
  plugins: [
25
- // 在最后一个插件中使用
26
56
  vitePluginDeployFtp({
27
57
  open: true,
28
- host: process.env.zH5FtpHost as string,
29
- port: +(process.env.zH5FtpPort || 21),
30
- user: process.env.zH5FtpUser as string,
31
- password: process.env.zH5FtpPassword as string,
32
- uploadPath: `${env.VITE_FTP_DIRNAME}`,
33
- alias: `https://h5.eventnet.cn/`,
58
+ uploadPath: '/public_html',
59
+ singleBack: false,
60
+ singleBackFiles: ['index.html'],
61
+ maxRetries: 3,
62
+ retryDelay: 1000,
63
+ ftps: [
64
+ {
65
+ name: '生产环境',
66
+ host: 'ftp.production.com',
67
+ port: 21,
68
+ user: 'prod_user',
69
+ password: 'prod_password',
70
+ alias: 'https://production.com',
71
+ },
72
+ {
73
+ name: '测试环境',
74
+ host: 'ftp.test.com',
75
+ port: 21,
76
+ user: 'test_user',
77
+ password: 'test_password',
78
+ alias: 'https://test.com',
79
+ },
80
+ {
81
+ name: '开发环境',
82
+ host: 'ftp.dev.com',
83
+ port: 21,
84
+ user: 'dev_user',
85
+ password: 'dev_password',
86
+ alias: 'https://dev.com',
87
+ },
88
+ ],
89
+ }),
90
+ ],
91
+ }
92
+ ```
93
+
94
+ ## 配置参数
95
+
96
+ ### 通用参数
97
+
98
+ | 参数 | 类型 | 默认值 | 说明 |
99
+ | ----------------- | ---------- | ---------------- | -------------------------------- |
100
+ | `open` | `boolean` | `true` | 是否启用插件 |
101
+ | `uploadPath` | `string` | - | FTP 服务器上的上传路径 |
102
+ | `singleBack` | `boolean` | `false` | 是否使用单文件备份模式 |
103
+ | `singleBackFiles` | `string[]` | `['index.html']` | 单文件备份模式下要备份的文件列表 |
104
+ | `maxRetries` | `number` | `3` | 连接失败时的最大重试次数 |
105
+ | `retryDelay` | `number` | `1000` | 重试延迟时间(毫秒) |
106
+
107
+ ### 单个 FTP 配置参数
108
+
109
+ | 参数 | 类型 | 默认值 | 说明 |
110
+ | ---------- | -------- | ------ | -------------------------- |
111
+ | `host` | `string` | - | FTP 服务器地址 |
112
+ | `port` | `number` | `21` | FTP 服务器端口 |
113
+ | `user` | `string` | - | FTP 用户名 |
114
+ | `password` | `string` | - | FTP 密码 |
115
+ | `alias` | `string` | `''` | 网站别名,用于生成完整 URL |
116
+
117
+ ### 多个 FTP 配置参数
118
+
119
+ | 参数 | 类型 | 说明 |
120
+ | ------ | ------------- | ------------------ |
121
+ | `ftps` | `FtpConfig[]` | FTP 服务器配置数组 |
122
+
123
+ #### FtpConfig 对象
124
+
125
+ | 参数 | 类型 | 默认值 | 说明 |
126
+ | ---------- | -------- | ------ | ---------------------------------- |
127
+ | `name` | `string` | - | FTP 服务器名称(用于选择界面显示) |
128
+ | `host` | `string` | - | FTP 服务器地址 |
129
+ | `port` | `number` | `21` | FTP 服务器端口 |
130
+ | `user` | `string` | - | FTP 用户名 |
131
+ | `password` | `string` | - | FTP 密码 |
132
+ | `alias` | `string` | `''` | 网站别名,用于生成完整 URL |
133
+
134
+ ## 功能特性
135
+
136
+ ### 多服务器选择
137
+
138
+ 当使用多个 FTP 配置时,插件会显示一个多选界面,让您选择要上传到哪些服务器:
139
+
140
+ ```
141
+ ? 选择要上传的FTP服务器(可多选)
142
+ ❯ ◯ 生产环境
143
+ ◯ 测试环境
144
+ ◯ 开发环境
145
+ ```
146
+
147
+ ### 备份功能
148
+
149
+ 插件提供两种备份模式:
150
+
151
+ 1. **完整备份**: 将远程目录下的所有文件打包备份
152
+ 2. **选择性备份**: 只备份指定的文件(通过 `singleBackFiles` 配置)
153
+
154
+ ### 连接重试
155
+
156
+ 当 FTP 连接失败时,插件会自动重试,您可以通过 `maxRetries` 和 `retryDelay` 参数控制重试行为。
157
+
158
+ ## 环境变量示例
159
+
160
+ 建议将敏感信息(如用户名和密码)放在环境变量中:
161
+
162
+ ```bash
163
+ # .env
164
+ VITE_FTP_HOST=ftp.example.com
165
+ VITE_FTP_PORT=21
166
+ VITE_FTP_USER=username
167
+ VITE_FTP_PASSWORD=password
168
+ VITE_FTP_PATH=/public_html
169
+ VITE_FTP_ALIAS=https://example.com
170
+ ```
171
+
172
+ ```ts
173
+ // vite.config.ts
174
+ export default {
175
+ plugins: [
176
+ vitePluginDeployFtp({
177
+ host: process.env.VITE_FTP_HOST,
178
+ port: +(process.env.VITE_FTP_PORT || 21),
179
+ user: process.env.VITE_FTP_USER,
180
+ password: process.env.VITE_FTP_PASSWORD,
181
+ uploadPath: process.env.VITE_FTP_PATH,
182
+ alias: process.env.VITE_FTP_ALIAS,
34
183
  }),
35
184
  ],
36
185
  }
package/dist/index.d.mts CHANGED
@@ -1,18 +1,39 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- type vitePluginDeployFtpOption = {
4
- host: string;
5
- port?: number;
6
- user: string;
7
- password: string;
3
+ type vitePluginDeployFtpOption = ({
4
+ uploadPath: string;
5
+ singleBackFiles?: string[];
6
+ singleBack?: boolean;
7
+ open?: boolean;
8
+ maxRetries?: number;
9
+ retryDelay?: number;
10
+ showBackFile?: boolean;
11
+ } & {
12
+ ftps: {
13
+ name: string;
14
+ host?: string;
15
+ port?: number;
16
+ user?: string;
17
+ password?: string;
18
+ alias?: string;
19
+ }[];
20
+ defaultFtp?: string;
21
+ }) | ({
8
22
  uploadPath: string;
9
23
  singleBackFiles?: string[];
10
24
  singleBack?: boolean;
11
- alias?: string;
12
25
  open?: boolean;
13
26
  maxRetries?: number;
14
27
  retryDelay?: number;
15
- };
28
+ showBackFile?: boolean;
29
+ } & {
30
+ name?: string;
31
+ host?: string;
32
+ port?: number;
33
+ user?: string;
34
+ password?: string;
35
+ alias?: string;
36
+ });
16
37
  declare function vitePluginDeployFtp(option: vitePluginDeployFtpOption): Plugin;
17
38
 
18
39
  export { vitePluginDeployFtp as default, type vitePluginDeployFtpOption };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,39 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- type vitePluginDeployFtpOption = {
4
- host: string;
5
- port?: number;
6
- user: string;
7
- password: string;
3
+ type vitePluginDeployFtpOption = ({
4
+ uploadPath: string;
5
+ singleBackFiles?: string[];
6
+ singleBack?: boolean;
7
+ open?: boolean;
8
+ maxRetries?: number;
9
+ retryDelay?: number;
10
+ showBackFile?: boolean;
11
+ } & {
12
+ ftps: {
13
+ name: string;
14
+ host?: string;
15
+ port?: number;
16
+ user?: string;
17
+ password?: string;
18
+ alias?: string;
19
+ }[];
20
+ defaultFtp?: string;
21
+ }) | ({
8
22
  uploadPath: string;
9
23
  singleBackFiles?: string[];
10
24
  singleBack?: boolean;
11
- alias?: string;
12
25
  open?: boolean;
13
26
  maxRetries?: number;
14
27
  retryDelay?: number;
15
- };
28
+ showBackFile?: boolean;
29
+ } & {
30
+ name?: string;
31
+ host?: string;
32
+ port?: number;
33
+ user?: string;
34
+ password?: string;
35
+ alias?: string;
36
+ });
16
37
  declare function vitePluginDeployFtp(option: vitePluginDeployFtpOption): Plugin;
17
38
 
18
39
  export { vitePluginDeployFtp as default, type vitePluginDeployFtpOption };
package/dist/index.js CHANGED
@@ -28,11 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
30
  // src/index.ts
31
- var src_exports = {};
32
- __export(src_exports, {
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
33
  default: () => vitePluginDeployFtp
34
34
  });
35
- module.exports = __toCommonJS(src_exports);
35
+ module.exports = __toCommonJS(index_exports);
36
36
  var import_prompts = require("@inquirer/prompts");
37
37
  var import_archiver = __toESM(require("archiver"));
38
38
  var import_basic_ftp = require("basic-ftp");
@@ -46,18 +46,17 @@ var import_vite = require("vite");
46
46
  function vitePluginDeployFtp(option) {
47
47
  const {
48
48
  open = true,
49
- host,
50
- port = 21,
51
- user,
52
- password,
53
49
  uploadPath,
54
- alias = "",
55
50
  singleBack = false,
56
51
  singleBackFiles = ["index.html"],
52
+ showBackFile = false,
57
53
  maxRetries = 3,
58
54
  retryDelay = 1e3
59
55
  } = option || {};
60
- if (!host || !user || !password || !uploadPath) {
56
+ const isMultiFtp = "ftps" in option;
57
+ const ftpConfigs = isMultiFtp ? option.ftps : [{ ...option, name: option.name || option.alias || option.host }];
58
+ const defaultFtp = isMultiFtp ? option.defaultFtp : void 0;
59
+ if (!uploadPath || isMultiFtp && (!option.ftps || option.ftps.length === 0)) {
61
60
  return {
62
61
  name: "vite-plugin-deploy-ftp",
63
62
  apply: "build",
@@ -69,10 +68,14 @@ function vitePluginDeployFtp(option) {
69
68
  };
70
69
  }
71
70
  let outDir = "dist";
71
+ let buildFailed = false;
72
72
  return {
73
73
  name: "vite-plugin-deploy-ftp",
74
74
  apply: "build",
75
75
  enforce: "post",
76
+ buildEnd(error) {
77
+ if (error) buildFailed = true;
78
+ },
76
79
  configResolved(config) {
77
80
  outDir = config.build?.outDir || "dist";
78
81
  },
@@ -80,55 +83,133 @@ function vitePluginDeployFtp(option) {
80
83
  sequential: true,
81
84
  order: "post",
82
85
  async handler() {
83
- if (!open) return;
86
+ if (!open || buildFailed) return;
84
87
  try {
85
88
  await deployToFtp();
86
89
  } catch (error) {
87
- console.error(import_chalk.default.red("FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
90
+ console.error(import_chalk.default.red("\u274C FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
88
91
  throw error;
89
92
  }
90
93
  }
91
94
  }
92
95
  };
93
96
  async function deployToFtp() {
94
- const { protocol, baseUrl } = parseAlias(alias);
95
97
  const ftpUploadChoice = await (0, import_prompts.select)({
96
98
  message: "\u662F\u5426\u4E0A\u4F20FTP",
97
99
  choices: ["\u662F", "\u5426"],
98
100
  default: "\u662F"
99
101
  });
100
102
  if (ftpUploadChoice === "\u5426") return;
101
- const client = new import_basic_ftp.Client();
102
- let uploadSpinner;
103
- try {
104
- uploadSpinner = (0, import_ora.default)("\u51C6\u5907\u81EA\u52A8\u4E0A\u4F20\uFF0C\u521B\u5EFA\u8FDE\u63A5\u4E2D...").start();
105
- await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
106
- uploadSpinner.color = "blue";
107
- uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
108
- const fileList = await client.list(uploadPath);
109
- uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${import_chalk.default.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
110
- if (fileList.length) {
111
- if (singleBack) {
112
- await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles);
113
- } else {
114
- const isBackFiles = await (0, import_prompts.select)({
115
- message: "\u662F\u5426\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6",
116
- choices: ["\u5426", "\u662F"],
117
- default: "\u5426"
103
+ let selectedConfigs = [];
104
+ if (isMultiFtp) {
105
+ if (defaultFtp) {
106
+ const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
107
+ if (defaultConfig) {
108
+ if (validateFtpConfig(defaultConfig)) {
109
+ console.log(import_chalk.default.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
110
+ selectedConfigs = [defaultConfig];
111
+ } else {
112
+ console.log(import_chalk.default.yellow(`\u26A0\uFE0F \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
113
+ }
114
+ }
115
+ }
116
+ if (selectedConfigs.length === 0) {
117
+ const validConfigs = ftpConfigs.filter(validateFtpConfig);
118
+ const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
119
+ if (invalidConfigs.length > 0) {
120
+ console.log(import_chalk.default.yellow("\n\u26A0\uFE0F \u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
121
+ invalidConfigs.forEach((config) => {
122
+ const missing = [];
123
+ if (!config.host) missing.push("host");
124
+ if (!config.user) missing.push("user");
125
+ if (!config.password) missing.push("password");
126
+ console.log(import_chalk.default.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
118
127
  });
119
- if (isBackFiles === "\u662F") {
120
- await createBackupFile(client, uploadPath, protocol, baseUrl);
128
+ console.log();
129
+ }
130
+ if (validConfigs.length === 0) {
131
+ console.error(import_chalk.default.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
132
+ return;
133
+ }
134
+ const choices = validConfigs.map((ftp) => ({
135
+ name: ftp.name,
136
+ value: ftp
137
+ }));
138
+ selectedConfigs = await (0, import_prompts.checkbox)({
139
+ message: "\u9009\u62E9\u8981\u4E0A\u4F20\u7684FTP\u670D\u52A1\u5668\uFF08\u53EF\u591A\u9009\uFF09",
140
+ choices,
141
+ required: true
142
+ });
143
+ }
144
+ } else {
145
+ const singleConfig = ftpConfigs[0];
146
+ if (validateFtpConfig(singleConfig)) {
147
+ selectedConfigs = [{ ...singleConfig, name: singleConfig.name || singleConfig.host }];
148
+ } else {
149
+ const missing = [];
150
+ if (!singleConfig.host) missing.push("host");
151
+ if (!singleConfig.user) missing.push("user");
152
+ if (!singleConfig.password) missing.push("password");
153
+ console.error(import_chalk.default.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
154
+ return;
155
+ }
156
+ }
157
+ for (const ftpConfig of selectedConfigs) {
158
+ const { host, port = 21, user, password, alias = "", name } = ftpConfig;
159
+ if (!host || !user || !password) {
160
+ console.error(import_chalk.default.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
161
+ if (!host) console.error(import_chalk.default.red(" - \u7F3A\u5C11 host"));
162
+ if (!user) console.error(import_chalk.default.red(" - \u7F3A\u5C11 user"));
163
+ if (!password) console.error(import_chalk.default.red(" - \u7F3A\u5C11 password"));
164
+ continue;
165
+ }
166
+ const { protocol, baseUrl } = parseAlias(alias);
167
+ const displayName = name || host;
168
+ console.log(import_chalk.default.blue(`
169
+ \u{1F680} \u5F00\u59CB\u4E0A\u4F20\u5230: ${displayName}`));
170
+ const client = new import_basic_ftp.Client();
171
+ let uploadSpinner;
172
+ try {
173
+ uploadSpinner = (0, import_ora.default)(`\u8FDE\u63A5\u5230 ${displayName} \u4E2D...`).start();
174
+ await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
175
+ uploadSpinner.color = "green";
176
+ uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
177
+ const fileList = await client.list(uploadPath);
178
+ uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${import_chalk.default.green(`${displayName} ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
179
+ if (fileList.length) {
180
+ if (singleBack) {
181
+ await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles, showBackFile);
182
+ } else {
183
+ const isBackFiles = await (0, import_prompts.select)({
184
+ message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
185
+ choices: ["\u5426", "\u662F"],
186
+ default: "\u5426"
187
+ });
188
+ if (isBackFiles === "\u662F") {
189
+ await createBackupFile(client, uploadPath, protocol, baseUrl, showBackFile);
190
+ }
121
191
  }
122
192
  }
193
+ const uploadFileSpinner = (0, import_ora.default)(`\u4E0A\u4F20\u5230 ${displayName} \u4E2D...`).start();
194
+ await client.uploadFromDir(outDir, uploadPath);
195
+ uploadFileSpinner.succeed(
196
+ `\u{1F389} \u4E0A\u4F20\u5230 ${displayName} \u6210\u529F! \u8BBF\u95EE\u5730\u5740: ` + import_chalk.default.green(buildUrl(protocol, baseUrl, uploadPath))
197
+ );
198
+ } catch (error) {
199
+ if (uploadSpinner) {
200
+ uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
201
+ }
202
+ console.error(import_chalk.default.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
203
+ throw error;
204
+ } finally {
205
+ client.close();
123
206
  }
124
- const uploadFileSpinner = (0, import_ora.default)("\u4E0A\u4F20\u4E2D...").start();
125
- await client.uploadFromDir(outDir, uploadPath);
126
- uploadFileSpinner.succeed("\u4E0A\u4F20\u6210\u529F url:" + import_chalk.default.green(buildUrl(protocol, baseUrl, uploadPath)));
127
- } finally {
128
- client.close();
129
207
  }
130
208
  }
131
209
  }
210
+ function validateFtpConfig(config) {
211
+ return !!(config.host && config.user && config.password);
212
+ }
132
213
  function parseAlias(alias = "") {
133
214
  const [protocol = "", baseUrl = ""] = alias.split("://");
134
215
  return {
@@ -153,12 +234,12 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
153
234
  } catch (error) {
154
235
  lastError = error instanceof Error ? error : new Error(String(error));
155
236
  if (attempt < maxRetries) {
156
- console.log(import_chalk.default.yellow(`\u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
237
+ console.log(import_chalk.default.yellow(`\u26A0\uFE0F \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
157
238
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
158
239
  }
159
240
  }
160
241
  }
161
- throw new Error(`FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
242
+ throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
162
243
  }
163
244
  function createTempDir(basePath) {
164
245
  const tempBaseDir = import_node_os.default.tmpdir();
@@ -174,13 +255,13 @@ function createTempDir(basePath) {
174
255
  import_node_fs.default.rmSync(tempPath, { recursive: true, force: true });
175
256
  }
176
257
  } catch (error) {
177
- console.warn(import_chalk.default.yellow(`\u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
258
+ console.warn(import_chalk.default.yellow(`\u26A0\uFE0F \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
178
259
  }
179
260
  }
180
261
  };
181
262
  }
182
- async function createBackupFile(client, dir, protocol, baseUrl) {
183
- const backupSpinner = (0, import_ora.default)(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
263
+ async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
264
+ const backupSpinner = (0, import_ora.default)(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
184
265
  const fileName = `backup_${(0, import_dayjs.default)().format("YYYYMMDD_HHmmss")}.zip`;
185
266
  const tempDir = createTempDir("backup-zip");
186
267
  const zipFilePath = import_node_path.default.join(import_node_os.default.tmpdir(), "vite-plugin-deploy-ftp", fileName);
@@ -189,21 +270,33 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
189
270
  if (!import_node_fs.default.existsSync(zipDir)) {
190
271
  import_node_fs.default.mkdirSync(zipDir, { recursive: true });
191
272
  }
192
- await client.downloadToDir(tempDir.path, dir);
193
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${import_chalk.default.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
194
- import_node_fs.default.readdirSync(tempDir.path).forEach((fileName2) => {
195
- if (fileName2.startsWith("backup_") && fileName2.endsWith(".zip")) {
196
- import_node_fs.default.rmSync(import_node_path.default.join(tempDir.path, fileName2));
273
+ const remoteFiles = await client.list(dir);
274
+ const filteredFiles = remoteFiles.filter((file) => !file.name.startsWith("backup_") || !file.name.endsWith(".zip"));
275
+ if (showBackFile) {
276
+ console.log(import_chalk.default.cyan(`
277
+ \u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
278
+ filteredFiles.forEach((file) => {
279
+ console.log(import_chalk.default.gray(` - ${file.name} (${file.size} bytes)`));
280
+ });
281
+ }
282
+ for (const file of filteredFiles) {
283
+ if (file.type === 1) {
284
+ await client.downloadTo(import_node_path.default.join(tempDir.path, file.name), (0, import_vite.normalizePath)(`${dir}/${file.name}`));
197
285
  }
198
- });
286
+ }
287
+ backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
199
288
  await createZipFile(tempDir.path, zipFilePath);
200
289
  backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${import_chalk.default.yellow(
201
- `\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
290
+ `==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
202
291
  )}`;
203
292
  await client.uploadFrom(zipFilePath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
204
- backupSpinner.succeed(`\u5907\u4EFD\u6210\u529F ${import_chalk.default.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`)}`);
293
+ const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
294
+ backupSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
295
+ console.log(import_chalk.default.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
296
+ console.log(import_chalk.default.green(` ${backupUrl}`));
297
+ console.log();
205
298
  } catch (error) {
206
- backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
299
+ backupSpinner.fail("\u274C \u5907\u4EFD\u5931\u8D25");
207
300
  throw error;
208
301
  } finally {
209
302
  tempDir.cleanup();
@@ -212,7 +305,7 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
212
305
  import_node_fs.default.rmSync(zipFilePath);
213
306
  }
214
307
  } catch (error) {
215
- console.warn(import_chalk.default.yellow("\u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
308
+ console.warn(import_chalk.default.yellow("\u26A0\uFE0F \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
216
309
  }
217
310
  }
218
311
  }
@@ -233,10 +326,11 @@ async function createZipFile(sourceDir, outputPath) {
233
326
  archive.finalize();
234
327
  });
235
328
  }
236
- async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles) {
329
+ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
237
330
  const timestamp = (0, import_dayjs.default)().format("YYYYMMDD_HHmmss");
238
- const backupSpinner = (0, import_ora.default)(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
331
+ const backupSpinner = (0, import_ora.default)(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
239
332
  const tempDir = createTempDir("single-backup");
333
+ let backupProgressSpinner;
240
334
  try {
241
335
  const remoteFiles = await client.list(dir);
242
336
  const backupTasks = singleBackFiles.map((fileName) => {
@@ -244,11 +338,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
244
338
  return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
245
339
  }).filter((task) => task.exists);
246
340
  if (backupTasks.length === 0) {
247
- backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
341
+ backupSpinner.warn("\u26A0\uFE0F \u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
248
342
  return;
249
343
  }
344
+ backupSpinner.stop();
345
+ if (showBackFile) {
346
+ console.log(import_chalk.default.cyan(`
347
+ \u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
348
+ backupTasks.forEach((task) => {
349
+ console.log(import_chalk.default.gray(` - ${task.fileName}`));
350
+ });
351
+ }
352
+ backupProgressSpinner = (0, import_ora.default)("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
250
353
  const concurrencyLimit = 3;
251
354
  let backedUpCount = 0;
355
+ const backedUpFiles = [];
252
356
  for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
253
357
  const batch = backupTasks.slice(i, i + concurrencyLimit);
254
358
  const promises = batch.map(async ({ fileName }) => {
@@ -256,12 +360,15 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
256
360
  const localTempPath = import_node_path.default.join(tempDir.path, fileName);
257
361
  const [name, ext] = fileName.split(".");
258
362
  const suffix = ext ? `.${ext}` : "";
259
- const backupRemotePath = (0, import_vite.normalizePath)(`${dir}/${name}.${timestamp}${suffix}`);
363
+ const backupFileName = `${name}.${timestamp}${suffix}`;
364
+ const backupRemotePath = (0, import_vite.normalizePath)(`${dir}/${backupFileName}`);
260
365
  await client.downloadTo(localTempPath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
261
366
  await client.uploadFrom(localTempPath, backupRemotePath);
367
+ const backupUrl = buildUrl(protocol, baseUrl, backupRemotePath);
368
+ backedUpFiles.push(backupUrl);
262
369
  return true;
263
370
  } catch (error) {
264
- console.warn(import_chalk.default.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
371
+ console.warn(import_chalk.default.yellow(`\u26A0\uFE0F \u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
265
372
  return false;
266
373
  }
267
374
  });
@@ -269,12 +376,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
269
376
  backedUpCount += results.filter(Boolean).length;
270
377
  }
271
378
  if (backedUpCount > 0) {
272
- backupSpinner.succeed(`\u5DF2\u5907\u4EFD ${backedUpCount} \u4E2A\u6587\u4EF6\u5230 ${import_chalk.default.green(buildUrl(protocol, baseUrl, dir))}`);
379
+ backupProgressSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
380
+ console.log(import_chalk.default.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
381
+ backedUpFiles.forEach((url) => {
382
+ console.log(import_chalk.default.green(` ${url}`));
383
+ });
384
+ console.log();
273
385
  } else {
274
- backupSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
386
+ backupProgressSpinner.fail("\u274C \u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
275
387
  }
276
388
  } catch (error) {
277
- backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
389
+ if (backupProgressSpinner) {
390
+ backupProgressSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
391
+ } else {
392
+ backupSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
393
+ }
278
394
  throw error;
279
395
  } finally {
280
396
  tempDir.cleanup();
package/dist/index.mjs CHANGED
@@ -1,29 +1,28 @@
1
1
  // src/index.ts
2
- import { select } from "@inquirer/prompts";
2
+ import { checkbox, select } from "@inquirer/prompts";
3
3
  import archiver from "archiver";
4
4
  import { Client } from "basic-ftp";
5
5
  import chalk from "chalk";
6
6
  import dayjs from "dayjs";
7
- import fs from "node:fs";
8
- import os from "node:os";
9
- import path from "node:path";
7
+ import fs from "fs";
8
+ import os from "os";
9
+ import path from "path";
10
10
  import ora from "ora";
11
11
  import { normalizePath } from "vite";
12
12
  function vitePluginDeployFtp(option) {
13
13
  const {
14
14
  open = true,
15
- host,
16
- port = 21,
17
- user,
18
- password,
19
15
  uploadPath,
20
- alias = "",
21
16
  singleBack = false,
22
17
  singleBackFiles = ["index.html"],
18
+ showBackFile = false,
23
19
  maxRetries = 3,
24
20
  retryDelay = 1e3
25
21
  } = option || {};
26
- if (!host || !user || !password || !uploadPath) {
22
+ const isMultiFtp = "ftps" in option;
23
+ const ftpConfigs = isMultiFtp ? option.ftps : [{ ...option, name: option.name || option.alias || option.host }];
24
+ const defaultFtp = isMultiFtp ? option.defaultFtp : void 0;
25
+ if (!uploadPath || isMultiFtp && (!option.ftps || option.ftps.length === 0)) {
27
26
  return {
28
27
  name: "vite-plugin-deploy-ftp",
29
28
  apply: "build",
@@ -35,10 +34,14 @@ function vitePluginDeployFtp(option) {
35
34
  };
36
35
  }
37
36
  let outDir = "dist";
37
+ let buildFailed = false;
38
38
  return {
39
39
  name: "vite-plugin-deploy-ftp",
40
40
  apply: "build",
41
41
  enforce: "post",
42
+ buildEnd(error) {
43
+ if (error) buildFailed = true;
44
+ },
42
45
  configResolved(config) {
43
46
  outDir = config.build?.outDir || "dist";
44
47
  },
@@ -46,55 +49,133 @@ function vitePluginDeployFtp(option) {
46
49
  sequential: true,
47
50
  order: "post",
48
51
  async handler() {
49
- if (!open) return;
52
+ if (!open || buildFailed) return;
50
53
  try {
51
54
  await deployToFtp();
52
55
  } catch (error) {
53
- console.error(chalk.red("FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
56
+ console.error(chalk.red("\u274C FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
54
57
  throw error;
55
58
  }
56
59
  }
57
60
  }
58
61
  };
59
62
  async function deployToFtp() {
60
- const { protocol, baseUrl } = parseAlias(alias);
61
63
  const ftpUploadChoice = await select({
62
64
  message: "\u662F\u5426\u4E0A\u4F20FTP",
63
65
  choices: ["\u662F", "\u5426"],
64
66
  default: "\u662F"
65
67
  });
66
68
  if (ftpUploadChoice === "\u5426") return;
67
- const client = new Client();
68
- let uploadSpinner;
69
- try {
70
- uploadSpinner = ora("\u51C6\u5907\u81EA\u52A8\u4E0A\u4F20\uFF0C\u521B\u5EFA\u8FDE\u63A5\u4E2D...").start();
71
- await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
72
- uploadSpinner.color = "blue";
73
- uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
74
- const fileList = await client.list(uploadPath);
75
- uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${chalk.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
76
- if (fileList.length) {
77
- if (singleBack) {
78
- await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles);
79
- } else {
80
- const isBackFiles = await select({
81
- message: "\u662F\u5426\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6",
82
- choices: ["\u5426", "\u662F"],
83
- default: "\u5426"
69
+ let selectedConfigs = [];
70
+ if (isMultiFtp) {
71
+ if (defaultFtp) {
72
+ const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
73
+ if (defaultConfig) {
74
+ if (validateFtpConfig(defaultConfig)) {
75
+ console.log(chalk.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
76
+ selectedConfigs = [defaultConfig];
77
+ } else {
78
+ console.log(chalk.yellow(`\u26A0\uFE0F \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
79
+ }
80
+ }
81
+ }
82
+ if (selectedConfigs.length === 0) {
83
+ const validConfigs = ftpConfigs.filter(validateFtpConfig);
84
+ const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
85
+ if (invalidConfigs.length > 0) {
86
+ console.log(chalk.yellow("\n\u26A0\uFE0F \u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
87
+ invalidConfigs.forEach((config) => {
88
+ const missing = [];
89
+ if (!config.host) missing.push("host");
90
+ if (!config.user) missing.push("user");
91
+ if (!config.password) missing.push("password");
92
+ console.log(chalk.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
84
93
  });
85
- if (isBackFiles === "\u662F") {
86
- await createBackupFile(client, uploadPath, protocol, baseUrl);
94
+ console.log();
95
+ }
96
+ if (validConfigs.length === 0) {
97
+ console.error(chalk.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
98
+ return;
99
+ }
100
+ const choices = validConfigs.map((ftp) => ({
101
+ name: ftp.name,
102
+ value: ftp
103
+ }));
104
+ selectedConfigs = await checkbox({
105
+ message: "\u9009\u62E9\u8981\u4E0A\u4F20\u7684FTP\u670D\u52A1\u5668\uFF08\u53EF\u591A\u9009\uFF09",
106
+ choices,
107
+ required: true
108
+ });
109
+ }
110
+ } else {
111
+ const singleConfig = ftpConfigs[0];
112
+ if (validateFtpConfig(singleConfig)) {
113
+ selectedConfigs = [{ ...singleConfig, name: singleConfig.name || singleConfig.host }];
114
+ } else {
115
+ const missing = [];
116
+ if (!singleConfig.host) missing.push("host");
117
+ if (!singleConfig.user) missing.push("user");
118
+ if (!singleConfig.password) missing.push("password");
119
+ console.error(chalk.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
120
+ return;
121
+ }
122
+ }
123
+ for (const ftpConfig of selectedConfigs) {
124
+ const { host, port = 21, user, password, alias = "", name } = ftpConfig;
125
+ if (!host || !user || !password) {
126
+ console.error(chalk.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
127
+ if (!host) console.error(chalk.red(" - \u7F3A\u5C11 host"));
128
+ if (!user) console.error(chalk.red(" - \u7F3A\u5C11 user"));
129
+ if (!password) console.error(chalk.red(" - \u7F3A\u5C11 password"));
130
+ continue;
131
+ }
132
+ const { protocol, baseUrl } = parseAlias(alias);
133
+ const displayName = name || host;
134
+ console.log(chalk.blue(`
135
+ \u{1F680} \u5F00\u59CB\u4E0A\u4F20\u5230: ${displayName}`));
136
+ const client = new Client();
137
+ let uploadSpinner;
138
+ try {
139
+ uploadSpinner = ora(`\u8FDE\u63A5\u5230 ${displayName} \u4E2D...`).start();
140
+ await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
141
+ uploadSpinner.color = "green";
142
+ uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
143
+ const fileList = await client.list(uploadPath);
144
+ uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${chalk.green(`${displayName} ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
145
+ if (fileList.length) {
146
+ if (singleBack) {
147
+ await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles, showBackFile);
148
+ } else {
149
+ const isBackFiles = await select({
150
+ message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
151
+ choices: ["\u5426", "\u662F"],
152
+ default: "\u5426"
153
+ });
154
+ if (isBackFiles === "\u662F") {
155
+ await createBackupFile(client, uploadPath, protocol, baseUrl, showBackFile);
156
+ }
87
157
  }
88
158
  }
159
+ const uploadFileSpinner = ora(`\u4E0A\u4F20\u5230 ${displayName} \u4E2D...`).start();
160
+ await client.uploadFromDir(outDir, uploadPath);
161
+ uploadFileSpinner.succeed(
162
+ `\u{1F389} \u4E0A\u4F20\u5230 ${displayName} \u6210\u529F! \u8BBF\u95EE\u5730\u5740: ` + chalk.green(buildUrl(protocol, baseUrl, uploadPath))
163
+ );
164
+ } catch (error) {
165
+ if (uploadSpinner) {
166
+ uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
167
+ }
168
+ console.error(chalk.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
169
+ throw error;
170
+ } finally {
171
+ client.close();
89
172
  }
90
- const uploadFileSpinner = ora("\u4E0A\u4F20\u4E2D...").start();
91
- await client.uploadFromDir(outDir, uploadPath);
92
- uploadFileSpinner.succeed("\u4E0A\u4F20\u6210\u529F url:" + chalk.green(buildUrl(protocol, baseUrl, uploadPath)));
93
- } finally {
94
- client.close();
95
173
  }
96
174
  }
97
175
  }
176
+ function validateFtpConfig(config) {
177
+ return !!(config.host && config.user && config.password);
178
+ }
98
179
  function parseAlias(alias = "") {
99
180
  const [protocol = "", baseUrl = ""] = alias.split("://");
100
181
  return {
@@ -119,12 +200,12 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
119
200
  } catch (error) {
120
201
  lastError = error instanceof Error ? error : new Error(String(error));
121
202
  if (attempt < maxRetries) {
122
- console.log(chalk.yellow(`\u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
203
+ console.log(chalk.yellow(`\u26A0\uFE0F \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
123
204
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
124
205
  }
125
206
  }
126
207
  }
127
- throw new Error(`FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
208
+ throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
128
209
  }
129
210
  function createTempDir(basePath) {
130
211
  const tempBaseDir = os.tmpdir();
@@ -140,13 +221,13 @@ function createTempDir(basePath) {
140
221
  fs.rmSync(tempPath, { recursive: true, force: true });
141
222
  }
142
223
  } catch (error) {
143
- console.warn(chalk.yellow(`\u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
224
+ console.warn(chalk.yellow(`\u26A0\uFE0F \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
144
225
  }
145
226
  }
146
227
  };
147
228
  }
148
- async function createBackupFile(client, dir, protocol, baseUrl) {
149
- const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
229
+ async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
230
+ const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
150
231
  const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
151
232
  const tempDir = createTempDir("backup-zip");
152
233
  const zipFilePath = path.join(os.tmpdir(), "vite-plugin-deploy-ftp", fileName);
@@ -155,21 +236,33 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
155
236
  if (!fs.existsSync(zipDir)) {
156
237
  fs.mkdirSync(zipDir, { recursive: true });
157
238
  }
158
- await client.downloadToDir(tempDir.path, dir);
159
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
160
- fs.readdirSync(tempDir.path).forEach((fileName2) => {
161
- if (fileName2.startsWith("backup_") && fileName2.endsWith(".zip")) {
162
- fs.rmSync(path.join(tempDir.path, fileName2));
239
+ const remoteFiles = await client.list(dir);
240
+ const filteredFiles = remoteFiles.filter((file) => !file.name.startsWith("backup_") || !file.name.endsWith(".zip"));
241
+ if (showBackFile) {
242
+ console.log(chalk.cyan(`
243
+ \u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
244
+ filteredFiles.forEach((file) => {
245
+ console.log(chalk.gray(` - ${file.name} (${file.size} bytes)`));
246
+ });
247
+ }
248
+ for (const file of filteredFiles) {
249
+ if (file.type === 1) {
250
+ await client.downloadTo(path.join(tempDir.path, file.name), normalizePath(`${dir}/${file.name}`));
163
251
  }
164
- });
252
+ }
253
+ backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
165
254
  await createZipFile(tempDir.path, zipFilePath);
166
255
  backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk.yellow(
167
- `\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
256
+ `==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
168
257
  )}`;
169
258
  await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
170
- backupSpinner.succeed(`\u5907\u4EFD\u6210\u529F ${chalk.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`)}`);
259
+ const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
260
+ backupSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
261
+ console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
262
+ console.log(chalk.green(` ${backupUrl}`));
263
+ console.log();
171
264
  } catch (error) {
172
- backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
265
+ backupSpinner.fail("\u274C \u5907\u4EFD\u5931\u8D25");
173
266
  throw error;
174
267
  } finally {
175
268
  tempDir.cleanup();
@@ -178,7 +271,7 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
178
271
  fs.rmSync(zipFilePath);
179
272
  }
180
273
  } catch (error) {
181
- console.warn(chalk.yellow("\u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
274
+ console.warn(chalk.yellow("\u26A0\uFE0F \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
182
275
  }
183
276
  }
184
277
  }
@@ -199,10 +292,11 @@ async function createZipFile(sourceDir, outputPath) {
199
292
  archive.finalize();
200
293
  });
201
294
  }
202
- async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles) {
295
+ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
203
296
  const timestamp = dayjs().format("YYYYMMDD_HHmmss");
204
- const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
297
+ const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
205
298
  const tempDir = createTempDir("single-backup");
299
+ let backupProgressSpinner;
206
300
  try {
207
301
  const remoteFiles = await client.list(dir);
208
302
  const backupTasks = singleBackFiles.map((fileName) => {
@@ -210,11 +304,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
210
304
  return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
211
305
  }).filter((task) => task.exists);
212
306
  if (backupTasks.length === 0) {
213
- backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
307
+ backupSpinner.warn("\u26A0\uFE0F \u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
214
308
  return;
215
309
  }
310
+ backupSpinner.stop();
311
+ if (showBackFile) {
312
+ console.log(chalk.cyan(`
313
+ \u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
314
+ backupTasks.forEach((task) => {
315
+ console.log(chalk.gray(` - ${task.fileName}`));
316
+ });
317
+ }
318
+ backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
216
319
  const concurrencyLimit = 3;
217
320
  let backedUpCount = 0;
321
+ const backedUpFiles = [];
218
322
  for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
219
323
  const batch = backupTasks.slice(i, i + concurrencyLimit);
220
324
  const promises = batch.map(async ({ fileName }) => {
@@ -222,12 +326,15 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
222
326
  const localTempPath = path.join(tempDir.path, fileName);
223
327
  const [name, ext] = fileName.split(".");
224
328
  const suffix = ext ? `.${ext}` : "";
225
- const backupRemotePath = normalizePath(`${dir}/${name}.${timestamp}${suffix}`);
329
+ const backupFileName = `${name}.${timestamp}${suffix}`;
330
+ const backupRemotePath = normalizePath(`${dir}/${backupFileName}`);
226
331
  await client.downloadTo(localTempPath, normalizePath(`${dir}/${fileName}`));
227
332
  await client.uploadFrom(localTempPath, backupRemotePath);
333
+ const backupUrl = buildUrl(protocol, baseUrl, backupRemotePath);
334
+ backedUpFiles.push(backupUrl);
228
335
  return true;
229
336
  } catch (error) {
230
- console.warn(chalk.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
337
+ console.warn(chalk.yellow(`\u26A0\uFE0F \u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
231
338
  return false;
232
339
  }
233
340
  });
@@ -235,12 +342,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
235
342
  backedUpCount += results.filter(Boolean).length;
236
343
  }
237
344
  if (backedUpCount > 0) {
238
- backupSpinner.succeed(`\u5DF2\u5907\u4EFD ${backedUpCount} \u4E2A\u6587\u4EF6\u5230 ${chalk.green(buildUrl(protocol, baseUrl, dir))}`);
345
+ backupProgressSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
346
+ console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
347
+ backedUpFiles.forEach((url) => {
348
+ console.log(chalk.green(` ${url}`));
349
+ });
350
+ console.log();
239
351
  } else {
240
- backupSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
352
+ backupProgressSpinner.fail("\u274C \u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
241
353
  }
242
354
  } catch (error) {
243
- backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
355
+ if (backupProgressSpinner) {
356
+ backupProgressSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
357
+ } else {
358
+ backupSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
359
+ }
244
360
  throw error;
245
361
  } finally {
246
362
  tempDir.cleanup();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-deploy-ftp",
3
- "version": "0.0.10",
3
+ "version": "1.0.0",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -25,21 +25,21 @@
25
25
  "license": "MIT",
26
26
  "description": "将dist目录下的文件上传到ftp服务器",
27
27
  "devDependencies": {
28
- "@types/node": "^22.10.1",
29
- "tsup": "^8.3.5",
30
- "typescript": "^5.7.2"
28
+ "@types/node": "^22.15.32",
29
+ "tsup": "^8.5.0",
30
+ "typescript": "^5.8.3"
31
31
  },
32
32
  "peerDependencies": {
33
- "vite": "^6.0.3"
33
+ "vite": "^6.0.3 || ^7"
34
34
  },
35
35
  "dependencies": {
36
- "@inquirer/prompts": "^7.2.0",
36
+ "@inquirer/prompts": "^7.5.3",
37
37
  "@types/archiver": "^6.0.3",
38
38
  "archiver": "^7.0.1",
39
39
  "basic-ftp": "^5.0.5",
40
- "chalk": "^5.3.0",
40
+ "chalk": "^5.4.1",
41
41
  "dayjs": "^1.11.13",
42
- "ora": "^8.1.1"
42
+ "ora": "^8.2.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup",