@seayoo-web/finder 2.2.3 → 2.2.4

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,3 +1,113 @@
1
1
  # webFinder 代理工具
2
2
 
3
- !! internal use only
3
+ > !! internal use only
4
+
5
+ 把构建产物(目录或单文件)压缩并部署到 finder 服务器的代理工具,可直接调用 API,也可作为 Vite 插件在构建结束后自动部署。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pnpm add -D @seayoo-web/finder
11
+ ```
12
+
13
+ > 需要 Node >= 22。
14
+
15
+ ## API
16
+
17
+ ### `finderDeploy(option)`
18
+
19
+ 部署一个目录。返回 `Promise<string>`(部署成功的目标地址,多目标时以逗号连接)。
20
+
21
+ ```ts
22
+ import { finderDeploy } from "@seayoo-web/finder";
23
+
24
+ await finderDeploy({
25
+ dist: "./dist",
26
+ deployTo: "finder.seayoo.io/my-project",
27
+ user: process.env.FINDER_USER!,
28
+ key: process.env.FINDER_KEY!,
29
+ preview: true,
30
+ commitLogs: "feat: 首页改版\nfix: 修复登录跳转",
31
+ });
32
+ ```
33
+
34
+ | 参数 | 类型 | 必填 | 说明 |
35
+ | ------------- | --------------------------------- | ---- | -------------------------------------------------------------------------- |
36
+ | `dist` | `string` | 是 | 需要部署的代码目录 |
37
+ | `deployTo` | `string \| string[]` | 是 | 部署目标地址,传数组则部署到多个目标 |
38
+ | `user` | `string` | 是 | 部署认证 user |
39
+ | `key` | `string` | 是 | 部署认证 key |
40
+ | `ignoreFiles` | `string[]` | 否 | 额外忽略规则,见下方「忽略规则」 |
41
+ | `preview` | `boolean \| string \| string[]` | 否 | 部署后是否打开预览;`true` 打开默认 `index.html`,也可指定一个或多个文件 |
42
+ | `commitLogs` | `string` | 否 | 本次部署的更新说明,换行用 `\n` |
43
+ | `debug` | `boolean` | 否 | 输出更多调试信息 |
44
+ | `ignoreCache` | `boolean` | 否 | 忽略本地缓存的服务器支持项目列表,强制重新拉取 |
45
+
46
+ 多目标部署会并发尝试所有目标:全部成功才正常返回,存在失败时会在尝试完所有目标后抛出聚合错误(包含每个失败目标及原因),不会因为单点失败而丢掉其余目标的结果。
47
+
48
+ ### `finderUpload(option)`
49
+
50
+ 上传单个文件。`filePath` 与 `fileContent` 二选一,`filePath` 优先级更高。
51
+
52
+ ```ts
53
+ import { finderUpload } from "@seayoo-web/finder";
54
+
55
+ await finderUpload({
56
+ fileContent: JSON.stringify({ version: "1.0.0" }),
57
+ deployTo: "finder.seayoo.io/my-project/meta.json",
58
+ user: process.env.FINDER_USER!,
59
+ key: process.env.FINDER_KEY!,
60
+ });
61
+ ```
62
+
63
+ | 参数 | 类型 | 必填 | 说明 |
64
+ | ------------- | ------------------ | ---- | ------------------------------------------ |
65
+ | `deployTo` | `string` | 是 | 部署目标全路径,需包含文件名 |
66
+ | `user` | `string` | 是 | 部署认证 user |
67
+ | `key` | `string` | 是 | 部署认证 key |
68
+ | `filePath` | `string` | 否\* | 需要上传的文件路径(与 `fileContent` 二选一) |
69
+ | `fileContent` | `string \| Buffer` | 否\* | 需要上传的文件内容(与 `filePath` 二选一) |
70
+ | `preview` | `boolean` | 否 | 上传后是否打开预览地址 |
71
+ | `debug` | `boolean` | 否 | 输出更多调试信息 |
72
+ | `ignoreCache` | `boolean` | 否 | 忽略本地缓存的服务器支持项目列表 |
73
+
74
+ > \* `filePath` 与 `fileContent` 至少提供其一。
75
+
76
+ ### `viteDeployPlugin(option)`
77
+
78
+ Vite 插件,在 `closeBundle` 阶段自动取构建产物目录并调用 `finderDeploy`(`preview` 默认为 `true`)。
79
+
80
+ ```ts
81
+ // vite.config.ts
82
+ import { defineConfig } from "vite";
83
+ import { viteDeployPlugin } from "@seayoo-web/finder";
84
+
85
+ export default defineConfig({
86
+ plugins: [
87
+ viteDeployPlugin({
88
+ deployTo: "finder.seayoo.io/my-project",
89
+ user: process.env.FINDER_USER!,
90
+ key: process.env.FINDER_KEY!,
91
+ onBeforeDeploy: (distDir) => console.log("即将部署", distDir),
92
+ onFinished: () => console.log("部署完成"),
93
+ onError: (error) => console.error("部署失败", error.message),
94
+ }),
95
+ ],
96
+ });
97
+ ```
98
+
99
+ 入参为 `finderDeploy` 的全部参数(`dist` 由插件自动推断,无需传入)外加以下钩子:
100
+
101
+ | 钩子 | 类型 | 说明 |
102
+ | ---------------- | ------------------------------- | -------------------------- |
103
+ | `onBeforeDeploy` | `(distDir: string) => unknown` | 部署开始前调用 |
104
+ | `onFinished` | `() => unknown` | 部署成功后调用 |
105
+ | `onError` | `(error: Error) => unknown` | 部署失败后调用,回传错误 |
106
+
107
+ ## 忽略规则
108
+
109
+ `ignoreFiles` 会与一组预设规则(`node_modules/`、`.git/`、`.vscode/`、`__MACOSX/`、`.DS_Store`、`.gitkeep`)合并后生效。规则只针对相对部署根的路径做匹配,不会被部署目录绝对路径中的祖先目录名干扰:
110
+
111
+ - 以 `/` 结尾表示目录规则,匹配路径中的某个目录段(如 `assets/`)。
112
+ - 不以 `/` 结尾表示文件规则,按文件名精确匹配(如 `manifest.json`)。
113
+ - 支持 `*` 通配(如 `*.map`、`temp*/`)。
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import fs, { existsSync, lstatSync, readFileSync, readdirSync, writeFileSync } from "fs";
1
+ import fs, { existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2
2
  import path, { basename, dirname, join, normalize, relative, resolve, sep } from "path";
3
3
  import open from "open";
4
- import colors from "picocolors";
5
4
  import { zip } from "compressing";
5
+ import colors from "picocolors";
6
6
  import os from "os";
7
7
  //#region src/compress.ts
8
8
  var presetIgnores = [
@@ -32,20 +32,28 @@ function compressToBuffer(sourceDir, ignoreFiles, debug) {
32
32
  zipStream.on("data", (chunk) => chunks.push(chunk)).on("end", () => resolve(Buffer.concat(chunks))).on("error", reject);
33
33
  });
34
34
  }
35
- function getAllFiles(dir, ignores = []) {
35
+ function getAllFiles(dir, ignores = [], root = dir) {
36
36
  const list = [];
37
37
  readdirSync(dir).forEach((file) => {
38
38
  const filePath = join(dir, file);
39
- if (lstatSync(filePath).isDirectory()) list.push(...getAllFiles(filePath, ignores));
40
- else if (!isIgnoreFile(filePath, ignores)) list.push(filePath);
39
+ const stats = lstatSync(filePath);
40
+ const relativePath = relative(root, filePath);
41
+ if (stats.isDirectory()) {
42
+ if (!isIgnoreFile(relativePath, ignores, true)) list.push(...getAllFiles(filePath, ignores, root));
43
+ } else if (!isIgnoreFile(relativePath, ignores)) list.push(filePath);
41
44
  });
42
45
  return list;
43
46
  }
44
- function isIgnoreFile(filePath, ignores) {
47
+ function isIgnoreFile(filePath, ignores, isDir = false) {
45
48
  const filename = basename(filePath);
46
- const dirs = normalize(filePath).split(sep);
49
+ const segments = normalize(filePath).split(sep);
50
+ const dirSegments = isDir ? segments : segments.slice(0, -1);
47
51
  return ignores.some((pattern) => {
48
- if (pattern.endsWith("/")) return pattern.includes("*") ? dirs.some((dir) => getRegexp(pattern.slice(0, -1)).test(dir)) : dirs.includes(pattern.slice(0, -1));
52
+ if (pattern.endsWith("/")) {
53
+ const dirPattern = pattern.slice(0, -1);
54
+ return pattern.includes("*") ? dirSegments.some((dir) => getRegexp(dirPattern).test(dir)) : dirSegments.includes(dirPattern);
55
+ }
56
+ if (isDir) return false;
49
57
  return pattern.includes("*") ? getRegexp(pattern).test(filename) : filename === pattern;
50
58
  });
51
59
  }
@@ -65,8 +73,9 @@ function getSystemTempDir() {
65
73
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
66
74
  return dir;
67
75
  }
68
- if (+process.version.replace(/\..+/, "").slice(1) < 20) process.emitWarning = function() {};
69
- async function request({ url, method, headers, data }) {
76
+ /** 单次请求默认超时(毫秒),避免 finder 服务器无响应时部署无限挂起 */
77
+ var DEFAULT_REQUEST_TIMEOUT = 12e4;
78
+ async function request({ url, method, headers, data, timeout = DEFAULT_REQUEST_TIMEOUT }) {
70
79
  const hasFileUpload = method === "POST" && data && Object.values(data).some((value) => typeof value === "object" && "buffer" in value);
71
80
  const reqHeaders = new Headers();
72
81
  if (headers) Object.entries(headers).forEach(([key, value]) => {
@@ -74,7 +83,8 @@ async function request({ url, method, headers, data }) {
74
83
  });
75
84
  const requestInit = {
76
85
  method,
77
- headers: reqHeaders
86
+ headers: reqHeaders,
87
+ signal: AbortSignal.timeout(timeout)
78
88
  };
79
89
  try {
80
90
  if (data) if (hasFileUpload) {
@@ -105,7 +115,7 @@ async function request({ url, method, headers, data }) {
105
115
  } catch (err) {
106
116
  return {
107
117
  status: 500,
108
- message: err instanceof Error ? err.message : String(err),
118
+ message: err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") ? `请求超时(${timeout}ms):${url}` : err instanceof Error ? err.message : String(err),
109
119
  data: null
110
120
  };
111
121
  }
@@ -130,8 +140,8 @@ var FinderApiPaths = {
130
140
  async function deploy(option) {
131
141
  const { debug, target, buffer, user, key, payload, ignoreCache } = option;
132
142
  const targetServer = await findTargetServer(target, debug, ignoreCache);
133
- if (!targetServer) throw colors.bgRed(`finder不支持该域名部署,请检查 ${target}`);
134
- if (!user || !key) throw colors.bgRed("部署缺少认证信息(user & key)");
143
+ if (!targetServer) throw new Error(`finder不支持该域名部署,请检查 ${target}`);
144
+ if (!user || !key) throw new Error("部署缺少认证信息(user & key)");
135
145
  const zipMockName = `${Date.now()}${Math.random().toString(16).slice(-3)}.zip`;
136
146
  const { status, data } = await request({
137
147
  url: `${getFinderServerFullPath(targetServer)}${FinderApiPaths.deploy}?target=${encodeURIComponent(pure(target))}`,
@@ -150,8 +160,8 @@ async function deploy(option) {
150
160
  payload: payload ? JSON.stringify(payload) : ""
151
161
  }
152
162
  });
153
- if (status !== 200) throw colors.bgRed(`部署接口错误,Server: ${targetServer},Status: ${status},Response: ${JSON.stringify(data)}`);
154
- if (!data || typeof data !== "object" || "err" in data && data.err || !("data" in data) || typeof data.data !== "string") throw colors.bgRed(`部署接口响应错误。Server: ${targetServer},Response: ${JSON.stringify(data)}`);
163
+ if (status !== 200) throw new Error(`部署接口错误,Server: ${targetServer},Status: ${status},Response: ${JSON.stringify(data)}`);
164
+ if (!data || typeof data !== "object" || "err" in data && data.err || !("data" in data) || typeof data.data !== "string") throw new Error(`部署接口响应错误。Server: ${targetServer},Response: ${JSON.stringify(data)}`);
155
165
  const url = data.data;
156
166
  if (debug) console.log("部署完毕,接口返回内容", data);
157
167
  return { previewUrl: url.endsWith("/") ? url.replace(/\/*$/, "/") + "index.html?" + Math.random().toString(16).slice(2) : url.startsWith("http") ? url : "" };
@@ -160,8 +170,8 @@ async function deploy(option) {
160
170
  async function upload(option) {
161
171
  const { debug, target, buffer, user, key, ignoreCache } = option;
162
172
  const targetServer = await findTargetServer(target, debug, ignoreCache);
163
- if (!targetServer) throw colors.bgRed(`finder不支持该域名部署,请检查 ${target}`);
164
- if (!user || !key) throw colors.bgRed("部署缺少认证信息(user & key)");
173
+ if (!targetServer) throw new Error(`finder不支持该域名部署,请检查 ${target}`);
174
+ if (!user || !key) throw new Error("部署缺少认证信息(user & key)");
165
175
  const filename = basename(target);
166
176
  const deployTarget = dirname(pure(target));
167
177
  const { status, data } = await request({
@@ -180,8 +190,8 @@ async function upload(option) {
180
190
  }
181
191
  }
182
192
  });
183
- if (status !== 200) throw colors.bgRed(`上传接口错误,Server: ${targetServer},Status: ${status}`);
184
- if (!data || typeof data !== "object" || "err" in data && data.err || !("data" in data) || typeof data.data !== "string") throw colors.bgRed(`上传接口响应错误,Server: ${targetServer},Response: ${JSON.stringify(data)}`);
193
+ if (status !== 200) throw new Error(`上传接口错误,Server: ${targetServer},Status: ${status}`);
194
+ if (!data || typeof data !== "object" || "err" in data && data.err || !("data" in data) || typeof data.data !== "string") throw new Error(`上传接口响应错误,Server: ${targetServer},Response: ${JSON.stringify(data)}`);
185
195
  return { previewUrl: `https://${pure(target)}` };
