@joker.front/cli
Version:
The Next-Generation Front-End Toolchain: Swift, Efficient, and Adaptive.
609 lines (593 loc) • 18.8 kB
JavaScript
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