baremetal.js 1.2.3 → 1.2.5
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/CHANGELOG.md +5 -0
- package/dist/baremetal.js +22 -2
- package/dist/baremetal.min.js +1 -1
- package/docs/api.md +32 -0
- package/package.json +1 -1
- package/src/loader.js +12 -1
- package/src/router.js +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.5] - 2026-07-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Added `onCleanup` hook for safe module cleanup. Implement a cleanup registry that allows modules to register cleanup callbacks via `context.onCleanup()` instead of explicitly returning a destroy function.
|
|
12
|
+
|
|
8
13
|
## [1.2.3] - 2026-06-19
|
|
9
14
|
|
|
10
15
|
### Added
|
package/dist/baremetal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* baremetal.js v1.2.
|
|
2
|
+
* baremetal.js v1.2.5
|
|
3
3
|
* A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
|
|
4
4
|
* (c) 2026 dkydivyansh
|
|
5
5
|
* Released under the GPL-3.0 License
|
|
@@ -194,11 +194,22 @@ const Loader = {
|
|
|
194
194
|
context.virtualize = virtualize;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Safe Cleanup Registry
|
|
198
|
+
const cleanups = [];
|
|
199
|
+
context.onCleanup = (cb) => cleanups.push(cb);
|
|
200
|
+
|
|
197
201
|
const instance = await module.mount(context);
|
|
198
202
|
|
|
203
|
+
const autoDestroy = () => {
|
|
204
|
+
cleanups.forEach(cb => { try { cb(); } catch(e) { console.error(e); } });
|
|
205
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
206
|
+
try { instance.destroy(); } catch(e) { console.error(e); }
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
199
210
|
this.activeModules[key] = {
|
|
200
211
|
path: path,
|
|
201
|
-
module:
|
|
212
|
+
module: { destroy: autoDestroy }
|
|
202
213
|
};
|
|
203
214
|
} else {
|
|
204
215
|
console.error(`[BareMetal] Module ${path} failed to provide a mount function even after wrapping.`);
|
|
@@ -364,6 +375,15 @@ const Router = {
|
|
|
364
375
|
Loader.log(`Used pre-fetched cache for ${url}`);
|
|
365
376
|
} else {
|
|
366
377
|
const response = await fetch(url, { signal });
|
|
378
|
+
|
|
379
|
+
if (response.redirected) {
|
|
380
|
+
const newUrl = new URL(response.url);
|
|
381
|
+
Loader.log(`Redirect detected to ${newUrl.pathname}. Continuing SPA transition to new destination.`);
|
|
382
|
+
// Sync the URL bar and router state to the redirect destination
|
|
383
|
+
history.replaceState(null, '', newUrl.pathname + newUrl.search);
|
|
384
|
+
this.lastPathAndSearch = newUrl.pathname + newUrl.search;
|
|
385
|
+
}
|
|
386
|
+
|
|
367
387
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
368
388
|
htmlText = await response.text();
|
|
369
389
|
}
|
package/dist/baremetal.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const t=new class{constructor(){this.state={},this.listeners={},this.eventBus={}}initPersistence(){try{const t=sessionStorage.getItem("baremetal_state");if(t){const e=JSON.parse(t);for(const[t,i]of Object.entries(e))this.state[t]=i}}catch(t){console.warn("Failed to hydrate state",t)}}init(t,e){void 0===this.state[t]&&(this.state[t]=e)}subscribe(t,e){return this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e),void 0!==this.state[t]&&e(this.state[t]),()=>this.unsubscribe(t,e)}unsubscribe(t,e){this.listeners[t]&&(this.listeners[t]=this.listeners[t].filter(t=>t!==e))}update(t,e){if(this.state[t]=e,this.listeners[t]&&this.listeners[t].forEach(t=>t(e)),window.__baremetal_persist_state)try{sessionStorage.setItem("baremetal_state",JSON.stringify(this.state))}catch(t){}}get(t){return this.state[t]}on(t,e){return this.eventBus[t]||(this.eventBus[t]=[]),this.eventBus[t].push(e),()=>this.off(t,e)}off(t,e){this.eventBus[t]&&(this.eventBus[t]=this.eventBus[t].filter(t=>t!==e))}publish(t,e){this.eventBus[t]&&this.eventBus[t].forEach(t=>t(e))}},e={activeModules:{},config:{keepAliveSameModules:!0,debug:!1,autoWrap:!0,hoverPrefetch:!1,showErrorNotification:!1,persistState:!1,virtualizeDom:!1,transition:{enabled:!1,simulatedDelay:0,module:null,useViewTransitions:!1}},setConfig(t){this.config={...this.config,...t}},log(...t){this.config.debug&&console.log("[BareMetal Loader]",...t)},async prepare(t){if(this.config.transition&&this.config.transition.enabled){const e=this.config.transition.module||"/src/transition.js";t.__baremetal_transition=e}const e={},i=[],o={};for(const[o,s]of Object.entries(this.activeModules)){const n="__baremetal_transition"===o,r="string"==typeof t[o]?t[o]:t[o]?t[o].path:null;(this.config.keepAliveSameModules||n)&&r===s.path?(e[o]=s,this.log(`Keep-Alive: ${o} (${s.path})`)):i.push(s)}for(const t of i)if(this.log(`Destroying module: ${t.path}`),t.module&&"function"==typeof t.module.destroy)try{t.module.destroy()}catch(e){console.error(`Error destroying module ${t.path}`,e)}for(const[i,s]of Object.entries(t))e[i]||(o[i]=s);return this.activeModules=e,o},async loadPrepared(e){const i=Object.keys(e).length;let o=0;const s=Object.entries(e).map(async([e,s])=>{const n="string"==typeof s?s:s.path,a="string"==typeof s?null:s.lazy;this.log(`Preparing module: ${n}`);const l=async()=>{try{const i=new URL(n,document.baseURI).href,o=this.config.debug?`${i}?t=${Date.now()}`:i;let s;if(this.config.autoWrap){const t=await fetch(o);if(!t.ok)throw new Error(`Failed to fetch ${o}`);const e=await t.text();if(/export\s+(function|const|let|var)\s+mount\b/.test(e)||/export\s+\{.*?\bmount\b.*?\}/.test(e))s=await import(o);else{console.warn(`[BareMetal] WARNING: Module ${n} does not explicitly export a mount() function. Auto-wrapping it...`);const t=`export async function mount(context) { const { state } = context; ${e} }`,i=new Blob([t],{type:"application/javascript"}),o=URL.createObjectURL(i);s=await import(o),URL.revokeObjectURL(o)}}else s=await import(o);if("function"==typeof s.mount){this.log(`Mounting module: ${n}`);const i={state:t};if(this.config.virtualizeDom){const{virtualize:t}=await Promise.resolve().then(function(){return r});i.virtualize=t}const o=await s.mount(i);this.activeModules[e]={path:n,module:o?{destroy:o.destroy}:s}}else console.error(`[BareMetal] Module ${n} failed to provide a mount function even after wrapping.`)}catch(t){console.error(`Failed to load module: ${n}`,t)}};if(a){const e=document.querySelector(a);if(e&&window.IntersectionObserver){this.log(`Deferred loading of module ${n} until ${a} is visible`);const s=new IntersectionObserver(t=>{t[0].isIntersecting&&(s.disconnect(),l())});if(s.observe(e),o++,i>0){const e=50+o/i*50;t.publish("ROUTE_PROGRESS",{url:window.location.pathname,progress:e})}return Promise.resolve()}await l()}else await l();if(o++,i>0){const e=50+o/i*50;t.publish("ROUTE_PROGRESS",{url:window.location.pathname,progress:e})}});await Promise.all(s),t.publish("ROUTE_END",{url:window.location.pathname})},async load(t){const e=await this.prepare(t);await this.loadPrepared(e)}};function i(t){return e.load(t)}const o={htmlCache:{},scrollMemory:{},historyStack:[],currentAbortController:null,lastPathAndSearch:"",init(){this.lastPathAndSearch=window.location.pathname+window.location.search,"scrollRestoration"in history&&(history.scrollRestoration="manual"),window.addEventListener("popstate",this.handleRoute.bind(this)),document.body.addEventListener("click",t=>{const e=t.target.closest("a");e&&(e.origin!==window.location.origin||"_blank"===e.target||e.rel&&e.rel.includes("noreferrer")||e.hasAttribute("download")||(e.getAttribute("href")||"").startsWith("#")||(t.preventDefault(),this.historyStack.push(window.location.pathname),this.scrollMemory[window.location.pathname]=window.scrollY,history.pushState(null,"",e.href),this.handleRoute()))}),document.body.addEventListener("mouseover",t=>{if(!e.config.hoverPrefetch)return;const i=t.target.closest("a");i&&(i.origin!==window.location.origin||"_blank"===i.target||i.hasAttribute("download")||(i.getAttribute("href")||"").startsWith("#")||this.htmlCache[i.href]||(this.htmlCache[i.href]="fetching",fetch(i.href).then(t=>{if(t.ok)return t.text();throw new Error("Failed to prefetch")}).then(t=>this.htmlCache[i.href]=t).catch(()=>delete this.htmlCache[i.href])))})},back(){if(this.historyStack.length>0){const t=this.historyStack.pop();history.pushState(null,"",t),this.handleRoute()}else history.back()},reload(){window.location.reload()},async handleRoute(i){const o=window.location.pathname+window.location.search;if(this.lastPathAndSearch===o)return;this.lastPathAndSearch=o,this.currentAbortController&&this.currentAbortController.abort(),this.currentAbortController=new AbortController;const s=this.currentAbortController.signal,n=window.location.pathname;try{let o;e.log(`Navigating to ${n}`),t.publish("ROUTE_START",{url:n}),e.config.transition&&e.config.transition.simulatedDelay&&(t.publish("ROUTE_PROGRESS",{url:n,progress:10}),await new Promise(t=>setTimeout(t,e.config.transition.simulatedDelay/2)),t.publish("ROUTE_PROGRESS",{url:n,progress:30}),await new Promise(t=>setTimeout(t,e.config.transition.simulatedDelay/2)));const r=new URL(n,document.baseURI).href;if(e.config.hoverPrefetch&&this.htmlCache[r]&&"fetching"!==this.htmlCache[r])o=this.htmlCache[r],e.log(`Used pre-fetched cache for ${n}`);else{const t=await fetch(n,{signal:s});if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);o=await t.text()}t.publish("ROUTE_PROGRESS",{url:n,progress:50});const a=(new DOMParser).parseFromString(o,"text/html");let l=null;const c=a.querySelectorAll("script");for(const t of c)if(t.textContent.includes("loader(")){const e=t.textContent.match(/loader\s*\(\s*(\{[\s\S]*?\})\s*\)/);if(e&&e[1])try{l=new Function("return "+e[1])()}catch(i){console.error("Failed to parse loader config in new page",i)}}if(!l)return e.log(`No BareMetal config found on ${n}. Falling back to native navigation.`),void window.location.assign(n);const h=await e.prepare(l);document.title=a.title;document.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]').forEach(t=>t.remove());a.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]').forEach(t=>document.head.appendChild(t.cloneNode(!0)));const d=[];document.querySelectorAll("[data-baremetal-preserve]").forEach(t=>{if(t.id&&a.getElementById(t.id)){const e=document.createElement("div");t.parentNode.replaceChild(e,t),d.push(t)}});const u=document.getElementById("baremetal-transition-root");u&&u.parentNode.removeChild(u);const p=()=>{document.body.innerHTML=a.body.innerHTML,d.forEach(t=>{const e=document.getElementById(t.id);e&&(Array.from(t.attributes).forEach(e=>{"id"!==e.name&&"data-baremetal-preserve"!==e.name&&t.removeAttribute(e.name)}),Array.from(e.attributes).forEach(e=>{"id"!==e.name&&t.setAttribute(e.name,e.value)}),e.parentNode.replaceChild(t,e))}),u&&document.body.appendChild(u),t.publish("DOM_SWAPPED",null)},f=()=>{p(),window.scrollTo(0,this.scrollMemory[n]||0)};e.config.transition&&e.config.transition.useViewTransitions&&document.startViewTransition?document.startViewTransition(()=>{f()}):f(),await e.loadPrepared(h)}catch(i){if("AbortError"===i.name)return void e.log(`Aborted fetch for ${n} due to new navigation.`);if(console.error("Routing error:",i),t.publish("ROUTE_ERROR",{url:n,error:i.message}),e.config.showErrorNotification){if(this.historyStack.length>0){const t=this.historyStack.pop();history.replaceState(null,"",t)}const t=document.createElement("div");t.style.position="fixed",t.style.bottom="20px",t.style.left="20px",t.style.background="#e74c3c",t.style.color="white",t.style.padding="15px 20px",t.style.borderRadius="8px",t.style.boxShadow="0 4px 12px rgba(0,0,0,0.15)",t.style.zIndex="999999",t.style.fontFamily="sans-serif",t.style.transition="opacity 0.3s ease",t.innerHTML=`<strong>Navigation Failed:</strong> ${i.message}`,document.body.appendChild(t),setTimeout(()=>{t.style.opacity="0",setTimeout(()=>t.remove(),300)},4e3)}else{if(this.historyStack.length>0){const t=this.historyStack.pop();history.replaceState(null,"",t)}window.location.assign(n)}}}};class s{constructor(t,e,i,o,s=20){this.container=document.getElementById(t),this.container&&(this.items=e,this.renderRow=i,this.itemHeight=o,this.visibleCount=s,this.totalHeight=this.items.length*this.itemHeight,this.container.style.overflowY="auto",this.container.style.position="relative",this.innerWrapper=document.createElement("div"),this.innerWrapper.style.height=`${this.totalHeight}px`,this.innerWrapper.style.position="relative",this.container.appendChild(this.innerWrapper),this.startIndex=0,this.onScroll=this.onScroll.bind(this),this.container.addEventListener("scroll",this.onScroll),this.render())}onScroll(){const t=this.container.scrollTop,e=Math.max(0,Math.floor(t/this.itemHeight)-2);e!==this.startIndex&&(this.startIndex=e,this.render())}render(){this.innerWrapper.innerHTML="";const t=Math.min(this.items.length-1,this.startIndex+this.visibleCount+4);for(let e=this.startIndex;e<=t;e++){const t=this.renderRow(this.items[e],e);t.style.position="absolute",t.style.top=e*this.itemHeight+"px",t.style.width="100%",this.innerWrapper.appendChild(t)}}destroy(){this.container&&(this.container.removeEventListener("scroll",this.onScroll),this.container.innerHTML="")}}const n=(t,e,i,o,n)=>new s(t,e,i,o,n);var r=Object.freeze({__proto__:null,Virtualizer:s,virtualize:n});const a={state:t,events:t,loader:i,router:o,virtualize:n,init(i={}){void 0!==i.debug&&e.setConfig({debug:i.debug}),void 0!==i.keepAliveSameModules&&e.setConfig({keepAliveSameModules:i.keepAliveSameModules}),void 0!==i.transition&&e.setConfig({transition:i.transition}),void 0!==i.autoWrap&&e.setConfig({autoWrap:i.autoWrap}),void 0!==i.hoverPrefetch&&e.setConfig({hoverPrefetch:i.hoverPrefetch}),void 0!==i.showErrorNotification&&e.setConfig({showErrorNotification:i.showErrorNotification}),void 0!==i.persistState&&(e.setConfig({persistState:i.persistState}),i.persistState&&(window.__baremetal_persist_state=!0,t.initPersistence())),void 0!==i.virtualizeDom&&e.setConfig({virtualizeDom:i.virtualizeDom}),o.init(),e.log("Initialized BareMetal Engine with config:",i)}};export{a as BareMetal,i as loader};
|
|
1
|
+
const t=new class{constructor(){this.state={},this.listeners={},this.eventBus={}}initPersistence(){try{const t=sessionStorage.getItem("baremetal_state");if(t){const e=JSON.parse(t);for(const[t,i]of Object.entries(e))this.state[t]=i}}catch(t){console.warn("Failed to hydrate state",t)}}init(t,e){void 0===this.state[t]&&(this.state[t]=e)}subscribe(t,e){return this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e),void 0!==this.state[t]&&e(this.state[t]),()=>this.unsubscribe(t,e)}unsubscribe(t,e){this.listeners[t]&&(this.listeners[t]=this.listeners[t].filter(t=>t!==e))}update(t,e){if(this.state[t]=e,this.listeners[t]&&this.listeners[t].forEach(t=>t(e)),window.__baremetal_persist_state)try{sessionStorage.setItem("baremetal_state",JSON.stringify(this.state))}catch(t){}}get(t){return this.state[t]}on(t,e){return this.eventBus[t]||(this.eventBus[t]=[]),this.eventBus[t].push(e),()=>this.off(t,e)}off(t,e){this.eventBus[t]&&(this.eventBus[t]=this.eventBus[t].filter(t=>t!==e))}publish(t,e){this.eventBus[t]&&this.eventBus[t].forEach(t=>t(e))}},e={activeModules:{},config:{keepAliveSameModules:!0,debug:!1,autoWrap:!0,hoverPrefetch:!1,showErrorNotification:!1,persistState:!1,virtualizeDom:!1,transition:{enabled:!1,simulatedDelay:0,module:null,useViewTransitions:!1}},setConfig(t){this.config={...this.config,...t}},log(...t){this.config.debug&&console.log("[BareMetal Loader]",...t)},async prepare(t){if(this.config.transition&&this.config.transition.enabled){const e=this.config.transition.module||"/src/transition.js";t.__baremetal_transition=e}const e={},i=[],o={};for(const[o,s]of Object.entries(this.activeModules)){const n="__baremetal_transition"===o,r="string"==typeof t[o]?t[o]:t[o]?t[o].path:null;(this.config.keepAliveSameModules||n)&&r===s.path?(e[o]=s,this.log(`Keep-Alive: ${o} (${s.path})`)):i.push(s)}for(const t of i)if(this.log(`Destroying module: ${t.path}`),t.module&&"function"==typeof t.module.destroy)try{t.module.destroy()}catch(e){console.error(`Error destroying module ${t.path}`,e)}for(const[i,s]of Object.entries(t))e[i]||(o[i]=s);return this.activeModules=e,o},async loadPrepared(e){const i=Object.keys(e).length;let o=0;const s=Object.entries(e).map(async([e,s])=>{const n="string"==typeof s?s:s.path,a="string"==typeof s?null:s.lazy;this.log(`Preparing module: ${n}`);const l=async()=>{try{const i=new URL(n,document.baseURI).href,o=this.config.debug?`${i}?t=${Date.now()}`:i;let s;if(this.config.autoWrap){const t=await fetch(o);if(!t.ok)throw new Error(`Failed to fetch ${o}`);const e=await t.text();if(/export\s+(function|const|let|var)\s+mount\b/.test(e)||/export\s+\{.*?\bmount\b.*?\}/.test(e))s=await import(o);else{console.warn(`[BareMetal] WARNING: Module ${n} does not explicitly export a mount() function. Auto-wrapping it...`);const t=`export async function mount(context) { const { state } = context; ${e} }`,i=new Blob([t],{type:"application/javascript"}),o=URL.createObjectURL(i);s=await import(o),URL.revokeObjectURL(o)}}else s=await import(o);if("function"==typeof s.mount){this.log(`Mounting module: ${n}`);const i={state:t};if(this.config.virtualizeDom){const{virtualize:t}=await Promise.resolve().then(function(){return r});i.virtualize=t}const o=[];i.onCleanup=t=>o.push(t);const a=await s.mount(i),l=()=>{if(o.forEach(t=>{try{t()}catch(t){console.error(t)}}),a&&"function"==typeof a.destroy)try{a.destroy()}catch(t){console.error(t)}};this.activeModules[e]={path:n,module:{destroy:l}}}else console.error(`[BareMetal] Module ${n} failed to provide a mount function even after wrapping.`)}catch(t){console.error(`Failed to load module: ${n}`,t)}};if(a){const e=document.querySelector(a);if(e&&window.IntersectionObserver){this.log(`Deferred loading of module ${n} until ${a} is visible`);const s=new IntersectionObserver(t=>{t[0].isIntersecting&&(s.disconnect(),l())});if(s.observe(e),o++,i>0){const e=50+o/i*50;t.publish("ROUTE_PROGRESS",{url:window.location.pathname,progress:e})}return Promise.resolve()}await l()}else await l();if(o++,i>0){const e=50+o/i*50;t.publish("ROUTE_PROGRESS",{url:window.location.pathname,progress:e})}});await Promise.all(s),t.publish("ROUTE_END",{url:window.location.pathname})},async load(t){const e=await this.prepare(t);await this.loadPrepared(e)}};function i(t){return e.load(t)}const o={htmlCache:{},scrollMemory:{},historyStack:[],currentAbortController:null,lastPathAndSearch:"",init(){this.lastPathAndSearch=window.location.pathname+window.location.search,"scrollRestoration"in history&&(history.scrollRestoration="manual"),window.addEventListener("popstate",this.handleRoute.bind(this)),document.body.addEventListener("click",t=>{const e=t.target.closest("a");e&&(e.origin!==window.location.origin||"_blank"===e.target||e.rel&&e.rel.includes("noreferrer")||e.hasAttribute("download")||(e.getAttribute("href")||"").startsWith("#")||(t.preventDefault(),this.historyStack.push(window.location.pathname),this.scrollMemory[window.location.pathname]=window.scrollY,history.pushState(null,"",e.href),this.handleRoute()))}),document.body.addEventListener("mouseover",t=>{if(!e.config.hoverPrefetch)return;const i=t.target.closest("a");i&&(i.origin!==window.location.origin||"_blank"===i.target||i.hasAttribute("download")||(i.getAttribute("href")||"").startsWith("#")||this.htmlCache[i.href]||(this.htmlCache[i.href]="fetching",fetch(i.href).then(t=>{if(t.ok)return t.text();throw new Error("Failed to prefetch")}).then(t=>this.htmlCache[i.href]=t).catch(()=>delete this.htmlCache[i.href])))})},back(){if(this.historyStack.length>0){const t=this.historyStack.pop();history.pushState(null,"",t),this.handleRoute()}else history.back()},reload(){window.location.reload()},async handleRoute(i){const o=window.location.pathname+window.location.search;if(this.lastPathAndSearch===o)return;this.lastPathAndSearch=o,this.currentAbortController&&this.currentAbortController.abort(),this.currentAbortController=new AbortController;const s=this.currentAbortController.signal,n=window.location.pathname;try{let o;e.log(`Navigating to ${n}`),t.publish("ROUTE_START",{url:n}),e.config.transition&&e.config.transition.simulatedDelay&&(t.publish("ROUTE_PROGRESS",{url:n,progress:10}),await new Promise(t=>setTimeout(t,e.config.transition.simulatedDelay/2)),t.publish("ROUTE_PROGRESS",{url:n,progress:30}),await new Promise(t=>setTimeout(t,e.config.transition.simulatedDelay/2)));const r=new URL(n,document.baseURI).href;if(e.config.hoverPrefetch&&this.htmlCache[r]&&"fetching"!==this.htmlCache[r])o=this.htmlCache[r],e.log(`Used pre-fetched cache for ${n}`);else{const t=await fetch(n,{signal:s});if(t.redirected){const i=new URL(t.url);e.log(`Redirect detected to ${i.pathname}. Continuing SPA transition to new destination.`),history.replaceState(null,"",i.pathname+i.search),this.lastPathAndSearch=i.pathname+i.search}if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);o=await t.text()}t.publish("ROUTE_PROGRESS",{url:n,progress:50});const a=(new DOMParser).parseFromString(o,"text/html");let l=null;const c=a.querySelectorAll("script");for(const t of c)if(t.textContent.includes("loader(")){const e=t.textContent.match(/loader\s*\(\s*(\{[\s\S]*?\})\s*\)/);if(e&&e[1])try{l=new Function("return "+e[1])()}catch(i){console.error("Failed to parse loader config in new page",i)}}if(!l)return e.log(`No BareMetal config found on ${n}. Falling back to native navigation.`),void window.location.assign(n);const h=await e.prepare(l);document.title=a.title;document.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]').forEach(t=>t.remove());a.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]').forEach(t=>document.head.appendChild(t.cloneNode(!0)));const d=[];document.querySelectorAll("[data-baremetal-preserve]").forEach(t=>{if(t.id&&a.getElementById(t.id)){const e=document.createElement("div");t.parentNode.replaceChild(e,t),d.push(t)}});const u=document.getElementById("baremetal-transition-root");u&&u.parentNode.removeChild(u);const p=()=>{document.body.innerHTML=a.body.innerHTML,d.forEach(t=>{const e=document.getElementById(t.id);e&&(Array.from(t.attributes).forEach(e=>{"id"!==e.name&&"data-baremetal-preserve"!==e.name&&t.removeAttribute(e.name)}),Array.from(e.attributes).forEach(e=>{"id"!==e.name&&t.setAttribute(e.name,e.value)}),e.parentNode.replaceChild(t,e))}),u&&document.body.appendChild(u),t.publish("DOM_SWAPPED",null)},f=()=>{p(),window.scrollTo(0,this.scrollMemory[n]||0)};e.config.transition&&e.config.transition.useViewTransitions&&document.startViewTransition?document.startViewTransition(()=>{f()}):f(),await e.loadPrepared(h)}catch(i){if("AbortError"===i.name)return void e.log(`Aborted fetch for ${n} due to new navigation.`);if(console.error("Routing error:",i),t.publish("ROUTE_ERROR",{url:n,error:i.message}),e.config.showErrorNotification){if(this.historyStack.length>0){const t=this.historyStack.pop();history.replaceState(null,"",t)}const t=document.createElement("div");t.style.position="fixed",t.style.bottom="20px",t.style.left="20px",t.style.background="#e74c3c",t.style.color="white",t.style.padding="15px 20px",t.style.borderRadius="8px",t.style.boxShadow="0 4px 12px rgba(0,0,0,0.15)",t.style.zIndex="999999",t.style.fontFamily="sans-serif",t.style.transition="opacity 0.3s ease",t.innerHTML=`<strong>Navigation Failed:</strong> ${i.message}`,document.body.appendChild(t),setTimeout(()=>{t.style.opacity="0",setTimeout(()=>t.remove(),300)},4e3)}else{if(this.historyStack.length>0){const t=this.historyStack.pop();history.replaceState(null,"",t)}window.location.assign(n)}}}};class s{constructor(t,e,i,o,s=20){this.container=document.getElementById(t),this.container&&(this.items=e,this.renderRow=i,this.itemHeight=o,this.visibleCount=s,this.totalHeight=this.items.length*this.itemHeight,this.container.style.overflowY="auto",this.container.style.position="relative",this.innerWrapper=document.createElement("div"),this.innerWrapper.style.height=`${this.totalHeight}px`,this.innerWrapper.style.position="relative",this.container.appendChild(this.innerWrapper),this.startIndex=0,this.onScroll=this.onScroll.bind(this),this.container.addEventListener("scroll",this.onScroll),this.render())}onScroll(){const t=this.container.scrollTop,e=Math.max(0,Math.floor(t/this.itemHeight)-2);e!==this.startIndex&&(this.startIndex=e,this.render())}render(){this.innerWrapper.innerHTML="";const t=Math.min(this.items.length-1,this.startIndex+this.visibleCount+4);for(let e=this.startIndex;e<=t;e++){const t=this.renderRow(this.items[e],e);t.style.position="absolute",t.style.top=e*this.itemHeight+"px",t.style.width="100%",this.innerWrapper.appendChild(t)}}destroy(){this.container&&(this.container.removeEventListener("scroll",this.onScroll),this.container.innerHTML="")}}const n=(t,e,i,o,n)=>new s(t,e,i,o,n);var r=Object.freeze({__proto__:null,Virtualizer:s,virtualize:n});const a={state:t,events:t,loader:i,router:o,virtualize:n,init(i={}){void 0!==i.debug&&e.setConfig({debug:i.debug}),void 0!==i.keepAliveSameModules&&e.setConfig({keepAliveSameModules:i.keepAliveSameModules}),void 0!==i.transition&&e.setConfig({transition:i.transition}),void 0!==i.autoWrap&&e.setConfig({autoWrap:i.autoWrap}),void 0!==i.hoverPrefetch&&e.setConfig({hoverPrefetch:i.hoverPrefetch}),void 0!==i.showErrorNotification&&e.setConfig({showErrorNotification:i.showErrorNotification}),void 0!==i.persistState&&(e.setConfig({persistState:i.persistState}),i.persistState&&(window.__baremetal_persist_state=!0,t.initPersistence())),void 0!==i.virtualizeDom&&e.setConfig({virtualizeDom:i.virtualizeDom}),o.init(),e.log("Initialized BareMetal Engine with config:",i)}};export{a as BareMetal,i as loader};
|
package/docs/api.md
CHANGED
|
@@ -62,6 +62,38 @@ Listens for a Pub/Sub event.
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
+
## 4. Module Auto-Cleanup & Sandboxing
|
|
66
|
+
|
|
67
|
+
BareMetal provides a safe cleanup registry during module `mount` to help manage memory and global pollution, reducing boilerplate.
|
|
68
|
+
|
|
69
|
+
### `context.onCleanup(callback)`
|
|
70
|
+
Instead of explicitly returning a `destroy` function from your module, you can register cleanup tasks using `onCleanup`. The engine will automatically execute all registered callbacks when the module unmounts.
|
|
71
|
+
|
|
72
|
+
This is highly recommended for cleaning up:
|
|
73
|
+
1. Global `window` function assignments.
|
|
74
|
+
2. `window.addEventListener` / `document.addEventListener`.
|
|
75
|
+
3. Local class instances (like third-party editors).
|
|
76
|
+
|
|
77
|
+
**Example:**
|
|
78
|
+
```javascript
|
|
79
|
+
export function mount({ state, onCleanup }) {
|
|
80
|
+
const chart = new Chart(document.getElementById('chart'), config);
|
|
81
|
+
|
|
82
|
+
window.myCustomFunc = () => console.log('hello');
|
|
83
|
+
|
|
84
|
+
// Register manual cleanups
|
|
85
|
+
onCleanup(() => {
|
|
86
|
+
chart.destroy();
|
|
87
|
+
delete window.myCustomFunc;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Alternatively, you can still return an explicit destroy function:
|
|
91
|
+
// return { destroy: () => { ... } };
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
65
97
|
## 4. Writing Custom Page Transitions
|
|
66
98
|
|
|
67
99
|
You can replace the default progress bar and fade overlay by pointing `config.transition.module` to your own JavaScript file. A custom transition is just a standard BareMetal module that listens to routing events.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baremetal.js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
package/src/loader.js
CHANGED
|
@@ -105,11 +105,22 @@ export const Loader = {
|
|
|
105
105
|
context.virtualize = virtualize;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Safe Cleanup Registry
|
|
109
|
+
const cleanups = [];
|
|
110
|
+
context.onCleanup = (cb) => cleanups.push(cb);
|
|
111
|
+
|
|
108
112
|
const instance = await module.mount(context);
|
|
109
113
|
|
|
114
|
+
const autoDestroy = () => {
|
|
115
|
+
cleanups.forEach(cb => { try { cb(); } catch(e) { console.error(e); } });
|
|
116
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
117
|
+
try { instance.destroy(); } catch(e) { console.error(e); }
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
110
121
|
this.activeModules[key] = {
|
|
111
122
|
path: path,
|
|
112
|
-
module:
|
|
123
|
+
module: { destroy: autoDestroy }
|
|
113
124
|
};
|
|
114
125
|
} else {
|
|
115
126
|
console.error(`[BareMetal] Module ${path} failed to provide a mount function even after wrapping.`);
|
package/src/router.js
CHANGED
|
@@ -110,6 +110,15 @@ export const Router = {
|
|
|
110
110
|
Loader.log(`Used pre-fetched cache for ${url}`);
|
|
111
111
|
} else {
|
|
112
112
|
const response = await fetch(url, { signal });
|
|
113
|
+
|
|
114
|
+
if (response.redirected) {
|
|
115
|
+
const newUrl = new URL(response.url);
|
|
116
|
+
Loader.log(`Redirect detected to ${newUrl.pathname}. Continuing SPA transition to new destination.`);
|
|
117
|
+
// Sync the URL bar and router state to the redirect destination
|
|
118
|
+
history.replaceState(null, '', newUrl.pathname + newUrl.search);
|
|
119
|
+
this.lastPathAndSearch = newUrl.pathname + newUrl.search;
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
114
123
|
htmlText = await response.text();
|
|
115
124
|
}
|