186
196
  }
187
197
  var getFinderServerFullPath = function(domain) {
@@ -190,10 +200,10 @@ var getFinderServerFullPath = function(domain) {
190
200
  async function findTargetServer(target, debug, ignoreCache) {
191
201
  const t = pure(target);
192
202
  await updateSupportedProjects(!!ignoreCache, debug);
193
- for (const domain in FinderServers) if (FinderServers[domain].find((url) => t.startsWith(url))) return domain;
203
+ for (const domain in FinderServers) if (FinderServers[domain].find((url) => url && t.startsWith(url))) return domain;
194
204
  if (!ignoreCache) {
195
205
  await updateSupportedProjects(true, debug);
196
- for (const domain in FinderServers) if (FinderServers[domain].find((url) => t.startsWith(url))) return domain;
206
+ for (const domain in FinderServers) if (FinderServers[domain].find((url) => url && t.startsWith(url))) return domain;
197
207
  }
198
208
  return null;
199
209
  }
@@ -235,7 +245,7 @@ async function getServerSupportedProjects(serverDomain, ignoreCache = false, deb
235
245
  serverDomain,
236
246
  list: data
237
247
  });
238
- const pureList = data.map(pure);
248
+ const pureList = data.map(pure).filter(Boolean);
239
249
  writeFileSync(cacheFile, JSON.stringify(pureList));
240
250
  return pureList;
241
251
  }
