@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 +111 -1
- package/dist/index.js +342 -342
- package/package.json +14 -14
- package/types/src/compress.d.ts +2 -2
- package/types/src/core.d.ts +0 -1
- package/types/src/plugin.d.ts +1 -1
- package/types/src/utils.d.ts +2 -1
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, {
|
|
2
|
-
import 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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
69
|
+
return url.replace(/(?:^https?:\/\/|\/*$)/gi, "");
|
|
61
70
|
}
|
|
62
71
|
function getSystemTempDir() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
205
|
-
|
|
197
|
+
var getFinderServerFullPath = function(domain) {
|
|
198
|
+
return (domain.endsWith("internal") ? "http://" : "https://") + domain;
|
|
206
199
|
};
|
|
207
200
|
async function findTargetServer(target, debug, ignoreCache) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
};
|
|
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
|
-
|
|
373
|
-
|
|
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.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "agent for web finder",
|
|
5
|
-
"
|
|
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
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
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": "^
|
|
31
|
-
"@seayoo-web/tsconfig": "^1.0.
|
|
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",
|
package/types/src/compress.d.ts
CHANGED
|
@@ -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;
|
package/types/src/core.d.ts
CHANGED
package/types/src/plugin.d.ts
CHANGED
|
@@ -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;
|
package/types/src/utils.d.ts
CHANGED
|
@@ -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;
|