UNPKG

@joker.front/cli

Version:

The Next-Generation Front-End Toolchain: Swift, Efficient, and Adaptive.

609 lines (593 loc) 18.8 kB
var logger; (function (logger) { /** * 日志输出 * * 当前方法只控制日志输出等级,不做逻辑注入 * 无论是H5、小程序、客户端,都需要在浏览器/V8中执行 * 日志输出到容器内即可 * @param type * @param tagName * @param content */ function writeLog(type, message) { let str = `[JOKERCLI]: ${message}`; console[type](str); } /** * 信息 * @param tag * @param content */ function info(content) { writeLog("info", content); } logger.info = info; /** * 警告 * @param tag * @param content */ function warn(content) { writeLog("warn", content); } logger.warn = warn; /** * 错误 * @param tag * @param content */ function error(content, err) { writeLog("error", content); err && console.error(err); } logger.error = error; })(logger || (logger = {})); /**遮罩元素ID */ const OVERLAY_ID = "joker-error-overlay"; const FILE_RE = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g; const CODE_FRAME_RE = /^(?:>?\s+\d+\s+\|.*|\s+\|\s*\^.*)\r?\n/gm; const TEMPLATE = ` <style> :host { position: fixed; top: 0; left: 0; z-index: 99999; width: 100%; height: 100%; overflow-y: scroll; margin: 0; background: rgba(0, 0, 0, 0.66); --font: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier; --red: #ff5555; --yellow: #e2aa53; --purple: #cfa4ff; --cyan: #2dd9da; --dim: #c9c9c9; } .window { font-family: var(--font); line-height: 1.5; color: #d8d8d8; margin: 30px 0; padding: 25px 40px; position: relative; background: #181818; border-radius: 6px 6px 8px 8px; box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); overflow: hidden; border-top: 8px solid var(--red); direction: ltr; text-align: left; } pre { font-family: var(--font); font-size: 16px; margin-top: 0; margin-bottom: 1em; overflow-x: scroll; scrollbar-width: none; } pre::-webkit-scrollbar { display: none; } .message { line-height: 1.3; font-weight: 600; white-space: pre-wrap; } .message-body { color: var(--red); } .plugin { color: var(--purple); } .file { color: var(--cyan); margin-bottom: 0; white-space: pre-wrap; word-break: break-all; } .frame { color: var(--yellow); } .stack { font-size: 13px; color: var(--dim); } .tip { font-size: 13px; color: #999; border-top: 1px dotted #999; padding-top: 13px; } .file-link { text-decoration: underline; cursor: pointer; } </style> <div class="window"> <pre class="message"> <span class="plugin"></span> <span class="message-body"></span> </pre> <pre class="file"></pre> <pre class="frame"></pre> <pre class="stack"></pre> <div class="tip">点击空白处关闭该遮罩提示</div> </div> `; class ErrorOverlay extends HTMLElement { root; constructor(err) { super(); this.root = this.attachShadow({ mode: "open" }); this.root.innerHTML = TEMPLATE; CODE_FRAME_RE.lastIndex = 0; let hasFrame = err.frame && CODE_FRAME_RE.test(err.frame); let message = hasFrame ? err.message.replace(CODE_FRAME_RE, "") : err.message; if (err.plugin) { this.text(".plugin", `[插件:${err.plugin}]`); } this.text(".message-body", message.trim()); let [file] = (err.loc?.file || err.id || "未知文件").split("?"); if (err.loc) { this.text(".file", `${file}:${err.loc.line}:${err.loc.column}`, true); } else if (err.id) { this.text(".file", file); } if (hasFrame) { this.text(".frame", err.frame.trim()); } this.text(".stack", err.stack, true); this.root.querySelector(".window")?.addEventListener("click", (e) => { e.stopPropagation(); }); this.addEventListener("click", () => { this.close(); }); } text(selector, text, linkFiles = false) { let el = this.root.querySelector(selector); if (linkFiles === false) { el.textContent = text; return; } let currentIndex = 0; let match; while ((match = FILE_RE.exec(text))) { let { 0: file, index } = match; if (index !== null) { let frag = text.slice(currentIndex, index); el.appendChild(document.createTextNode(frag)); let link = document.createElement("a"); link.textContent = file; link.className = "file-link"; link.onclick = () => { fetch(`/__open-in-editor?file=${encodeURIComponent(file)}`); }; el.appendChild(link); currentIndex += frag.length + file.length; } } } close() { this.parentNode?.removeChild(this); } } if (customElements.get(OVERLAY_ID) === undefined) { customElements.define(OVERLAY_ID, ErrorOverlay); } function hasErrorOverlay() { return document.querySelectorAll(OVERLAY_ID).length !== 0; } function clearErrorOverlay() { document.querySelectorAll(OVERLAY_ID).forEach((el) => { el.close(); }); } function createErrorOverlay(err) { clearErrorOverlay(); document.body.appendChild(new ErrorOverlay(err)); } //取值设值 let importMetaUrl = new URL(import.meta.url); /**Socket协议 */ let socketProtocol = location.protocol === "https" ? "wss" : "ws"; /**基础目录 */ let base = __BASE__; /**热更新端口 */ let hmrPort = __HMR_PORT__; /**热更新端口 */ let hmrClientId = __HMR_CLIENT_ID__; /**Socket HOST */ let socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${hmrPort || importMetaUrl.port}${base}`; class SocketService { socket; /** * 连接是否已打开 */ isOpened = false; /** * 消息列队 */ messageQueue = new Set(); /** * 监听列队 */ listeners = new Map(); /** * 是否已经执行过更新 */ isUpdated = false; /** * 排除文件字典 */ pruneMap = new Map(); /** * url于数据字典 */ urlDataMap = new Map(); /** * 热更模块字典 */ hotModuleMap = new Map(); /** * Dep和处理事件字典 */ depExecMap = new Map(); constructor() { //如果开启HMR时才做处理 if (hmrPort) { try { this.socket = new WebSocket(`${socketProtocol}://${socketHost}`, "joker-hmr"); } catch (e) { logger.error("WebSocket connection failed"); } this.initEventListener(); } else { logger.warn("HMR is disabled. Applying fallback mode with feature degradation compensation only"); } } initEventListener() { this.socket?.addEventListener("open", () => { this.isOpened = true; }, { once: true }); this.socket?.addEventListener("message", async ({ data }) => { this.receiveMessage(JSON.parse(data)); }); this.socket?.addEventListener("close", async ({ wasClean }) => { if (wasClean) return; logger.info("Connection to server lost. Attempting to reconnect..."); await this.waitingToConnect(); location.reload(); }); } receiveMessage(hmr) { if (hmr.clientId !== hmrClientId) return; switch (hmr.type) { case "connected": logger.info("Server connection established"); this.sendMessages(); setInterval(() => { this.socket?.send(`{'type':'ping'}`); }, __HMR_HEARTTIMER__); break; case "update": this.notify("before:update", hmr); //如果已更新 && 有阻塞遮罩提示,需要进行reload if (this.isUpdated === false && hasErrorOverlay()) { window.location.reload(); return; } clearErrorOverlay(); this.isUpdated = true; hmr.updates.forEach((update) => { if (update.type === "css-update") { this.updateCss(update); } else { this.updateScript(update); } }); break; case "custom": this.notify(hmr.event, hmr.data); break; case "reload": this.notify("before:reload", hmr); if (hmr.path?.endsWith(".html")) { let pagePath = decodeURI(location.pathname); let loadPath = base + hmr.path.slice(1); if (pagePath === loadPath || hmr.path === "/index.html" || (pagePath.endsWith("/") && pagePath + "index.html" === loadPath)) { location.reload(); } } else { location.reload(); } break; case "prune": this.notify("before:prune", hmr); hmr.paths.forEach((p) => { this.pruneMap.get(p)?.(this.urlDataMap.get(p)); }); break; case "error": this.notify("error", hmr); logger.error(`Error detected:\n${hmr.err.message}\n${hmr.err.stack}`); createErrorOverlay(hmr.err); break; default: logger.warn(`Unknown HMR type detected. This might be caused by version mismatch between CLI and client`); break; } } async waitingToConnect() { let hostProtocol = socketProtocol === "wss" ? "https" : "http"; while (true) { try { await fetch(`${hostProtocol}://${socketHost}`, { mode: "no-cors" }); break; } catch (e) { await sleep(1500); } } } sendMessages() { if (this.socket?.readyState === 1) { this.messageQueue.forEach((msg) => { this.socket?.send(msg); }); this.messageQueue.clear(); } } notify(event, data) { let callBacks = this.listeners.get(event); if (callBacks) { callBacks.forEach((cb) => cb(data)); } } updateCss(update) { let searchUrl = this.clearnUrl(update.path); let el = Array.from(document.querySelectorAll("link")).find((el) => { return this.clearnUrl(el.href).includes(searchUrl); }); if (el) { let newPath = `${base}${searchUrl.slice(1)}${searchUrl.includes("?") ? "&" : "?"}t=${update.timestamp}`; let newLinkTag = el.cloneNode(); newLinkTag.href = new URL(newPath, el.href).href; newLinkTag.addEventListener("load", () => el?.remove()); newLinkTag.addEventListener("error", () => el?.remove()); //先挂新的link,等待加载完毕或者失败时,删除原始link el.after(newLinkTag); logger.info(`CSS file ${searchUrl} has been updated`); } else { logger.warn(`Server requested update for ${searchUrl}, but corresponding link not found in DOM. Update skipped.`); } } /**脚本更新执行列队 */ scriptUpdateQueue = []; /**脚本更新执行等待pending */ scriptUpdatePending = false; async updateScript(update) { let module = this.hotModuleMap.get(update.path); if (module === undefined) return; //创建更新执行程序 let createUpdateFn = async () => { let moduleMap = new Map(); let isSelfUpdate = update.path === update.acceptedPath; let moduleToUpdate = new Set(); //如果是自身更新 if (isSelfUpdate) { moduleToUpdate.add(update.path); } else { //判断当前页面的依赖Dep如果有也存在依赖该module时,做记录并同步更新dep for (let cb of module.callbacks) { cb.deps.forEach((dep) => { if (update.acceptedPath === dep) { moduleToUpdate.add(dep); } }); } } //筛选出符合dep变更范围的回调 let callBacks = module.callbacks.filter((cb) => { return cb.deps.some((dep) => moduleToUpdate.has(dep)); }); await Promise.all(Array.from(moduleToUpdate).map(async (dep) => { let beforeExec = this.depExecMap.get(dep); //处理dep之前的自定义处理函数 if (beforeExec) { await beforeExec(this.urlDataMap.get(dep)); } let [path, query] = dep.split("?"); try { let newModule = await import(`${base}${path.slice(1)}?import&t=${update.timestamp}${query ? `&${query}` : ""}`); moduleMap.set(dep, newModule); } catch (err) { logger.error(`Request to ${path} failed. This could be due to syntax errors or importing non-existent modules. (See console for details). Attempting to reload to fix HMR failure in 2 seconds.`, err); setTimeout(() => { location.reload(); }, 2000); } })); return () => { for (let cb of callBacks) { cb.fn(cb.deps.map((dep) => moduleMap.get(dep))); } let prettyUrl = isSelfUpdate ? update.path : `${update.acceptedPath} updated via ${update.path}`; logger.info(`Hot update completed: ${prettyUrl}`); }; }; this.scriptUpdateQueue.push(createUpdateFn()); if (this.scriptUpdatePending === false) { this.scriptUpdatePending = true; //等待微任务周期 await Promise.resolve(); this.scriptUpdatePending = false; //clone let loading = [...this.scriptUpdateQueue]; //清空 this.scriptUpdateQueue = []; (await Promise.all(loading)).forEach((fn) => fn?.()); } } /** * 去除地址中非有效参数 * @param path * @returns */ clearnUrl(path) { let url = new URL(path, location.toString()); url.searchParams.delete("direct"); return url.pathname + url.search; } } async function sleep(timer) { await new Promise((resolve) => setTimeout(resolve, timer)); } //初始化服务 let socket = new SocketService(); let styleMap = new Map(); let listenersMap = new Map(); let ctxListenersMap = new Map(); function updateStyle(id, content) { let style = styleMap.get(id); if (style) { style.innerHTML = content; } else { style = document.createElement("style"); style.setAttribute("type", "text/css"); style.innerHTML = content; document.head.appendChild(style); } styleMap.set(id, style); } function removeStyle(id) { let style = styleMap.get(id); if (style) { document.head.removeChild(style); styleMap.delete(id); } } class JokerHotContext { path; listeners = new Map(); constructor(path) { this.path = path; if (socket.urlDataMap.has(path) === false) { socket.urlDataMap.set(path, {}); } let module = socket.hotModuleMap.get(path); //新的Hot上下文创建,需要清空历史回调 if (module) { module.callbacks = []; } let ownerListeners = ctxListenersMap.get(path); if (ownerListeners) { for (let [event, fns] of ownerListeners) { let customListener = listenersMap.get(event); //如果存在,则进行同步处理 if (customListener) { listenersMap.set(event, customListener.filter((l) => fns.includes(l) === false)); } } } //注册整体监听者 ctxListenersMap.set(path, this.listeners); } get data() { return socket.urlDataMap.get(this.path); } /**接收引用 */ accept(deps, callBacks) { if (typeof deps === "function") { //接收自己 this.acceptDeps([this.path], deps); } else { deps = [deps].flat(); if (deps.length) { this.acceptDeps(deps, callBacks); } } } dispose(cb) { socket.depExecMap.set(this.path, cb); } prune(cb) { socket.pruneMap.set(this.path, cb); } on(event, cb) { this.addToListionMap(event, listenersMap, cb); this.addToListionMap(event, this.listeners, cb); } send(event, data) { socket.messageQueue.add(JSON.stringify({ type: "custom", event, data })); socket.sendMessages(); } acceptDeps(deps, callBacks = () => { }) { let module = socket.hotModuleMap.get(this.path) || { id: this.path, callbacks: [] }; module.callbacks.push({ deps, fn: callBacks }); socket.hotModuleMap.set(this.path, module); } addToListionMap(event, souceMap, cb) { let map = souceMap.get(event) || []; map.push(cb); listenersMap.set(event, map); } } function injectQuery(url, query) { //针对非内部地址,直接做返回,不处理 //这里只处理本地地址 if (url.startsWith(".") === false || url.startsWith("/") === false) { return url; } //clean let pathname = url.replace(/#.*$/, "").replace(/\?.*$/, ""); let { search, hash } = new URL(url, "http://jokers.pub"); return `${pathname}?${query}${search ? `&${search.slice(1)}` : ""}${hash || ""}`; } export { JokerHotContext, clearErrorOverlay, createErrorOverlay, injectQuery, removeStyle, updateStyle }; //# sourceMappingURL=client.es.js.map