@@ -244,8 +254,9 @@ async function getServerSupportedProjects(serverDomain, ignoreCache = false, deb
244
254
  /** 部署一个目录 */
245
255
  async function finderDeploy(option) {
246
256
  const { dist, ignoreFiles, deployTo, user, key, debug, preview, commitLogs, ignoreCache } = option;
247
- if (!dist) throw colors.bgRed("部署参数 dist 缺失");
248
- if (!existsSync(resolve(dist)) || !lstatSync(resolve(dist)).isDirectory()) throw colors.bgRed("部署参数错误,dist 需要是一个存在的文件目录") + " " + colors.red(dist);
257
+ if (!dist) throw new Error("部署参数 dist 缺失");
258
+ if (!existsSync(resolve(dist)) || !lstatSync(resolve(dist)).isDirectory()) throw new Error(`部署参数错误,dist 需要是一个存在的文件目录 ${dist}`);
259
+ if (!user || !key) throw new Error("部署缺少认证信息(user & key)");
249
260
  const payload = commitLogs ? { 更新内容: commitLogs } : void 0;
250
261
  if (debug) console.log({
251
262
  method: "finderDeploy",
@@ -257,10 +268,10 @@ async function finderDeploy(option) {
257
268
  preview
258
269
  });
259
270
  const buffer = await compressToBuffer(dist, ignoreFiles, debug).catch((e) => {
260
- throw colors.bgRed("部署预处理之压缩代码失败") + " " + (e instanceof Error ? e.message : String(e));
271
+ throw new Error(`部署预处理之压缩代码失败 ${e instanceof Error ? e.message : String(e)}`);
261
272
  });
262
273
  if (Array.isArray(deployTo)) {
263
- const results = await Promise.all(deployTo.map((target) => {
274
+ const results = await Promise.allSettled(deployTo.map((target) => {
264
275
  return deploy({
265
276
  debug,
266
277
  target,
@@ -271,8 +282,21 @@ async function finderDeploy(option) {
271
282
  ignoreCache
272
283
  });
273
284
  }));
274
- const lastDeployResult = results[results.length - 1];
275
- if (lastDeployResult && lastDeployResult.previewUrl) doPreview(lastDeployResult.previewUrl, preview);
285
+ const succeeded = [];
286
+ const failed = [];
287
+ let lastPreviewUrl = "";
288
+ results.forEach((result, index) => {
289
+ const target = deployTo[index];
290
+ if (result.status === "fulfilled") {
291
+ succeeded.push(target);
292
+ if (result.value && result.value.previewUrl) lastPreviewUrl = result.value.previewUrl;
293
+ } else {
294
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
295
+ failed.push(`${target}(${reason})`);
296
+ }
297
+ });
298
+ if (lastPreviewUrl) doPreview(lastPreviewUrl, preview);
299
+ if (failed.length) throw new Error(`部分目标部署失败:${failed.join(";")}` + (succeeded.length ? `;成功:${succeeded.join(",")}` : ""));
276
300
  return deployTo.join(",");
277
301
  }
278
302
  const deployResult = await deploy({
@@ -301,10 +325,11 @@ function doPreview(defaultPreviewUrl, option) {
301
325
  /** 上传一个文件到 finder */
302
326
  async function finderUpload(option) {
303
327
  const { filePath, fileContent, deployTo, user, key, preview, debug, ignoreCache } = option;
304
- if (!filePath && !fileContent) throw colors.bgRed("部署缺少参数 filePath(文件全路径) 或 fileContent(文件内容)");
305
- if (filePath && !existsSync(filePath)) throw colors.bgRed("部署文件不存在(请确保传入完整文件路径)") + " " + colors.red(filePath);
306
- if (!deployTo) throw colors.bgRed("部署缺少参数 deployTo(部署目标)");
307
- const content = filePath ? Buffer.from(readFileSync(filePath)) : Buffer.isBuffer(fileContent) ? fileContent : Buffer.from(fileContent || "");
328
+ if (!filePath && !fileContent) throw new Error("部署缺少参数 filePath(文件全路径) 或 fileContent(文件内容)");
329
+ if (filePath && (!existsSync(filePath) || !statSync(filePath).isFile())) throw new Error(`部署文件不存在或不是文件(请确保传入完整文件路径) ${filePath}`);
330
+ if (!deployTo) throw new Error("部署缺少参数 deployTo(部署目标)");
331
+ if (!user || !key) throw new Error("部署缺少认证信息(user & key)");
332
+ const content = filePath ? readFileSync(filePath) : Buffer.isBuffer(fileContent) ? fileContent : Buffer.from(fileContent || "");
308
333
  const resp = await upload({
309
334
  debug,
310
335
  target: pure(deployTo),
@@ -318,9 +343,10 @@ async function finderUpload(option) {
318
343
  //#endregion
319
344
  //#region src/plugin.ts
320
345
  function viteDeployPlugin(option) {
346
+ const { onBeforeDeploy, onFinished, onError, ...deployOption } = option;
321
347
  let distDir = null;
322
348
  return {
323
- name: "finerDeployAgent",
349
+ name: "finderDeployAgent",
324
350
  generateBundle({ dir }) {
325
351
  distDir = process.cwd();
326
352
  if (dir) distDir = resolve(distDir, dir);
@@ -330,17 +356,17 @@ function viteDeployPlugin(option) {
330
356
  console.error(colors.bgRed("没有找到部署资源,请尝试检查 build 是否生成了正确的资源"));
331
357
  return;
332
358
  }
333
- await option.onBeforeDeploy?.(distDir);
359
+ await onBeforeDeploy?.(distDir);
334
360
  const result = await finderDeploy({
335
361
  preview: true,
336
- ...option,
362
+ ...deployOption,
337
363
  dist: distDir
338
- }).catch((e) => e instanceof Error ? e : typeof e === "string" ? new Error(e) : /* @__PURE__ */ new Error(e + ""));
364
+ }).catch((e) => e instanceof Error ? e : new Error(String(e)));
339
365
  if (result instanceof Error) {
340
- option.onError?.();
366
+ onError?.(result);
341
367
  console.log(colors.bgRed("部署失败"), colors.red(result.message));
342
368
  } else {
343
- option.onFinished?.();
369
+ onFinished?.();
344
370
  console.log(colors.bgGreen("部署成功"), colors.green(result || ""));
345
371
  }
346
372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seayoo-web/finder",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "agent for web finder",
5
5
  "license": "MIT",
6
6
  "author": "web@seayoo.com",
@@ -1,4 +1,4 @@
1
1
  /** 代码压缩 */
2
2
  export declare function compressToBuffer(sourceDir: string, ignoreFiles?: string[], debug?: boolean): Promise<Buffer>;
3
- export declare function getAllFiles(dir: string, ignores?: string[]): string[];
4
- export declare function isIgnoreFile(filePath: string, ignores: string[]): boolean;
3
+ export declare function getAllFiles(dir: string, ignores?: string[], root?: string): string[];
4
+ export declare function isIgnoreFile(filePath: string, ignores: string[], isDir?: boolean): boolean;
@@ -2,7 +2,7 @@ import { finderDeploy } from "./core";
2
2
  type FinderDeployVitePluginOption = Omit<Parameters<typeof finderDeploy>[0], "dist"> & {
3
3
  onBeforeDeploy?: (distDir: string) => unknown;
4
4
  onFinished?: () => unknown;
5
- onError?: () => unknown;
5
+ onError?: (error: Error) => unknown;
6
6
  };
7
7
  export declare function viteDeployPlugin(option: FinderDeployVitePluginOption): {
8
8
  name: string;
@@ -5,11 +5,12 @@ type RequestData = Record<string, string | number | {
5
5
  contentType: string;
6
6
  filename: string;
7
7
  }>;
8
- export declare function request({ url, method, headers, data, }: {
8
+ export declare function request({ url, method, headers, data, timeout, }: {
9
9
  url: string;
10
10
  method: "GET" | "POST";
11
11
  headers?: Record<string, string>;
12
12
  data?: RequestData;
13
+ timeout?: number;
13
14
  }): Promise<{
14
15
  status: number;
15
16
  message?: string;