webhanger-front 1.0.5 → 1.0.7
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 +2 -0
- package/browser.min.js +1 -1
- package/package.json +6 -1
- package/browser.js +0 -259
- package/build.js +0 -29
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Browser SDK for WebHanger. Loads encrypted UI components from CDN and injects them into the DOM. No eval, no readable source — decrypted in memory and applied directly.
|
|
4
4
|
|
|
5
|
+
> **Note:** This package uses `fetch` to load components from your CDN. This is expected behavior and is flagged as an informational notice by npm's security scanner — not a vulnerability.
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Install
|
package/browser.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e){const t="components",n="
|
|
1
|
+
!function(e){const t="components",n="wh2_",r=[],o={};function a(e,t){o[e]||(o[e]=[]),o[e].push(t)}function s(e,t){(o[e]||[]).forEach(e=>e(t))}const c={loads:0,cacheHits:0,errors:0,totalTime:0};function i(e,t){e in c&&(c[e]+=t),s("metric",{name:e,value:t,metrics:{...c}})}const d=new Map;async function l(e,t,n){if(!e)return"";try{const r=e.split(":");if(3!==r.length)return console.warn("[WebHanger] decryptChunk: unexpected format, parts:",r.length),"";const[o,a,s]=r,c=Uint8Array.from(atob(o),e=>e.charCodeAt(0)),i=Uint8Array.from(atob(a),e=>e.charCodeAt(0)),l=Uint8Array.from(atob(s),e=>e.charCodeAt(0)),u=new Uint8Array(l.length+i.length);u.set(l),u.set(i,l.length);const h=await async function(e,t){const n=e+t;if(d.has(n))return d.get(n);const r=new TextEncoder,o=await crypto.subtle.digest("SHA-256",r.encode(e+t)),a=await crypto.subtle.importKey("raw",o,{name:"AES-GCM"},!1,["decrypt"]);return d.set(n,a),a}(t,n),f=await crypto.subtle.decrypt({name:"AES-GCM",iv:c},h,u);return(new TextDecoder).decode(f)}catch(e){return console.warn("[WebHanger] decryptChunk failed:",n,e.message),""}}function u(){return new Promise((e,n)=>{const r=indexedDB.open("webhanger_v2",1);r.onupgradeneeded=e=>e.target.result.createObjectStore(t),r.onsuccess=t=>e(t.target.result),r.onerror=()=>n(r.error)})}async function h(e,r){try{if(r.length<51200)return void localStorage.setItem(n+e,r)}catch(e){}await async function(e,n){const r=await u();return new Promise((o,a)=>{const s=r.transaction(t,"readwrite").objectStore(t).put(n,e);s.onsuccess=()=>o(),s.onerror=()=>a(s.error)})}(e,r)}async function f(e,r){const o=await async function(e){try{const t=localStorage.getItem(n+e);if(t)return t}catch(e){}return await async function(e){const n=await u();return new Promise((r,o)=>{const a=n.transaction(t,"readonly").objectStore(t).get(e);a.onsuccess=()=>r(a.result),a.onerror=()=>o(a.error)})}(e)}(e);if(o)return i("cacheHits",1),r().then(t=>{t&&t!==o&&h(e,t)}).catch(()=>{}),{data:o,source:"cache"};const a=await r();return a&&await h(e,a),{data:a,source:"cdn"}}async function m(e,t,n){const r=Array.isArray(e)?e:[e];let o;for(const e of r)try{const r=n?`${e}?token=${t}&expires=${n}`:`${e}?token=${t}`,o=await fetch(r);if(!o.ok)throw Error("HTTP "+o.status);return await o.text()}catch(t){console.warn(`[WebHanger] CDN failed (${e}): ${t.message} \u2014 trying next...`),o=t}throw Error("All CDN endpoints failed. Last error: "+o?.message)}function w(e){return new Promise(t=>{if("style"===e.type?document.querySelector(`link[href="${e.url}"]`):document.querySelector(`script[src="${e.url}"]`))return t();if("style"===e.type){const n=document.createElement("link");n.rel="stylesheet",n.href=e.url,n.onload=t,n.onerror=t,document.head.appendChild(n)}else{const n=document.createElement("script");n.src=e.url,e.defer&&(n.defer=!0),e.async&&(n.async=!0),n.onload=t,n.onerror=t,document.head.appendChild(n)}})}async function p(e=[]){for(const t of e)await w(t)}async function y(e,t){const n=`${e.cdnUrl}@${e.expires}`,{data:r}=await f(n,()=>m(e.cdnUrl,e.token,e.expires));if(!r)return;let o;try{o=JSON.parse(r)}catch(e){return}o.assets&&o.assets.length&&await p(o.assets);const a=await l(o.c,t,"::css");if(a&&!document.querySelector(`style[data-wh-dep="${e.name}@${e.version}"]`)){const t=document.createElement("style");t.setAttribute("data-wh-dep",`${e.name}@${e.version}`),t.textContent=a,document.head.appendChild(t)}const s=await l(o.h,t,"::html");if(s){const t=document.querySelector(`[data-wh-${e.name}]`);t&&(t.innerHTML=s)}const c=await l(o.j,t,"::js");if(c){const e=document.createElement("script");e.textContent=c,document.head.appendChild(e),document.head.removeChild(e)}}async function g(t,n,r,o,a="[data-wh]",c=null,d=[],u={}){if(!t||!n||!r||null==o)return void console.error("[WebHanger] Missing required params");if(0!==o&&Math.floor(Date.now()/1e3)>o)return void console.warn("[WebHanger] Token expired.");const h=function(e){return(t,n={})=>{"function"==typeof e&&e({stage:t,...n}),s(t,n)}}(c),w=function(e){const t=document.querySelector(e||"[data-wh]");if(!t)return{show:()=>{},hide:()=>{}};const n=document.createElement("div");return n.setAttribute("data-wh-loader",""),n.style.cssText="display:flex;align-items:center;justify-content:center;padding:24px;width:100%;box-sizing:border-box;",n.innerHTML='<div style="width:28px;height:28px;border:3px solid #e5e7eb;border-top-color:#6366f1;border-radius:50%;animation:wh-spin 0.7s linear infinite;"></div><style>@keyframes wh-spin{to{transform:rotate(360deg);}}</style>',{show(){t.appendChild(n)},hide(){n.parentNode&&n.parentNode.removeChild(n)}}}(a),g=performance.now();h("start",{cdnUrl:t,selector:a}),w.show(),i("loads",1);const b=`${t}@${o}`;try{h("fetching");const{data:c,source:v}=await f(b,()=>m(t,r,o));let x;try{x=JSON.parse(c)}catch(e){x={}}x.assets&&x.assets.length&&(h("assets",{count:x.assets.length}),await p(x.assets));const C=[...d,...x.dependencies||[]];if(C.length){h("deps",{count:C.length});for(const e of C)"object"==typeof e&&e.cdnUrl&&await y(e,n)}h("injecting"),w.hide(),await async function(t,n,r,o={}){const{sandbox:a=!1,allowedDomains:c,beforeMount:i,afterMount:d}=o,u=document.querySelector(r||"[data-wh]");if(!u)return void console.warn("[WebHanger] No mount target found.");let h;try{h=JSON.parse(t)}catch(e){return void console.error("[WebHanger] Invalid payload.")}if(!function(t){if(!t||!t.length)return!0;const n=e.location?.hostname||"";return t.some(e=>n===e||n.endsWith("."+e))}(c))return console.error("[WebHanger] Domain not allowed: "+e.location?.hostname),void s("error",{reason:"domain_restricted"});const f=await l(h.c,n,"::css"),m=await l(h.h,n,"::html"),w=await l(h.j,n,"::js");if(h.integrity&&(m||w)){const e=await async function(e,t,n,r){if(!r)return!0;const o=(new TextEncoder).encode(e+t+n),a=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(a)).map(e=>e.toString(16).padStart(2,"0")).join("")===r}(m,f,w,h.integrity);if(!e)return console.error("[WebHanger] Integrity check failed \u2014 bundle may be tampered or re-deploy needed."),void s("error",{reason:"integrity_failed"})}if("function"==typeof i&&i({target:u,html:m,css:f}),s("beforeMount",{target:u,selector:r}),a)!function(e,t,n){const r=e.attachShadow({mode:"closed"});if(n){const e=document.createElement("style");e.textContent=n,r.appendChild(e)}if(t){const e=document.createElement("div");e.innerHTML=t,r.appendChild(e)}}(u,m,f);else{if(f){const e=document.createElement("style");e.textContent=f,document.head.appendChild(e)}m&&(u.innerHTML=m)}if(w){const e=document.createElement("script");e.textContent=w,document.head.appendChild(e),document.head.removeChild(e)}"function"==typeof d&&d({target:u}),s("afterMount",{target:u,selector:r})}(c,n,a,u);const S=Math.round(performance.now()-g);i("totalTime",S),h("done",{time:S,source:v}),s("load",{cdnUrl:t,time:S,source:v,selector:a})}catch(e){w.hide(),i("errors",1),h("error",{message:e.message}),"function"==typeof u.onError&&u.onError(e),console.error("[WebHanger] Load failed:",e.message)}}let b=null,v="./wh-manifest.json";"undefined"!=typeof customElements&&customElements.define("wh-component",class extends HTMLElement{async connectedCallback(){b&&await this._load()}async _load(){const e=this.getAttribute("name"),t=this.hasAttribute("sandbox");if(!e)return void console.error("[WebHanger] <wh-component> missing 'name' attribute");const n=b||await fetch(this.getAttribute("src")||v).then(e=>e.json()),r=n.components[e];if(!r)return void console.error(`[WebHanger] <wh-component>: "${e}" not found in manifest`);this.setAttribute("data-wh-el",e);const o=`wh-component[data-wh-el="${e}"]`;await g(r.urls||r.url,n.pid,r.token,r.expires,o,null,[],{sandbox:t})}}),e.WebHangerFront={load:g,clearCache:async function(){Object.keys(localStorage).filter(e=>e.startsWith(n)).forEach(e=>localStorage.removeItem(e));try{(await u()).transaction(t,"readwrite").objectStore(t).clear()}catch(e){}if("caches"in window){const e=await caches.keys();await Promise.all(e.map(e=>caches.delete(e)))}if("serviceWorker"in navigator){const e=await navigator.serviceWorker.getRegistrations();await Promise.all(e.map(e=>e.unregister()))}Object.keys(sessionStorage).filter(e=>e.startsWith(n)).forEach(e=>sessionStorage.removeItem(e))},use:function(e){"function"==typeof e.install&&e.install({on:a,emit:s}),r.push(e)},on:a,metrics:c,initialize:async function(e="./wh-manifest.json"){v=e;const t=await fetch(e);b=await t.json(),document.querySelectorAll("wh-component[name]").forEach(e=>e._load())},version:"2.0.0"}}(window);
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webhanger-front",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "WebHanger browser SDK — load encrypted UI components from CDN",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
7
7
|
"browser": "browser.min.js",
|
|
8
8
|
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"browser.min.js",
|
|
11
|
+
"index.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
9
14
|
"scripts": {
|
|
10
15
|
"build": "node build.js",
|
|
11
16
|
"prepublishOnly": "node build.js"
|
package/browser.js
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// WebHanger Frontend SDK — IIFE browser build
|
|
2
|
-
// Use via <script src="browser.js"> or from a CDN
|
|
3
|
-
|
|
4
|
-
(function (global) {
|
|
5
|
-
const VERSION = "1.0.0";
|
|
6
|
-
const IDB_NAME = "webhanger_cache";
|
|
7
|
-
const IDB_STORE = "components";
|
|
8
|
-
const LS_PREFIX = "wh_";
|
|
9
|
-
const SIZE_THRESHOLD = 50 * 1024;
|
|
10
|
-
|
|
11
|
-
// UTF-8 safe XOR decrypt — mirrors bundler byte-level encoding
|
|
12
|
-
function decrypt(b64, projectId, salt) {
|
|
13
|
-
if (!b64) return "";
|
|
14
|
-
const key = projectId + salt;
|
|
15
|
-
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
16
|
-
const keyBytes = new TextEncoder().encode(key);
|
|
17
|
-
const out = new Uint8Array(bytes.length);
|
|
18
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
-
out[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
|
|
20
|
-
}
|
|
21
|
-
return new TextDecoder().decode(out);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function openIDB() {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const req = indexedDB.open(IDB_NAME, 1);
|
|
27
|
-
req.onupgradeneeded = (e) => e.target.result.createObjectStore(IDB_STORE);
|
|
28
|
-
req.onsuccess = (e) => resolve(e.target.result);
|
|
29
|
-
req.onerror = () => reject(req.error);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function idbGet(key) {
|
|
34
|
-
const db = await openIDB();
|
|
35
|
-
return new Promise((resolve, reject) => {
|
|
36
|
-
const tx = db.transaction(IDB_STORE, "readonly");
|
|
37
|
-
const req = tx.objectStore(IDB_STORE).get(key);
|
|
38
|
-
req.onsuccess = () => resolve(req.result);
|
|
39
|
-
req.onerror = () => reject(req.error);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function idbSet(key, value) {
|
|
44
|
-
const db = await openIDB();
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
47
|
-
const req = tx.objectStore(IDB_STORE).put(value, key);
|
|
48
|
-
req.onsuccess = () => resolve();
|
|
49
|
-
req.onerror = () => reject(req.error);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function cacheGet(key) {
|
|
54
|
-
try {
|
|
55
|
-
const ls = localStorage.getItem(LS_PREFIX + key);
|
|
56
|
-
if (ls) return ls;
|
|
57
|
-
} catch (_) {}
|
|
58
|
-
return await idbGet(key);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function cacheSet(key, value) {
|
|
62
|
-
try {
|
|
63
|
-
if (value.length < SIZE_THRESHOLD) {
|
|
64
|
-
localStorage.setItem(LS_PREFIX + key, value);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
} catch (_) {}
|
|
68
|
-
await idbSet(key, value);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function fetchComponent(cdnUrl, token, expires) {
|
|
72
|
-
const url = expires
|
|
73
|
-
? `${cdnUrl}?token=${token}&expires=${expires}`
|
|
74
|
-
: `${cdnUrl}?token=${token}`;
|
|
75
|
-
const res = await fetch(url);
|
|
76
|
-
if (!res.ok) throw new Error(`Failed to fetch component: ${res.status}`);
|
|
77
|
-
return await res.text();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ─── Load external CDN assets ─────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
function loadAsset(asset) {
|
|
83
|
-
return new Promise((resolve) => {
|
|
84
|
-
const existing = asset.type === "style"
|
|
85
|
-
? document.querySelector(`link[href="${asset.url}"]`)
|
|
86
|
-
: document.querySelector(`script[src="${asset.url}"]`);
|
|
87
|
-
if (existing) return resolve();
|
|
88
|
-
|
|
89
|
-
if (asset.type === "style") {
|
|
90
|
-
const link = document.createElement("link");
|
|
91
|
-
link.rel = "stylesheet";
|
|
92
|
-
link.href = asset.url;
|
|
93
|
-
link.onload = resolve;
|
|
94
|
-
link.onerror = resolve;
|
|
95
|
-
document.head.appendChild(link);
|
|
96
|
-
} else {
|
|
97
|
-
const script = document.createElement("script");
|
|
98
|
-
script.src = asset.url;
|
|
99
|
-
if (asset.defer) script.defer = true;
|
|
100
|
-
if (asset.async) script.async = true;
|
|
101
|
-
script.onload = resolve;
|
|
102
|
-
script.onerror = resolve;
|
|
103
|
-
document.head.appendChild(script);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function loadAssets(assets = []) {
|
|
109
|
-
for (const asset of assets) await loadAsset(asset);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function injectComponent(encryptedPayload, projectId, targetSelector) {
|
|
113
|
-
const target = document.querySelector(targetSelector || "[data-wh]");
|
|
114
|
-
if (!target) { console.warn("[WebHanger] No mount target found."); return; }
|
|
115
|
-
|
|
116
|
-
let payload;
|
|
117
|
-
try { payload = JSON.parse(encryptedPayload); }
|
|
118
|
-
catch (_) { console.error("[WebHanger] Invalid component payload."); return; }
|
|
119
|
-
|
|
120
|
-
const css = decrypt(payload.c, projectId, "::css");
|
|
121
|
-
if (css) {
|
|
122
|
-
const style = document.createElement("style");
|
|
123
|
-
style.textContent = css;
|
|
124
|
-
document.head.appendChild(style);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const html = decrypt(payload.h, projectId, "::html");
|
|
128
|
-
if (html) target.innerHTML = html;
|
|
129
|
-
|
|
130
|
-
const js = decrypt(payload.j, projectId, "::js");
|
|
131
|
-
if (js) {
|
|
132
|
-
const script = document.createElement("script");
|
|
133
|
-
script.textContent = js;
|
|
134
|
-
document.head.appendChild(script);
|
|
135
|
-
document.head.removeChild(script);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ─── Loading Signaler ─────────────────────────────────────────────────────
|
|
140
|
-
// Fires callbacks at each stage of the load lifecycle.
|
|
141
|
-
// Stages: "start" | "fetching" | "assets" | "injecting" | "done" | "error"
|
|
142
|
-
|
|
143
|
-
function createSignaler(onSignal) {
|
|
144
|
-
return function signal(stage, detail = {}) {
|
|
145
|
-
if (typeof onSignal === "function") {
|
|
146
|
-
onSignal({ stage, ...detail });
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ─── Built-in preloader ───────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
function createPreloader(selector) {
|
|
154
|
-
const target = document.querySelector(selector || "[data-wh]");
|
|
155
|
-
if (!target) return { show: () => {}, hide: () => {} };
|
|
156
|
-
|
|
157
|
-
const loader = document.createElement("div");
|
|
158
|
-
loader.setAttribute("data-wh-loader", "");
|
|
159
|
-
loader.style.cssText = `
|
|
160
|
-
display:flex; align-items:center; justify-content:center;
|
|
161
|
-
padding: 24px; width:100%; box-sizing:border-box;
|
|
162
|
-
`;
|
|
163
|
-
loader.innerHTML = `
|
|
164
|
-
<div style="
|
|
165
|
-
width:28px; height:28px;
|
|
166
|
-
border:3px solid #e5e7eb;
|
|
167
|
-
border-top-color:#6366f1;
|
|
168
|
-
border-radius:50%;
|
|
169
|
-
animation:wh-spin 0.7s linear infinite;
|
|
170
|
-
"></div>
|
|
171
|
-
<style>
|
|
172
|
-
@keyframes wh-spin { to { transform: rotate(360deg); } }
|
|
173
|
-
</style>
|
|
174
|
-
`;
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
show() { target.appendChild(loader); },
|
|
178
|
-
hide() { if (loader.parentNode) loader.parentNode.removeChild(loader); }
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function load(cdnUrl, projectId, token, expires, selector = "[data-wh]", onSignal = null) {
|
|
183
|
-
if (!cdnUrl || !projectId || !token || expires === undefined || expires === null) {
|
|
184
|
-
console.error("[WebHanger] Missing required params: cdnUrl, projectId, token, expires");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (expires !== 0 && Math.floor(Date.now() / 1000) > expires) {
|
|
188
|
-
console.warn("[WebHanger] Token expired.");
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const signal = createSignaler(onSignal);
|
|
193
|
-
const preloader = createPreloader(selector);
|
|
194
|
-
|
|
195
|
-
signal("start", { cdnUrl, selector });
|
|
196
|
-
preloader.show();
|
|
197
|
-
|
|
198
|
-
const cacheKey = `${cdnUrl}@${expires}`;
|
|
199
|
-
try {
|
|
200
|
-
signal("fetching");
|
|
201
|
-
let payload = await cacheGet(cacheKey);
|
|
202
|
-
if (!payload) {
|
|
203
|
-
payload = await fetchComponent(cdnUrl, token, expires);
|
|
204
|
-
await cacheSet(cacheKey, payload);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let parsed;
|
|
208
|
-
try { parsed = JSON.parse(payload); } catch (_) { parsed = {}; }
|
|
209
|
-
|
|
210
|
-
if (parsed.assets && parsed.assets.length) {
|
|
211
|
-
signal("assets", { count: parsed.assets.length, assets: parsed.assets });
|
|
212
|
-
await loadAssets(parsed.assets);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
signal("injecting");
|
|
216
|
-
preloader.hide();
|
|
217
|
-
injectComponent(payload, projectId, selector);
|
|
218
|
-
signal("done");
|
|
219
|
-
} catch (err) {
|
|
220
|
-
preloader.hide();
|
|
221
|
-
signal("error", { message: err.message });
|
|
222
|
-
console.error("[WebHanger] Load failed:", err.message);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function clearCache() {
|
|
227
|
-
// 1. Clear all wh_ keys from localStorage
|
|
228
|
-
Object.keys(localStorage)
|
|
229
|
-
.filter(k => k.startsWith(LS_PREFIX))
|
|
230
|
-
.forEach(k => localStorage.removeItem(k));
|
|
231
|
-
|
|
232
|
-
// 2. Clear IndexedDB store
|
|
233
|
-
try {
|
|
234
|
-
const db = await openIDB();
|
|
235
|
-
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
236
|
-
tx.objectStore(IDB_STORE).clear();
|
|
237
|
-
} catch (_) {}
|
|
238
|
-
|
|
239
|
-
// 3. Clear service worker caches
|
|
240
|
-
if ("caches" in window) {
|
|
241
|
-
const keys = await caches.keys();
|
|
242
|
-
await Promise.all(keys.map(k => caches.delete(k)));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 4. Unregister service workers
|
|
246
|
-
if ("serviceWorker" in navigator) {
|
|
247
|
-
const regs = await navigator.serviceWorker.getRegistrations();
|
|
248
|
-
await Promise.all(regs.map(r => r.unregister()));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// 5. Clear sessionStorage wh_ keys
|
|
252
|
-
Object.keys(sessionStorage)
|
|
253
|
-
.filter(k => k.startsWith(LS_PREFIX))
|
|
254
|
-
.forEach(k => sessionStorage.removeItem(k));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
global.WebHangerFront = { load, clearCache, version: VERSION };
|
|
258
|
-
|
|
259
|
-
})(window);
|
package/build.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { minify } from "terser";
|
|
2
|
-
import fs from "fs-extra";
|
|
3
|
-
|
|
4
|
-
const src = await fs.readFile("./browser.js", "utf-8");
|
|
5
|
-
|
|
6
|
-
const result = await minify(src, {
|
|
7
|
-
compress: {
|
|
8
|
-
dead_code: true,
|
|
9
|
-
drop_console: false,
|
|
10
|
-
passes: 3,
|
|
11
|
-
unsafe: true,
|
|
12
|
-
unsafe_math: true,
|
|
13
|
-
pure_getters: true
|
|
14
|
-
},
|
|
15
|
-
mangle: {
|
|
16
|
-
toplevel: true, // mangle top-level names
|
|
17
|
-
properties: false // keep property names (needed for API surface)
|
|
18
|
-
},
|
|
19
|
-
format: {
|
|
20
|
-
comments: false, // strip all comments
|
|
21
|
-
ascii_only: true // ensure safe output
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
await fs.writeFile("./browser.min.js", result.code, "utf-8");
|
|
26
|
-
|
|
27
|
-
const orig = (await fs.stat("./browser.js")).size;
|
|
28
|
-
const mini = Buffer.byteLength(result.code);
|
|
29
|
-
console.log(`✅ Minified: ${(orig/1024).toFixed(1)}kB → ${(mini/1024).toFixed(1)}kB (${Math.round((1-mini/orig)*100)}% reduction)`);
|