@seayoo-web/finder 2.2.2 → 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,376 +1,376 @@
1
- import fs, { readdirSync, lstatSync, existsSync, readFileSync, writeFileSync } from "fs";
2
- import path, { relative, join, basename, normalize, sep, dirname, resolve } from "path";
1
+ import fs, { existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2
+ import path, { basename, dirname, join, normalize, relative, resolve, sep } from "path";
3
3
  import open from "open";
4
- import "colors";
5
4
  import { zip } from "compressing";
5
+ import colors from "picocolors";
6
6
  import os from "os";
7
- const presetIgnores = ["node_modules/", ".git/", ".vscode/", "__MACOSX/", ".DS_Store", ".gitkeep"];
7
+ //#region src/compress.ts
8
+ var presetIgnores = [
9
+ "node_modules/",
10
+ ".git/",
11
+ ".vscode/",
12
+ "__MACOSX/",
13
+ ".DS_Store",
14
+ ".gitkeep"
15
+ ];
16
+ /** 代码压缩 */
8
17
  function compressToBuffer(sourceDir, ignoreFiles, debug) {
9
- const ignoreFileList = [...presetIgnores, ...ignoreFiles || []];
10
- const filesToCompress = getAllFiles(sourceDir, ignoreFileList);
11
- const zipStream = new zip.Stream();
12
- filesToCompress.forEach((file) => {
13
- zipStream.addEntry(file, { relativePath: relative(sourceDir, file) });
14
- });
15
- if (debug) {
16
- console.log({
17
- method: "compressToBuffer",
18
- sourceDir,
19
- ignores: ignoreFileList,
20
- filesCount: filesToCompress.length
21
- });
22
- }
23
- const chunks = [];
24
- return new Promise(function(resolve2, reject) {
25
- zipStream.on("data", (chunk) => chunks.push(chunk)).on("end", () => resolve2(Buffer.concat(chunks))).on("error", reject);
26
- });
18
+ const ignoreFileList = [...presetIgnores, ...ignoreFiles || []];
19
+ const filesToCompress = getAllFiles(sourceDir, ignoreFileList);
20
+ const zipStream = new zip.Stream();
21
+ filesToCompress.forEach((file) => {
22
+ zipStream.addEntry(file, { relativePath: relative(sourceDir, file) });
23
+ });
24
+ if (debug) console.log({
25
+ method: "compressToBuffer",
26
+ sourceDir,
27
+ ignores: ignoreFileList,
28
+ filesCount: filesToCompress.length
29
+ });
30
+ const chunks = [];
31
+ return new Promise(function(resolve, reject) {
32
+ zipStream.on("data", (chunk) => chunks.push(chunk)).on("end", () => resolve(Buffer.concat(chunks))).on("error", reject);
33
+ });
27
34
  }
28
- function getAllFiles(dir, ignores = []) {
29
- const list = [];
30
- readdirSync(dir).forEach((file) => {
31
- const filePath = join(dir, file);
32
- const stats = lstatSync(filePath);
33
- if (stats.isDirectory()) {
34
- list.push(...getAllFiles(filePath, ignores));
35
- } else if (!isIgnoreFile(filePath, ignores)) {
36
- list.push(filePath);
37
- }
38
- });
39
- return list;
35
+ function getAllFiles(dir, ignores = [], root = dir) {
36
+ const list = [];
37
+ readdirSync(dir).forEach((file) => {
38
+ const filePath = join(dir, file);
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);
44
+ });
45
+ return list;
40
46
  }
41
- function isIgnoreFile(filePath, ignores) {
42
- const filename = basename(filePath);
43
- const dirs = normalize(filePath).split(sep);
44
- return ignores.some((pattern) => {
45
- if (pattern.endsWith("/")) {
46
- return pattern.includes("*") ? dirs.some((dir) => getRegexp(pattern.slice(0, -1)).test(dir)) : dirs.includes(pattern.slice(0, -1));
47
- }
48
- return pattern.includes("*") ? getRegexp(pattern).test(filename) : filename === pattern;
49
- });
47
+ function isIgnoreFile(filePath, ignores, isDir = false) {
48
+ const filename = basename(filePath);
49
+ const segments = normalize(filePath).split(sep);
50
+ const dirSegments = isDir ? segments : segments.slice(0, -1);
51
+ return ignores.some((pattern) => {
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;
57
+ return pattern.includes("*") ? getRegexp(pattern).test(filename) : filename === pattern;
58
+ });
50
59
  }
51
- const pathRegCache = {};
52
- const getRegexp = function(path2) {
53
- if (path2 && pathRegCache[path2]) {
54
- return pathRegCache[path2];
55
- }
56
- const fixRegStr = path2.replace(/([\\(){}\[\]\^\$\+\-\?\.|])/g, "\\$1").replace(/\*{1,}/g, ".*");
57
- return pathRegCache[path2] = new RegExp("^" + fixRegStr + "$");
60
+ var pathRegCache = {};
61
+ var getRegexp = function(path) {
62
+ if (path && pathRegCache[path]) return pathRegCache[path];
63
+ const fixRegStr = path.replace(/([\\(){}[\]^$+\-?.|])/g, "\\$1").replace(/\*{1,}/g, ".*");
64
+ return pathRegCache[path] = new RegExp("^" + fixRegStr + "$");
58
65
  };
66
+ //#endregion
67
+ //#region src/utils.ts
59
68
  function pure(url) {
60
- return url.replace(/(?:^https?:\/\/|\/*$)/gi, "");
69
+ return url.replace(/(?:^https?:\/\/|\/*$)/gi, "");
61
70
  }
62
71
  function getSystemTempDir() {
63
- const dir = path.resolve(os.tmpdir(), "webfinder");
64
- if (!fs.existsSync(dir)) {
65
- fs.mkdirSync(dir, { recursive: true });
66
- }
67
- return dir;
72
+ const dir = path.resolve(os.tmpdir(), "webfinder");
73
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
74
+ return dir;
68
75
  }
69
- const nodeVersion = +process.version.replace(/\..+/, "").slice(1);
70
- if (nodeVersion < 20) {
71
- process.emitWarning = function() {
72
- };
76
+ /** 单次请求默认超时(毫秒),避免 finder 服务器无响应时部署无限挂起 */
77
+ var DEFAULT_REQUEST_TIMEOUT = 12e4;
78
+ async function request({ url, method, headers, data, timeout = DEFAULT_REQUEST_TIMEOUT }) {
79
+ const hasFileUpload = method === "POST" && data && Object.values(data).some((value) => typeof value === "object" && "buffer" in value);
80
+ const reqHeaders = new Headers();
81
+ if (headers) Object.entries(headers).forEach(([key, value]) => {
82
+ reqHeaders.set(key, value);
83
+ });
84
+ const requestInit = {
85
+ method,
86
+ headers: reqHeaders,
87
+ signal: AbortSignal.timeout(timeout)
88
+ };
89
+ try {
90
+ if (data) if (hasFileUpload) {
91
+ const formData = new FormData();
92
+ Object.entries(data).forEach(([key, value]) => {
93
+ if (typeof value === "object" && "buffer" in value) {
94
+ const { buffer, filename, contentType } = value;
95
+ const blob = new Blob([new Uint8Array(buffer)], { type: contentType });
96
+ formData.append(key, blob, filename);
97
+ } else formData.append(key, String(value));
98
+ });
99
+ requestInit.headers = reqHeaders;
100
+ requestInit.body = formData;
101
+ } else {
102
+ reqHeaders.set("content-type", reqHeaders.get("content-type") || "application/json");
103
+ requestInit.headers = reqHeaders;
104
+ requestInit.body = JSON.stringify(data);
105
+ }
106
+ const response = await fetch(url, requestInit);
107
+ let responseData;
108
+ if (response.headers.get("content-type")?.includes("application/json")) responseData = await response.json();
109
+ else responseData = await response.text();
110
+ return {
111
+ status: response.status,
112
+ message: response.statusText,
113
+ data: responseData
114
+ };
115
+ } catch (err) {
116
+ return {
117
+ status: 500,
118
+ message: err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") ? `请求超时(${timeout}ms):${url}` : err instanceof Error ? err.message : String(err),
119
+ data: null
120
+ };
121
+ }
73
122
  }
74
- async function request({
75
- url,
76
- method,
77
- headers,
78
- data
79
- }) {
80
- const hasFileUpload = method === "POST" && data && Object.values(data).some((value) => typeof value === "object" && "buffer" in value);
81
- const reqHeaders = new Headers();
82
- if (headers) {
83
- Object.entries(headers).forEach(([key, value]) => {
84
- reqHeaders.set(key, value);
85
- });
86
- }
87
- const requestInit = { method, headers: reqHeaders };
88
- try {
89
- if (data) {
90
- if (hasFileUpload) {
91
- const formData = new FormData();
92
- Object.entries(data).forEach(([key, value]) => {
93
- if (typeof value === "object" && "buffer" in value) {
94
- const { buffer, filename, contentType: contentType2 } = value;
95
- const blob = new Blob([new Uint8Array(buffer)], { type: contentType2 });
96
- formData.append(key, blob, filename);
97
- } else {
98
- formData.append(key, String(value));
99
- }
100
- });
101
- requestInit.headers = reqHeaders;
102
- requestInit.body = formData;
103
- } else {
104
- reqHeaders.set("content-type", reqHeaders.get("content-type") || "application/json");
105
- requestInit.headers = reqHeaders;
106
- requestInit.body = JSON.stringify(data);
107
- }
108
- }
109
- const response = await fetch(url, requestInit);
110
- let responseData;
111
- const contentType = response.headers.get("content-type");
112
- if (contentType?.includes("application/json")) {
113
- responseData = await response.json();
114
- } else {
115
- responseData = await response.text();
116
- }
117
- return {
118
- status: response.status,
119
- message: response.statusText,
120
- data: responseData
121
- };
122
- } catch (err) {
123
- return {
124
- status: 500,
125
- message: err instanceof Error ? err.message : String(err),
126
- data: null
127
- };
128
- }
129
- }
130
- const FinderServers = {
131
- "finder.seayoo.io": [],
132
- "finder.seayoo.com": [],
133
- "finder.seayoo.internal": [],
134
- "finder.dev.seayoo.com": [],
135
- "finder.dev.seayoo.io": []
123
+ //#endregion
124
+ //#region src/service.ts
125
+ /** finder 服务器列表以及所支持的域名 */
126
+ var FinderServers = {
127
+ "finder.seayoo.io": [],
128
+ "finder.seayoo.com": [],
129
+ "finder.seayoo.internal": [],
130
+ "finder.dev.seayoo.com": [],
131
+ "finder.dev.seayoo.io": []
136
132
  };
137
- const FinderApiPaths = {
138
- deploy: "/service/deploy",
139
- inspect: "/inspect/supported/projects",
140
- upload: "/service/upload"
133
+ /** finder api path */
134
+ var FinderApiPaths = {
135
+ deploy: "/service/deploy",
136
+ inspect: "/inspect/supported/projects",
137
+ upload: "/service/upload"
141
138
  };
139
+ /** 将指定的 zip buffer 部署到指定目录 */
142
140
  async function deploy(option) {
143
- const { debug, target, buffer, user, key, payload, ignoreCache } = option;
144
- const targetServer = await findTargetServer(target, debug, ignoreCache);
145
- if (!targetServer) {
146
- throw `finder不支持该域名部署,请检查 ${target}`.bgRed;
147
- }
148
- if (!user || !key) {
149
- throw `部署缺少认证信息(user & key)`.bgRed;
150
- }
151
- const zipMockName = `${Date.now()}${Math.random().toString(16).slice(-3)}.zip`;
152
- const { status, data } = await request({
153
- url: `${getFinderServerFullPath(targetServer)}${FinderApiPaths.deploy}?target=${encodeURIComponent(pure(target))}`,
154
- method: "POST",
155
- headers: { user, key },
156
- data: {
157
- path: zipMockName,
158
- file: { buffer, filename: zipMockName, contentType: "application/octet-stream" },
159
- payload: payload ? JSON.stringify(payload) : ""
160
- }
161
- });
162
- if (status !== 200) {
163
- throw `部署接口错误,Server: ${targetServer},Status: ${status},Response: ${JSON.stringify(data)}`.red;
164
- }
165
- if (!data || typeof data !== "object" || "err" in data && data.err || !("data" in data) || typeof data.data !== "string") {
166
- throw `部署接口响应错误。Server: ${targetServer},Response: ${JSON.stringify(data)}`.red;
167
- }
168
- const url = data.data;
169
- if (debug) {
170
- console.log("部署完毕,接口返回内容", data);
171
- }
172
- return {
173
- previewUrl: url.endsWith("/") ? url.replace(/\/*$/, "/") + "index.html?" + Math.random().toString(16).slice(2) : url.startsWith("http") ? url : ""
174
- };
141
+ const { debug, target, buffer, user, key, payload, ignoreCache } = option;
142
+ const targetServer = await findTargetServer(target, debug, ignoreCache);
143
+ if (!targetServer) throw new Error(`finder不支持该域名部署,请检查 ${target}`);
144
+ if (!user || !key) throw new Error("部署缺少认证信息(user & key)");
145
+ const zipMockName = `${Date.now()}${Math.random().toString(16).slice(-3)}.zip`;
146
+ const { status, data } = await request({
147
+ url: `${getFinderServerFullPath(targetServer)}${FinderApiPaths.deploy}?target=${encodeURIComponent(pure(target))}`,
148
+ method: "POST",
149
+ headers: {
150
+ user,
151
+ key
152
+ },
153
+ data: {
154
+ path: zipMockName,
155
+ file: {
156
+ buffer,
157
+ filename: zipMockName,
158
+ contentType: "application/octet-stream"
159
+ },
160
+ payload: payload ? JSON.stringify(payload) : ""
161
+ }
162
+ });
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)}`);
165
+ const url = data.data;
166
+ if (debug) console.log("部署完毕,接口返回内容", data);
167
+ return { previewUrl: url.endsWith("/") ? url.replace(/\/*$/, "/") + "index.html?" + Math.random().toString(16).slice(2) : url.startsWith("http") ? url : "" };
175
168
  }
169
+ /** 将文件上传到指定位置 */
176
170
  async function upload(option) {
177
- const { debug, target, buffer, user, key, ignoreCache } = option;
178
- const targetServer = await findTargetServer(target, debug, ignoreCache);
179
- if (!targetServer) {
180
- throw `finder不支持该域名部署,请检查 ${target}`.bgRed;
181
- }
182
- if (!user || !key) {
183
- throw `部署缺少认证信息(user & key)`.bgRed;
184
- }
185
- const filename = basename(target);
186
- const deployTarget = dirname(pure(target));
187
- const { status, data } = await request({
188
- url: `${getFinderServerFullPath(targetServer)}${FinderApiPaths.upload}?target=${encodeURIComponent(deployTarget)}`,
189
- method: "POST",
190
- headers: { user, key },
191
- data: {
192
- path: filename,
193
- file: { buffer, filename, contentType: "application/octet-stream" }
194
- }
195
- });
196
- if (status !== 200) {
197
- throw `上传接口错误,Server: ${targetServer},Status: ${status}`.red;
198
- }
199
- if (!data || typeof data !== "object" || "err" in data && data.err || !("data" in data) || typeof data.data !== "string") {
200
- throw `上传接口响应错误,Server: ${targetServer},Response: ${JSON.stringify(data)}`.red;
201
- }
202
- return { previewUrl: `https://${pure(target)}` };
171
+ const { debug, target, buffer, user, key, ignoreCache } = option;
172
+ const targetServer = await findTargetServer(target, debug, ignoreCache);
173
+ if (!targetServer) throw new Error(`finder不支持该域名部署,请检查 ${target}`);
174
+ if (!user || !key) throw new Error("部署缺少认证信息(user & key)");
175
+ const filename = basename(target);
176
+ const deployTarget = dirname(pure(target));
177
+ const { status, data } = await request({
178
+ url: `${getFinderServerFullPath(targetServer)}${FinderApiPaths.upload}?target=${encodeURIComponent(deployTarget)}`,
179
+ method: "POST",
180
+ headers: {
181
+ user,
182
+ key
183
+ },
184
+ data: {
185
+ path: filename,
186
+ file: {
187
+ buffer,
188
+ filename,
189
+ contentType: "application/octet-stream"
190
+ }
191
+ }
192
+ });
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)}`);
195
+ return { previewUrl: `https://${pure(target)}` };
203
196
  }
204
- const getFinderServerFullPath = function(domain) {
205
- return (domain.endsWith("internal") ? "http://" : "https://") + domain;
197
+ var getFinderServerFullPath = function(domain) {
198
+ return (domain.endsWith("internal") ? "http://" : "https://") + domain;
206
199
  };
207
200
  async function findTargetServer(target, debug, ignoreCache) {
208
- const t = pure(target);
209
- await updateSupportedProjects(!!ignoreCache, debug);
210
- for (const domain in FinderServers) {
211
- if (FinderServers[domain].find((url) => t.startsWith(url))) {
212
- return domain;
213
- }
214
- }
215
- if (!ignoreCache) {
216
- await updateSupportedProjects(true, debug);
217
- for (const domain in FinderServers) {
218
- if (FinderServers[domain].find((url) => t.startsWith(url))) {
219
- return domain;
220
- }
221
- }
222
- }
223
- return null;
201
+ const t = pure(target);
202
+ await updateSupportedProjects(!!ignoreCache, debug);
203
+ for (const domain in FinderServers) if (FinderServers[domain].find((url) => url && t.startsWith(url))) return domain;
204
+ if (!ignoreCache) {
205
+ await updateSupportedProjects(true, debug);
206
+ for (const domain in FinderServers) if (FinderServers[domain].find((url) => url && t.startsWith(url))) return domain;
207
+ }
208
+ return null;
224
209
  }
225
210
  async function updateSupportedProjects(force = false, debug) {
226
- const domains = Object.keys(FinderServers);
227
- for (const domain of domains) {
228
- FinderServers[domain] = await getServerSupportedProjects(domain, force, debug) || [];
229
- }
211
+ const domains = Object.keys(FinderServers);
212
+ await Promise.all(domains.map((domain) => getServerSupportedProjects(domain, force, debug).then((r) => FinderServers[domain] = r, () => FinderServers[domain] = [])));
230
213
  }
231
214
  async function getServerSupportedProjects(serverDomain, ignoreCache = false, debug) {
232
- const cacheFile = resolve(getSystemTempDir(), `${serverDomain}.json`);
233
- if (existsSync(cacheFile) && !ignoreCache) {
234
- try {
235
- const cache = JSON.parse(readFileSync(cacheFile).toString());
236
- if (Array.isArray(cache) && cache.every((d) => typeof d === "string")) {
237
- if (debug) {
238
- console.log({ method: "getServerSupportedProjects", serverDomain, cache });
239
- }
240
- return cache;
241
- }
242
- } catch (e) {
243
- console.error("ReadFinderCacheError", e);
244
- }
245
- }
246
- const inspectURL = `${getFinderServerFullPath(serverDomain)}${FinderApiPaths.inspect}`;
247
- const { status, message, data } = await request({
248
- url: inspectURL,
249
- method: "GET",
250
- headers: { "user-agent": `web finder agent v2` }
251
- });
252
- if (status !== 200) {
253
- if (debug) {
254
- console.error(`服务器 ${inspectURL} 检查接口错误`.bgRed, (message || "").red);
255
- }
256
- return [];
257
- }
258
- if (!Array.isArray(data) || !data.every((d) => typeof d === "string")) {
259
- console.error(`服务器 ${inspectURL} 接口返回内容错误`.bgRed, JSON.stringify(data).red);
260
- return [];
261
- }
262
- if (debug) {
263
- console.log({ method: "getServerSupportedProjects", serverDomain, list: data });
264
- }
265
- const pureList = data.map(pure);
266
- writeFileSync(cacheFile, JSON.stringify(pureList));
267
- return pureList;
215
+ const cacheFile = resolve(getSystemTempDir(), `${serverDomain}.json`);
216
+ if (existsSync(cacheFile) && !ignoreCache) try {
217
+ const cache = JSON.parse(readFileSync(cacheFile).toString());
218
+ if (Array.isArray(cache) && cache.every((d) => typeof d === "string")) {
219
+ if (debug) console.log({
220
+ method: "getServerSupportedProjects",
221
+ serverDomain,
222
+ cache
223
+ });
224
+ return cache;
225
+ }
226
+ } catch (e) {
227
+ console.error(colors.bgRed("ReadFinderCacheError"), e);
228
+ }
229
+ const inspectURL = `${getFinderServerFullPath(serverDomain)}${FinderApiPaths.inspect}`;
230
+ const { status, message, data } = await request({
231
+ url: inspectURL,
232
+ method: "GET",
233
+ headers: { "user-agent": `web finder agent v2` }
234
+ });
235
+ if (status !== 200) {
236
+ if (debug) console.error(colors.bgRed(`服务器 ${inspectURL} 检查接口错误`), colors.red(message || ""));
237
+ return [];
238
+ }
239
+ if (!Array.isArray(data) || !data.every((d) => typeof d === "string")) {
240
+ console.error(colors.bgRed(`服务器 ${inspectURL} 接口返回内容错误`), colors.red(JSON.stringify(data)));
241
+ return [];
242
+ }
243
+ if (debug) console.log({
244
+ method: "getServerSupportedProjects",
245
+ serverDomain,
246
+ list: data
247
+ });
248
+ const pureList = data.map(pure).filter(Boolean);
249
+ writeFileSync(cacheFile, JSON.stringify(pureList));
250
+ return pureList;
268
251
  }
252
+ //#endregion
253
+ //#region src/core.ts
254
+ /** 部署一个目录 */
269
255
  async function finderDeploy(option) {
270
- const { dist, ignoreFiles, deployTo, user, key, debug, preview, commitLogs, ignoreCache } = option;
271
- if (!dist) {
272
- throw "部署参数 dist 缺失".bgRed;
273
- }
274
- if (!existsSync(resolve(dist)) || !lstatSync(resolve(dist)).isDirectory()) {
275
- throw "部署参数错误,dist 需要是一个存在的文件目录".bgRed + " " + dist.red;
276
- }
277
- const payload = commitLogs ? { 更新内容: commitLogs } : void 0;
278
- if (debug) {
279
- console.log({
280
- method: "finderDeploy",
281
- dist,
282
- deployTo,
283
- ignoreFiles,
284
- payload,
285
- user,
286
- preview
287
- });
288
- }
289
- const buffer = await compressToBuffer(dist, ignoreFiles, debug).catch((e) => {
290
- throw "部署预处理之压缩代码失败".bgRed + " " + (e instanceof Error ? e.message : String(e));
291
- });
292
- if (Array.isArray(deployTo)) {
293
- const results = await Promise.all(
294
- deployTo.map((target) => {
295
- return deploy({ debug, target, buffer, user, key, payload, ignoreCache });
296
- })
297
- );
298
- const lastDeployResult = results[results.length - 1];
299
- if (lastDeployResult && lastDeployResult.previewUrl) {
300
- doPreview(lastDeployResult.previewUrl, preview);
301
- }
302
- return deployTo.join(",");
303
- }
304
- const deployResult = await deploy({ debug, target: deployTo, buffer, user, key, payload, ignoreCache });
305
- if (deployResult && deployResult.previewUrl) {
306
- doPreview(deployResult.previewUrl, preview);
307
- }
308
- return deployTo;
256
+ const { dist, ignoreFiles, deployTo, user, key, debug, preview, commitLogs, ignoreCache } = option;
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)");
260
+ const payload = commitLogs ? { 更新内容: commitLogs } : void 0;
261
+ if (debug) console.log({
262
+ method: "finderDeploy",
263
+ dist,
264
+ deployTo,
265
+ ignoreFiles,
266
+ payload,
267
+ user,
268
+ preview
269
+ });
270
+ const buffer = await compressToBuffer(dist, ignoreFiles, debug).catch((e) => {
271
+ throw new Error(`部署预处理之压缩代码失败 ${e instanceof Error ? e.message : String(e)}`);
272
+ });
273
+ if (Array.isArray(deployTo)) {
274
+ const results = await Promise.allSettled(deployTo.map((target) => {
275
+ return deploy({
276
+ debug,
277
+ target,
278
+ buffer,
279
+ user,
280
+ key,
281
+ payload,
282
+ ignoreCache
283
+ });
284
+ }));
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(",")}` : ""));
300
+ return deployTo.join(",");
301
+ }
302
+ const deployResult = await deploy({
303
+ debug,
304
+ target: deployTo,
305
+ buffer,
306
+ user,
307
+ key,
308
+ payload,
309
+ ignoreCache
310
+ });
311
+ if (deployResult && deployResult.previewUrl) doPreview(deployResult.previewUrl, preview);
312
+ return deployTo;
309
313
  }
310
314
  function doPreview(defaultPreviewUrl, option) {
311
- if (!option) {
312
- return;
313
- }
314
- if (option === true) {
315
- open(defaultPreviewUrl);
316
- return;
317
- }
318
- const base = defaultPreviewUrl.replace(/index\.html.*$/i, "");
319
- const files = Array.isArray(option) ? option : [option];
320
- files.forEach((file) => {
321
- open(base + file);
322
- });
315
+ if (!option) return;
316
+ if (option === true) {
317
+ open(defaultPreviewUrl);
318
+ return;
319
+ }
320
+ const base = defaultPreviewUrl.replace(/index\.html.*$/i, "");
321
+ (Array.isArray(option) ? option : [option]).forEach((file) => {
322
+ open(base + file);
323
+ });
323
324
  }
325
+ /** 上传一个文件到 finder */
324
326
  async function finderUpload(option) {
325
- const { filePath, fileContent, deployTo, user, key, preview, debug, ignoreCache } = option;
326
- if (!filePath && !fileContent) {
327
- throw `部署缺少参数 filePath(文件全路径) fileContent(文件内容)`.bgRed;
328
- }
329
- if (filePath && !existsSync(filePath)) {
330
- throw `部署文件不存在(请确保传入完整文件路径)`.bgRed + " " + filePath;
331
- }
332
- if (!deployTo) {
333
- throw `部署缺少参数 deployTo(部署目标)`.bgRed;
334
- }
335
- const content = filePath ? Buffer.from(readFileSync(filePath)) : Buffer.isBuffer(fileContent) ? fileContent : Buffer.from(fileContent || "");
336
- const resp = await upload({ debug, target: pure(deployTo), buffer: content, user, key, ignoreCache });
337
- if (preview && resp.previewUrl) {
338
- open(resp.previewUrl);
339
- }
327
+ const { filePath, fileContent, deployTo, user, key, preview, debug, ignoreCache } = option;
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 || "");
333
+ const resp = await upload({
334
+ debug,
335
+ target: pure(deployTo),
336
+ buffer: content,
337
+ user,
338
+ key,
339
+ ignoreCache
340
+ });
341
+ if (preview && resp.previewUrl) open(resp.previewUrl);
340
342
  }
343
+ //#endregion
344
+ //#region src/plugin.ts
341
345
  function viteDeployPlugin(option) {
342
- let distDir = null;
343
- return {
344
- name: "finerDeployAgent",
345
- generateBundle({ dir }) {
346
- distDir = process.cwd();
347
- if (dir) {
348
- distDir = resolve(distDir, dir);
349
- }
350
- },
351
- async closeBundle() {
352
- if (!distDir) {
353
- console.error("没有找到部署资源,请尝试检查 build 是否生成了正确的资源".bgRed);
354
- return;
355
- }
356
- await option.onBeforeDeploy?.(distDir);
357
- const result = await finderDeploy({
358
- preview: true,
359
- ...option,
360
- dist: distDir
361
- }).catch((e) => e instanceof Error ? e : typeof e === "string" ? new Error(e) : new Error(e + ""));
362
- if (result instanceof Error) {
363
- option.onError?.();
364
- console.log("部署失败".bgRed, result.message);
365
- } else {
366
- option.onFinished?.();
367
- console.log("部署成功".bgGreen, (result || "").green);
368
- }
369
- }
370
- };
346
+ const { onBeforeDeploy, onFinished, onError, ...deployOption } = option;
347
+ let distDir = null;
348
+ return {
349
+ name: "finderDeployAgent",
350
+ generateBundle({ dir }) {
351
+ distDir = process.cwd();
352
+ if (dir) distDir = resolve(distDir, dir);
353
+ },
354
+ async closeBundle() {
355
+ if (!distDir) {
356
+ console.error(colors.bgRed("没有找到部署资源,请尝试检查 build 是否生成了正确的资源"));
357
+ return;
358
+ }
359
+ await onBeforeDeploy?.(distDir);
360
+ const result = await finderDeploy({
361
+ preview: true,
362
+ ...deployOption,
363
+ dist: distDir
364
+ }).catch((e) => e instanceof Error ? e : new Error(String(e)));
365
+ if (result instanceof Error) {
366
+ onError?.(result);
367
+ console.log(colors.bgRed("部署失败"), colors.red(result.message));
368
+ } else {
369
+ onFinished?.();
370
+ console.log(colors.bgGreen("部署成功"), colors.green(result || ""));
371
+ }
372
+ }
373
+ };
371
374
  }
372
- export {
373
- finderDeploy,
374
- finderUpload,
375
- viteDeployPlugin
376
- };
375
+ //#endregion
376
+ export { finderDeploy, finderUpload, viteDeployPlugin };
package/package.json CHANGED
@@ -1,34 +1,34 @@
1
1
  {
2
2
  "name": "@seayoo-web/finder",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "agent for web finder",
5
- "type": "module",
5
+ "license": "MIT",
6
+ "author": "web@seayoo.com",
6
7
  "source": "index.ts",
7
- "main": "./dist/index.js",
8
- "module": "./dist/index.js",
9
- "types": "./types/index.d.ts",
10
8
  "files": [
11
9
  "dist",
12
10
  "types",
13
11
  "README.md"
14
12
  ],
15
- "engines": {
16
- "node": ">=22"
17
- },
18
- "author": "web@seayoo.com",
19
- "license": "MIT",
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.js",
16
+ "types": "./types/index.d.ts",
20
17
  "publishConfig": {
21
18
  "access": "public"
22
19
  },
23
20
  "dependencies": {
24
- "colors": "^1.4.0",
25
21
  "compressing": "^1.10.1",
26
- "open": "^10.1.0"
22
+ "open": "^10.1.0",
23
+ "picocolors": "^1.1.1"
27
24
  },
28
25
  "devDependencies": {
29
26
  "@types/node": "^22.13.1",
30
- "vitest": "^3.0.5",
31
- "@seayoo-web/tsconfig": "^1.0.5"
27
+ "vitest": "^4.1.4",
28
+ "@seayoo-web/tsconfig": "^1.0.6"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "vite build && tsc --emitDeclarationOnly",
@@ -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;
@@ -1,4 +1,3 @@
1
- import "colors";
2
1
  /** 部署一个目录 */
3
2
  export declare function finderDeploy(option: {
4
3
  /** 需要推送的代码目录 */
@@ -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;