UNPKG

@lonu/stc

Version:

A tool for converting OpenApi/Swagger/Apifox into code.

243 lines (242 loc) 7.43 kB
import * as dntShim from "../_dnt.shims.js"; import { dirname, resolve } from "../deps/jsr.io/@std/path/1.1.6/mod.js"; import { start } from "./app.js"; import Logs from "./console.js"; import { getT } from "./i18n/index.js"; /** * 远程地址判断 */ const REMOTE_REG = /^http(s?):\/\//; /** * 本地文件监听的防抖时间 */ const DEBOUNCE_MS = 300; /** * 解析符号链接,保证监听路径与事件路径一致(如 macOS 的 /tmp -> /private/tmp) * @param path - 路径 */ const realPath = (path) => { try { return dntShim.Deno.realPathSync(path); } catch { return path; } }; /** * 格式化错误信息 * @param error - 错误 */ const formatError = (error) => { if (error instanceof Error) return error.message; if (typeof error === "string") return error; try { return JSON.stringify(error); } catch { return String(error); } }; /** * 执行一次生成,失败时仅输出错误,不中断监听 * @param options - 配置 */ export const runOnce = async (options) => { try { await start(options); } catch (error) { Logs.error(getT("$t(watch.regenerateError)", { error: formatError(error) })); } }; /** * 计算文本的 SHA-256 哈希 * @param text - 文本 */ const hashText = async (text) => { const data = new TextEncoder().encode(text); const digest = await crypto.subtle.digest("SHA-256", data); return Array.from(new Uint8Array(digest)) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); }; /** * 请求远程地址并计算内容哈希,失败时返回 undefined * @param url - 远程地址 */ const fetchSpecHash = async (url) => { try { const res = await fetch(url); if (!res.ok) return undefined; return await hashText(await res.text()); } catch { return undefined; } }; /** * 监听本地文件变化(可同时监听 API 文档和配置文件) * * @param targets - 监听目标,路径需为绝对路径 */ const watchLocalFiles = (targets) => { const watchDirs = [ ...new Set([targets.spec, targets.config] .filter(Boolean) .map((path) => dirname(path))), ]; const watcher = dntShim.Deno.watchFs(watchDirs); const closeWatcher = () => { try { watcher.close(); } catch { // 忽略重复关闭产生的错误 } }; let timer; let settled = false; let kind = "spec"; let resolvePromise; const promise = new Promise((resolve) => { resolvePromise = resolve; }); // 防抖,避免编辑器保存时触发多次 const settle = () => { clearTimeout(timer); timer = setTimeout(() => { if (settled) return; settled = true; closeWatcher(); resolvePromise(kind); }, DEBOUNCE_MS); }; const cancel = () => { if (settled) return; settled = true; clearTimeout(timer); closeWatcher(); }; (async () => { try { for await (const event of watcher) { if (settled) break; // 配置文件删除后也需要重新加载,以便回退到默认配置或其他候选文件。 if (event.kind === "access") continue; const matchedConfig = Boolean(targets.config) && event.paths.some((path) => path === targets.config); const matchedSpec = event.paths.some((path) => path === targets.spec); if (!matchedConfig && !matchedSpec) continue; // 配置文件变化优先,因为重新加载配置后也会重新生成 if (matchedConfig) { kind = "config"; } else if (kind !== "config") { kind = "spec"; } settle(); } } catch { // 忽略 watcher 关闭产生的错误 } })(); return { promise, cancel }; }; /** * 等待远程地址内容变化(轮询),同时监听本地配置文件变化 * * @param url - 远程地址 * @param intervalMs - 轮询间隔(毫秒) * @param configPath - 本地配置文件路径(可选) */ const waitForRemoteChange = async (url, intervalMs, configPath) => { // 以监听开始时的内容作为基准 const baseline = await fetchSpecHash(url); return await new Promise((resolvePromise) => { let settled = false; let configWatcher; const finish = (kind) => { if (settled) return; settled = true; clearInterval(timer); configWatcher?.cancel(); resolvePromise(kind); }; const timer = setInterval(async () => { const hash = await fetchSpecHash(url); if (hash && hash !== baseline) { finish("spec"); } }, intervalMs); if (configPath) { // 复用本地监听器,但明确标记为配置文件,避免误判为 spec 变化。 configWatcher = watchLocalFiles({ spec: configPath, config: configPath, }); configWatcher.promise .then(() => finish("config")) .catch(() => { }); } }); }; /** * 启动监听模式 * * @param initial - 初始选项和配置文件路径 * @param reload - 重新解析选项(配置文件变化时调用) */ export const startWatch = async (initial, reload) => { try { dntShim.Deno.addSignalListener("SIGINT", () => { console.log(""); Logs.info(getT("$t(watch.stop)")); dntShim.Deno.exit(0); }); } catch { // 部分环境不支持 SIGINT 监听 } let current = initial; // 配置文件变化时重新加载选项,需要重启监听,因此使用循环 while (true) { const { options, configPath } = current; const remote = REMOTE_REG.test(options.url); const absoluteConfigPath = configPath ? resolve(dntShim.Deno.cwd(), configPath) : undefined; Logs.info(getT("$t(watch.start)")); if (remote) { Logs.info(getT("$t(watch.pollRemote)", { interval: Math.round((options.interval ?? 3000) / 1000), })); } const kind = remote ? await waitForRemoteChange(options.url, options.interval ?? 3000, absoluteConfigPath ? realPath(absoluteConfigPath) : undefined) : await watchLocalFiles({ spec: realPath(resolve(dntShim.Deno.cwd(), options.url)), config: absoluteConfigPath ? realPath(absoluteConfigPath) : undefined, }).promise; if (kind === "config") { Logs.info(getT("$t(watch.configChanged)")); current = await reload(); // 新配置关闭了监听 if (!current.options.watch) return; await runOnce(current.options); continue; } Logs.info(getT("$t(watch.changeDetected)")); await runOnce(options); } };