@mbws/plugin-sdk 0.2.3 → 0.3.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/dist/caps.d.ts +97 -0
- package/dist/caps.js +24 -0
- package/dist/client.d.ts +57 -7
- package/dist/client.js +109 -17
- package/dist/config.d.ts +24 -5
- package/dist/config.js +74 -7
- package/dist/dev-mock.d.ts +1 -7
- package/dist/dev-mock.js +142 -13
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +51 -1
- package/dist/manifest.js +55 -4
- package/dist/protocol.d.ts +10 -3
- package/dist/protocol.js +12 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +372 -9
- package/package.json +2 -1
package/dist/vite.js
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*
|
|
15
15
|
* vite 只进 devDependencies 供类型与测试(type-only import),SDK 产物不依赖 vite。
|
|
16
16
|
*/
|
|
17
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
18
|
+
import crypto from "node:crypto";
|
|
17
19
|
import fs from "node:fs";
|
|
18
20
|
import path from "node:path";
|
|
19
|
-
import { buildManifest } from "./config.js";
|
|
21
|
+
import { buildManifest, resolveAttachmentRefs } from "./config.js";
|
|
20
22
|
import { generateContractTypes } from "./schema-types.js";
|
|
21
23
|
// 契约类型生成经本子路径暴露(Node 端工具链用;依赖 prettier,不进根入口)
|
|
22
24
|
export { generateContractTypes } from "./schema-types.js";
|
|
@@ -47,8 +49,31 @@ export function mbwsVitePlugin(opts) {
|
|
|
47
49
|
const generateTypes = opts.generateTypes ?? true;
|
|
48
50
|
// 工程 root:config 钩子里记(用户显式 root 优先),closeBundle 写产物时用
|
|
49
51
|
let projectRoot = process.cwd();
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
+
// 附件声明的解析缓存(dev 会话内;build 每次新鲜解析防用陈旧 hash 出产物)
|
|
53
|
+
let resolvedAttachmentsCache = null;
|
|
54
|
+
const ensureAttachments = async () => {
|
|
55
|
+
const refs = config.attachments ?? [];
|
|
56
|
+
if (refs.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
if (resolvedAttachmentsCache)
|
|
59
|
+
return resolvedAttachmentsCache;
|
|
60
|
+
resolvedAttachmentsCache = await resolveAttachmentRefs(refs, resolveApiBase(config));
|
|
61
|
+
return resolvedAttachmentsCache;
|
|
62
|
+
};
|
|
63
|
+
// manifest 是纯派生物,构建/供给共用同一序列化姿势(尾换行对齐文件写习惯)。
|
|
64
|
+
// dev 附件解析失败时降级空附件出 manifest(dev-mock 不依赖它),build 不允许
|
|
65
|
+
const manifestJson = async (dev) => {
|
|
66
|
+
try {
|
|
67
|
+
const decls = await ensureAttachments();
|
|
68
|
+
return `${JSON.stringify(buildManifest(config, { resolvedAttachments: decls }), null, 2)}\n`;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (dev) {
|
|
72
|
+
return `${JSON.stringify(buildManifest(config, { allowEmptyAttachments: true }), null, 2)}\n`;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
52
77
|
// 解包形态目录 <id>@<version>/:静态产物与 manifest 同目录(CI 的 tar -C 按它整目录打包)
|
|
53
78
|
const distDir = () => path.resolve(projectRoot, `dist/${config.id}@${config.version}`);
|
|
54
79
|
return {
|
|
@@ -76,7 +101,10 @@ export function mbwsVitePlugin(opts) {
|
|
|
76
101
|
buildDefaults.outDir = distDir();
|
|
77
102
|
if (!userConfig.build?.rollupOptions?.input) {
|
|
78
103
|
buildDefaults.rollupOptions = {
|
|
79
|
-
input: [
|
|
104
|
+
input: [
|
|
105
|
+
path.resolve(projectRoot, "panel.html"),
|
|
106
|
+
path.resolve(projectRoot, "viewer.html"),
|
|
107
|
+
],
|
|
80
108
|
};
|
|
81
109
|
}
|
|
82
110
|
if (Object.keys(buildDefaults).length > 0)
|
|
@@ -104,8 +132,38 @@ export function mbwsVitePlugin(opts) {
|
|
|
104
132
|
if (pathname === "/manifest.json") {
|
|
105
133
|
// dev 虚拟供给:SDK fetchContract 的 manifest 兜底读取在 dev/产物两形态行为一致
|
|
106
134
|
res.setHeader("content-type", "application/json");
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
return manifestJson(true)
|
|
136
|
+
.then((body) => {
|
|
137
|
+
res.end(body);
|
|
138
|
+
})
|
|
139
|
+
.catch(next);
|
|
140
|
+
}
|
|
141
|
+
if (pathname === "/__mbws__/dev/exec") {
|
|
142
|
+
if (req.method !== "POST") {
|
|
143
|
+
res.statusCode = 405;
|
|
144
|
+
res.end();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// dev 真执行:Node 侧 spawn 附件(本地覆盖优先,否则按声明自动下载官方附件库)
|
|
148
|
+
return handleDevExec(config, projectRoot, req, res).catch(next);
|
|
149
|
+
}
|
|
150
|
+
if (pathname === "/__mbws__/dev/localize") {
|
|
151
|
+
const ref = new URL(req.url ?? "/", "http://dev").searchParams.get("ref") ?? "";
|
|
152
|
+
if (!ref) {
|
|
153
|
+
res.statusCode = 400;
|
|
154
|
+
res.end();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return handleDevLocalize(projectRoot, res, ref).catch(next);
|
|
158
|
+
}
|
|
159
|
+
if (pathname === "/__mbws__/dev/file") {
|
|
160
|
+
const relPath = new URL(req.url ?? "/", "http://dev").searchParams.get("path") ?? "";
|
|
161
|
+
if (!relPath) {
|
|
162
|
+
res.statusCode = 400;
|
|
163
|
+
res.end();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
return handleDevFile(projectRoot, req, res, relPath).catch(next);
|
|
109
167
|
}
|
|
110
168
|
if (pathname.startsWith(`${DEV_CONTRACT_BASE}/api`)) {
|
|
111
169
|
// 转发异步进行,失败交 next(connect 约定);返回 promise 供测试 await
|
|
@@ -115,12 +173,15 @@ export function mbwsVitePlugin(opts) {
|
|
|
115
173
|
return undefined;
|
|
116
174
|
});
|
|
117
175
|
},
|
|
118
|
-
closeBundle() {
|
|
119
|
-
// 产物直出解包形态目录 <id>@<version>/,桌面 MBWS_DEV_PLUGIN_DIR 指向 dist
|
|
176
|
+
async closeBundle() {
|
|
177
|
+
// 产物直出解包形态目录 <id>@<version>/,桌面 MBWS_DEV_PLUGIN_DIR 指向 dist 即装载。
|
|
178
|
+
// 附件声明新鲜解析(不缓存):产物钉官方库当前 hash,解析失败炸构建是正确行为
|
|
120
179
|
const outDir = distDir();
|
|
121
180
|
// mkdir 容错:在线构建场景 outDir 被平台覆盖,本目录可能不存在(写失败会炸构建)
|
|
122
181
|
fs.mkdirSync(outDir, { recursive: true });
|
|
123
|
-
|
|
182
|
+
const refs = config.attachments ?? [];
|
|
183
|
+
const decls = refs.length > 0 ? await resolveAttachmentRefs(refs, resolveApiBase(config)) : [];
|
|
184
|
+
fs.writeFileSync(path.join(outDir, "manifest.json"), `${JSON.stringify(buildManifest(config, { resolvedAttachments: decls }), null, 2)}\n`);
|
|
124
185
|
},
|
|
125
186
|
};
|
|
126
187
|
}
|
|
@@ -152,6 +213,308 @@ async function writeContractTypesFile(config, root, log) {
|
|
|
152
213
|
}
|
|
153
214
|
}
|
|
154
215
|
/** /__mbws__/<rest> → <apiBase>/<rest>:方法/头透传,响应状态码/content-type/body 透传 */
|
|
216
|
+
// —— 以下为 dev 执行中间件(serve 专属):真 spawn 附件二进制,NDJSON 流回事件 ——
|
|
217
|
+
/**
|
|
218
|
+
* 当前进程 → 附件平台键(与 manifest 的 ATTACHMENT_PLATFORMS 同词表)。
|
|
219
|
+
*/
|
|
220
|
+
export function currentAttachmentPlatform() {
|
|
221
|
+
const os = process.platform === "win32" ? "win" : process.platform === "darwin" ? "darwin" : "linux";
|
|
222
|
+
return `${os}-${process.arch === "arm64" ? "arm64" : "x64"}`;
|
|
223
|
+
}
|
|
224
|
+
/** dev 附件缓存目录(工程内,应进插件工程 .gitignore) */
|
|
225
|
+
const DEV_ATTACHMENT_CACHE = ".mbws/attachments";
|
|
226
|
+
/**
|
|
227
|
+
* dev 用附件本地覆盖:env MBWS_DEV_ATTACHMENT_<NAME>(名字大写、- → _)优先,
|
|
228
|
+
* 其次 attachments 声明里的 devPath(线上还没有的新版本指本机路径)。
|
|
229
|
+
* 未配置/路径不存在返回 null(走官方附件库自动下载)。
|
|
230
|
+
*/
|
|
231
|
+
export function resolveDevAttachmentOverride(config, name) {
|
|
232
|
+
const envValue = process.env[`MBWS_DEV_ATTACHMENT_${name.replace(/-/g, "_").toUpperCase()}`];
|
|
233
|
+
if (envValue && fs.existsSync(envValue))
|
|
234
|
+
return envValue;
|
|
235
|
+
const ref = (config.attachments ?? []).find((a) => a.name === name);
|
|
236
|
+
if (typeof ref?.devPath === "string" && fs.existsSync(ref.devPath))
|
|
237
|
+
return ref.devPath;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
/** 同一附件并发解析去重(下载进行中第二个 spawn 等同一 Promise) */
|
|
241
|
+
const inflightDevAttachments = new Map();
|
|
242
|
+
/**
|
|
243
|
+
* resolve 端点结果的 dev 会话级 memo(name@version → 下载参数)。版本即 hash 锚,
|
|
244
|
+
* 同版本重复 spawn 不再查端点;重发附件(应换版本)后重启 dev 即同步。
|
|
245
|
+
*/
|
|
246
|
+
const devAttachmentResolves = new Map();
|
|
247
|
+
/**
|
|
248
|
+
* 确保官方附件库附件在本地就绪:按 config.attachments 声明查 resolve 端点,
|
|
249
|
+
* 内容寻址缓存 `.mbws/attachments/<name>@<version>/<sha256>/<entry>`——命中零下载;
|
|
250
|
+
* 未命中流式下载(sha256 校验)+ `tar -xf` 解包(bsdtar 通吃 tar.gz/zip)+ chmod 755。
|
|
251
|
+
* 附件未声明 / 库未登记返回 null(调用方回 501);下载/校验/解包失败抛错。
|
|
252
|
+
* log 收下载进度行(调用方转 NDJSON 事件流)。
|
|
253
|
+
*/
|
|
254
|
+
async function ensureLibraryAttachment(config, root, name, log) {
|
|
255
|
+
const key = `${root}::${name}`;
|
|
256
|
+
const inflight = inflightDevAttachments.get(key);
|
|
257
|
+
if (inflight)
|
|
258
|
+
return inflight;
|
|
259
|
+
const task = (async () => {
|
|
260
|
+
const ref = (config.attachments ?? []).find((a) => a.name === name);
|
|
261
|
+
if (!ref)
|
|
262
|
+
return null;
|
|
263
|
+
const platform = currentAttachmentPlatform();
|
|
264
|
+
const resolveKey = `${name}@${ref.version}`;
|
|
265
|
+
let resolved = devAttachmentResolves.get(resolveKey);
|
|
266
|
+
if (!resolved) {
|
|
267
|
+
try {
|
|
268
|
+
const response = await fetch(`${resolveApiBase(config)}/api/plugin-attachments/resolve` +
|
|
269
|
+
`?name=${encodeURIComponent(name)}&version=${encodeURIComponent(ref.version)}` +
|
|
270
|
+
`&platform=${platform}`);
|
|
271
|
+
// 未登记/平台缺失/后端不可达都归「dev 不可用」,交给调用方 501 文案
|
|
272
|
+
if (!response.ok)
|
|
273
|
+
return null;
|
|
274
|
+
resolved = (await response.json());
|
|
275
|
+
devAttachmentResolves.set(resolveKey, resolved);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// win 平台包内入口带 .exe 后缀(publish 脚本布局),显式 entry 原样用
|
|
282
|
+
const entry = ref.entry ?? `${name}${platform.startsWith("win") ? ".exe" : ""}`;
|
|
283
|
+
const dir = path.join(root, DEV_ATTACHMENT_CACHE, `${name}@${ref.version}`, resolved.sha256);
|
|
284
|
+
const entryPath = path.join(dir, entry);
|
|
285
|
+
if (fs.existsSync(entryPath))
|
|
286
|
+
return entryPath;
|
|
287
|
+
log(`[mbws:dev] 下载附件 ${name}@${ref.version}(${platform},${Math.round(resolved.size / 1e6)} MB)`);
|
|
288
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
289
|
+
const tmpPath = path.join(dir, `download.${resolved.archive}`);
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(resolved.url);
|
|
292
|
+
if (!response.ok || !response.body)
|
|
293
|
+
throw new Error(`下载 HTTP ${response.status}`);
|
|
294
|
+
const hash = crypto.createHash("sha256");
|
|
295
|
+
const out = fs.createWriteStream(tmpPath);
|
|
296
|
+
let received = 0;
|
|
297
|
+
let lastReported = 0;
|
|
298
|
+
// Node fetch 的 body 是 async iterable:流式落盘 + 边下边算 hash(不整包进内存)
|
|
299
|
+
for await (const chunk of response.body) {
|
|
300
|
+
const bytes = Buffer.from(chunk);
|
|
301
|
+
hash.update(bytes);
|
|
302
|
+
out.write(bytes);
|
|
303
|
+
received += bytes.byteLength;
|
|
304
|
+
if (received - lastReported >= 5 * 1e6) {
|
|
305
|
+
lastReported = received;
|
|
306
|
+
log(`[mbws:dev] 下载中 ${Math.round(received / 1e6)} / ${Math.round(resolved.size / 1e6)} MB`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
await new Promise((resolve, reject) => out.end((error) => (error ? reject(error) : resolve())));
|
|
310
|
+
const sha256 = hash.digest("hex");
|
|
311
|
+
if (sha256 !== resolved.sha256) {
|
|
312
|
+
throw new Error(`sha256 不符(期望 ${resolved.sha256},实得 ${sha256})`);
|
|
313
|
+
}
|
|
314
|
+
const extracted = spawnSync("tar", ["-xf", tmpPath, "-C", dir]);
|
|
315
|
+
if (extracted.status !== 0) {
|
|
316
|
+
throw new Error(`解包失败:${extracted.stderr?.toString().trim() || `tar exit ${extracted.status}`}`);
|
|
317
|
+
}
|
|
318
|
+
if (!fs.existsSync(entryPath))
|
|
319
|
+
throw new Error(`解包后未见入口 ${entry}`);
|
|
320
|
+
fs.chmodSync(entryPath, 0o755);
|
|
321
|
+
log(`[mbws:dev] 附件 ${name}@${ref.version} 就绪(已缓存,后续零下载)`);
|
|
322
|
+
return entryPath;
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
fs.rmSync(tmpPath, { force: true });
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
inflightDevAttachments.set(key, task);
|
|
329
|
+
try {
|
|
330
|
+
return await task;
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
inflightDevAttachments.delete(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** POST /__mbws__/dev/exec:spawn 本机二进制(cwd = 工程根,插件 args 里 samples/xxx 相对可用) */
|
|
337
|
+
async function handleDevExec(config, root, req, res) {
|
|
338
|
+
let body;
|
|
339
|
+
try {
|
|
340
|
+
body = JSON.parse((await readRequestBody(req)).toString("utf8"));
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
res.statusCode = 400;
|
|
344
|
+
res.end(JSON.stringify({ message: "body 不是合法 JSON" }));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const { attachment, args } = body;
|
|
348
|
+
if (typeof attachment !== "string" ||
|
|
349
|
+
!Array.isArray(args) ||
|
|
350
|
+
!args.every((a) => typeof a === "string")) {
|
|
351
|
+
res.statusCode = 400;
|
|
352
|
+
res.end(JSON.stringify({ message: "attachment(string) 与 args(string[]) 必填" }));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
res.setHeader("content-type", "application/x-ndjson");
|
|
356
|
+
res.setHeader("cache-control", "no-store");
|
|
357
|
+
const write = (event) => res.write(`${JSON.stringify(event)}\n`);
|
|
358
|
+
// 附件解析:本地覆盖(env / devPath)零网络;未覆盖按 attachments 声明
|
|
359
|
+
// 自动从官方附件库下载缓存(下载进度经 NDJSON 事件流回面板)
|
|
360
|
+
let binPath = resolveDevAttachmentOverride(config, attachment);
|
|
361
|
+
if (!binPath) {
|
|
362
|
+
try {
|
|
363
|
+
binPath = await ensureLibraryAttachment(config, root, attachment, (text) => write({ kind: "lines", lines: [{ stream: "stderr", text }] }));
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
// 下载/校验/解包失败:已经开始流式响应(可能已写过进度行),按 job 失败收尾
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
write({ kind: "exit", exitCode: -1, stderrTail: `附件 ${attachment} 下载失败:${message}` });
|
|
369
|
+
res.end();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!binPath) {
|
|
373
|
+
// 501 = 能力明确不可用:SDK dev-mock 收到非 2xx 退模拟执行
|
|
374
|
+
const declared = (config.attachments ?? []).some((a) => a.name === attachment);
|
|
375
|
+
res.statusCode = 501;
|
|
376
|
+
res.end(JSON.stringify({
|
|
377
|
+
message: declared
|
|
378
|
+
? `附件 ${attachment} 未在官方附件库登记当前平台产物(发布见 scripts/publish-attachment.mjs),或后端不可达;本机覆盖可配 attachments 的 devPath / env MBWS_DEV_ATTACHMENT_${attachment.replace(/-/g, "_").toUpperCase()}`
|
|
379
|
+
: `mbws.config.ts 的 attachments 未声明附件 ${attachment}(dev 自动下载依赖声明);本机覆盖可配该项 devPath / env MBWS_DEV_ATTACHMENT_${attachment.replace(/-/g, "_").toUpperCase()}`,
|
|
380
|
+
}));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const proc = spawn(binPath, args, { cwd: root });
|
|
385
|
+
const stderrTailChunks = [];
|
|
386
|
+
let stderrTailBytes = 0;
|
|
387
|
+
const pendingLines = [];
|
|
388
|
+
let flushTimer = null;
|
|
389
|
+
const flush = () => {
|
|
390
|
+
if (pendingLines.length > 0)
|
|
391
|
+
write({ kind: "lines", lines: pendingLines.splice(0) });
|
|
392
|
+
};
|
|
393
|
+
const pushLine = (stream, text) => {
|
|
394
|
+
pendingLines.push({ stream, text });
|
|
395
|
+
if (!flushTimer)
|
|
396
|
+
flushTimer = setTimeout(() => {
|
|
397
|
+
flushTimer = null;
|
|
398
|
+
flush();
|
|
399
|
+
}, 100);
|
|
400
|
+
};
|
|
401
|
+
const wireLines = (source, stream) => {
|
|
402
|
+
let buffer = "";
|
|
403
|
+
source.setEncoding?.("utf8");
|
|
404
|
+
source.on("data", (chunk) => {
|
|
405
|
+
buffer += chunk;
|
|
406
|
+
let newlineAt = buffer.indexOf("\n");
|
|
407
|
+
while (newlineAt >= 0) {
|
|
408
|
+
const line = buffer.slice(0, newlineAt).replace(/\r$/, "");
|
|
409
|
+
buffer = buffer.slice(newlineAt + 1);
|
|
410
|
+
if (line)
|
|
411
|
+
pushLine(stream, line);
|
|
412
|
+
if (stream === "stderr") {
|
|
413
|
+
stderrTailChunks.push(Buffer.from(`${line}\n`));
|
|
414
|
+
stderrTailBytes += line.length + 1;
|
|
415
|
+
while (stderrTailBytes > 8192) {
|
|
416
|
+
stderrTailBytes -= stderrTailChunks[0].length;
|
|
417
|
+
stderrTailChunks.shift();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
newlineAt = buffer.indexOf("\n");
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
const timeoutMs = typeof body.timeoutMs === "number" && body.timeoutMs > 0 ? body.timeoutMs : 30 * 60_000;
|
|
425
|
+
const timer = setTimeout(() => proc.kill("SIGKILL"), timeoutMs);
|
|
426
|
+
const finish = (exitCode, stderrTail) => {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
if (flushTimer)
|
|
429
|
+
clearTimeout(flushTimer);
|
|
430
|
+
flush();
|
|
431
|
+
write({ kind: "exit", exitCode, stderrTail });
|
|
432
|
+
res.end();
|
|
433
|
+
};
|
|
434
|
+
if (proc.stdout)
|
|
435
|
+
wireLines(proc.stdout, "stdout");
|
|
436
|
+
if (proc.stderr)
|
|
437
|
+
wireLines(proc.stderr, "stderr");
|
|
438
|
+
proc.on("error", (error) => finish(-1, `spawn ${binPath} 失败:${error.message}`));
|
|
439
|
+
proc.on("exit", (code) => finish(code ?? -1, Buffer.concat(stderrTailChunks).toString("utf8")));
|
|
440
|
+
}
|
|
441
|
+
/** GET /__mbws__/dev/localize?ref=:映射到工程内 samples/ 夹具(存在才 200) */
|
|
442
|
+
async function handleDevLocalize(root, res, ref) {
|
|
443
|
+
const basename = ref.split("/").pop() ?? "";
|
|
444
|
+
const filePath = path.resolve(root, "samples", basename);
|
|
445
|
+
try {
|
|
446
|
+
const stat = await fs.promises.stat(filePath);
|
|
447
|
+
if (stat.isFile()) {
|
|
448
|
+
res.setHeader("content-type", "application/json");
|
|
449
|
+
res.end(JSON.stringify({ path: `samples/${basename}`, size: stat.size }));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// 不存在走 404
|
|
455
|
+
}
|
|
456
|
+
res.statusCode = 404;
|
|
457
|
+
res.end(JSON.stringify({ message: `samples/${basename} 不存在` }));
|
|
458
|
+
}
|
|
459
|
+
const DEV_FILE_MIME = {
|
|
460
|
+
".mp4": "video/mp4",
|
|
461
|
+
".webm": "video/webm",
|
|
462
|
+
".mov": "video/quicktime",
|
|
463
|
+
".m4v": "video/mp4",
|
|
464
|
+
".mp3": "audio/mpeg",
|
|
465
|
+
".wav": "audio/wav",
|
|
466
|
+
".png": "image/png",
|
|
467
|
+
".jpg": "image/jpeg",
|
|
468
|
+
".jpeg": "image/jpeg",
|
|
469
|
+
".webp": "image/webp",
|
|
470
|
+
".gif": "image/gif",
|
|
471
|
+
".json": "application/json",
|
|
472
|
+
".txt": "text/plain; charset=utf-8",
|
|
473
|
+
".vtt": "text/vtt",
|
|
474
|
+
".srt": "text/plain; charset=utf-8",
|
|
475
|
+
};
|
|
476
|
+
/** GET /__mbws__/dev/file?path=:dev 媒体预览(工程内路径白名单 + Range 支持) */
|
|
477
|
+
async function handleDevFile(root, req, res, relPath) {
|
|
478
|
+
const filePath = path.resolve(root, relPath);
|
|
479
|
+
if (!filePath.startsWith(path.resolve(root))) {
|
|
480
|
+
res.statusCode = 403;
|
|
481
|
+
res.end();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
let stat;
|
|
485
|
+
try {
|
|
486
|
+
stat = await fs.promises.stat(filePath);
|
|
487
|
+
if (!stat.isFile())
|
|
488
|
+
throw new Error("not file");
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
res.statusCode = 404;
|
|
492
|
+
res.end();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const mime = DEV_FILE_MIME[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
496
|
+
res.setHeader("content-type", mime);
|
|
497
|
+
res.setHeader("accept-ranges", "bytes");
|
|
498
|
+
const range = req.headers.range;
|
|
499
|
+
const match = typeof range === "string" ? range.match(/^bytes=(\d*)-(\d*)$/) : null;
|
|
500
|
+
if (match) {
|
|
501
|
+
const start = match[1] ? Number(match[1]) : 0;
|
|
502
|
+
const end = match[2] ? Math.min(Number(match[2]), stat.size - 1) : stat.size - 1;
|
|
503
|
+
if (Number.isNaN(start) || start > end) {
|
|
504
|
+
res.statusCode = 416;
|
|
505
|
+
res.setHeader("content-range", `bytes */${stat.size}`);
|
|
506
|
+
res.end();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
res.statusCode = 206;
|
|
510
|
+
res.setHeader("content-range", `bytes ${start}-${end}/${stat.size}`);
|
|
511
|
+
res.setHeader("content-length", String(end - start + 1));
|
|
512
|
+
fs.createReadStream(filePath, { start, end }).pipe(res);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
res.setHeader("content-length", String(stat.size));
|
|
516
|
+
fs.createReadStream(filePath).pipe(res);
|
|
517
|
+
}
|
|
155
518
|
async function forwardApi(config, req, res) {
|
|
156
519
|
const pathname = (req.url ?? "").split("?")[0];
|
|
157
520
|
const rest = pathname.slice(DEV_CONTRACT_BASE.length);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mbws/plugin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Moye 插件开发 SDK——panel/viewer 与宿主的类型定义与运行时端口适配",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./package.json": "./package.json",
|
|
14
15
|
"./vite": {
|
|
15
16
|
"types": "./dist/vite.d.ts",
|
|
16
17
|
"default": "./dist/vite.js"
|