baremetal.js 1.2.1 → 1.2.3

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 CHANGED
@@ -5,6 +5,17 @@ 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.3] - 2026-06-19
9
+
10
+ ### Added
11
+ - Added comprehensive documentation pages for `BareMetal.init()` configuration options (`api-config.html`).
12
+
13
+ ### Changed
14
+ - Updated the documentation site to dog-food the live jsdelivr CDN for its BareMetal imports.
15
+
16
+ ### Fixed
17
+ - Fixed the router to correctly ignore anchor links and navigations that only modify the URL hash fragment (`#hash`), preventing unnecessary page reloads and pre-fetches.
18
+
8
19
  ## [1.2.0] - 2026-06-18
9
20
 
10
21
  ### Added
package/README.md CHANGED
@@ -26,12 +26,22 @@ Load heavy widgets only when they scroll into view! The Loader supports an `Inte
26
26
  Never lose state on a hard reload again. If enabled, BareMetal automatically serializes the `stateManager` to `sessionStorage` in real-time. If the user hits F5, the engine instantly hydrates the state and bounces right back to where they were!
27
27
 
28
28
  ### DOM Virtualization Helper
29
- Rendering 10,000 table rows? BareMetal exposes a `Virtualizer` class that recycles DOM nodes to render massive lists with zero jank. When enabled globally, the engine automatically injects the `virtualize` helper directly into your module's `mount` context!.
30
- - **Scroll Memory & Programmatic Back:** Maintains a persistent history stack, restoring your exact scroll depth instantly when navigating backward.
31
- - **Reactive State Management:** Includes a built-in publish/subscribe Signals pattern, preventing race conditions and keeping your UI synced.
32
- - **Custom Page Transitions:** Build and integrate your own loading animations and transition effects hooking into the routing lifecycle.
33
- - **Error Boundaries:** Handles navigation failures gracefully with configurable fallback UIs.
34
- - **Auto-Wrap Module Loader:** Can automatically wrap simple scripts into conformant lifecycle modules to prevent errors.
29
+ Rendering 10,000 table rows? BareMetal exposes a `Virtualizer` class that recycles DOM nodes to render massive lists with zero jank. When enabled globally, the engine automatically injects the `virtualize` helper directly into your module's `mount` context.
30
+
31
+ ### Scroll Memory & Programmatic Back
32
+ Maintains a persistent history stack, restoring your exact scroll depth instantly when navigating backward.
33
+
34
+ ### Reactive State Management
35
+ Includes a built-in publish/subscribe Signals pattern, preventing race conditions and keeping your UI synced.
36
+
37
+ ### Custom Page Transitions
38
+ Build and integrate your own loading animations and transition effects hooking into the routing lifecycle.
39
+
40
+ ### Error Boundaries
41
+ Handles navigation failures gracefully with configurable fallback UIs.
42
+
43
+ ### Auto-Wrap Module Loader
44
+ Can automatically wrap simple scripts into conformant lifecycle modules to prevent errors.
35
45
 
36
46
  ## Quick Start
37
47
 
package/dist/baremetal.js CHANGED
@@ -1,12 +1,5 @@
1
1
  /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
- /**
9
- * baremetal.js v1.2.1
2
+ * baremetal.js v1.2.3
10
3
  * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
11
4
  * (c) 2026 dkydivyansh
12
5
  * Released under the GPL-3.0 License
@@ -66,7 +59,7 @@ class StateManager {
66
59
  if (window.__baremetal_persist_state) {
67
60
  try {
68
61
  sessionStorage.setItem('baremetal_state', JSON.stringify(this.state));
69
- } catch(e) {}
62
+ } catch (e) { }
70
63
  }
71
64
  }
72
65
 
@@ -96,14 +89,6 @@ class StateManager {
96
89
 
97
90
  const stateManager = new StateManager();
98
91
 
99
- /**
100
- * baremetal.js v1.2.1
101
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
102
- * (c) 2026 dkydivyansh
103
- * Released under the GPL-3.0 License
104
- */
105
-
106
-
107
92
  const Loader = {
108
93
  activeModules: {},
109
94
  config: { keepAliveSameModules: true, debug: false, autoWrap: true, hoverPrefetch: false, showErrorNotification: false, persistState: false, virtualizeDom: false, transition: { enabled: false, simulatedDelay: 0, module: null, useViewTransitions: false } },
@@ -270,21 +255,15 @@ function loader(config) {
270
255
  return Loader.load(config);
271
256
  }
272
257
 
273
- /**
274
- * baremetal.js v1.2.1
275
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
276
- * (c) 2026 dkydivyansh
277
- * Released under the GPL-3.0 License
278
- */
279
-
280
-
281
258
  const Router = {
282
259
  htmlCache: {},
283
260
  scrollMemory: {},
284
261
  historyStack: [],
285
262
  currentAbortController: null,
263
+ lastPathAndSearch: '',
286
264
 
287
265
  init() {
266
+ this.lastPathAndSearch = window.location.pathname + window.location.search;
288
267
  if ('scrollRestoration' in history) {
289
268
  history.scrollRestoration = 'manual';
290
269
  }
@@ -299,7 +278,8 @@ const Router = {
299
278
  anchor.origin !== window.location.origin ||
300
279
  anchor.target === '_blank' ||
301
280
  (anchor.rel && anchor.rel.includes('noreferrer')) ||
302
- anchor.hasAttribute('download')
281
+ anchor.hasAttribute('download') ||
282
+ (anchor.getAttribute('href') || '').startsWith('#')
303
283
  ) {
304
284
  return;
305
285
  }
@@ -321,6 +301,7 @@ const Router = {
321
301
  anchor.origin === window.location.origin &&
322
302
  anchor.target !== '_blank' &&
323
303
  !anchor.hasAttribute('download') &&
304
+ !(anchor.getAttribute('href') || '').startsWith('#') &&
324
305
  !this.htmlCache[anchor.href]
325
306
  ) {
326
307
  this.htmlCache[anchor.href] = 'fetching';
@@ -351,6 +332,11 @@ const Router = {
351
332
  },
352
333
 
353
334
  async handleRoute(e) {
335
+ const currentPathAndSearch = window.location.pathname + window.location.search;
336
+ if (this.lastPathAndSearch === currentPathAndSearch) {
337
+ return; // Ignore navigations that only change the #hash
338
+ }
339
+ this.lastPathAndSearch = currentPathAndSearch;
354
340
 
355
341
  if (this.currentAbortController) {
356
342
  this.currentAbortController.abort();
@@ -524,13 +510,6 @@ const Router = {
524
510
  }
525
511
  };
526
512
 
527
- /**
528
- * baremetal.js v1.2.1
529
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
530
- * (c) 2026 dkydivyansh
531
- * Released under the GPL-3.0 License
532
- */
533
-
534
513
  class Virtualizer {
535
514
  constructor(containerId, items, renderRow, itemHeight, visibleCount = 20) {
536
515
  this.container = document.getElementById(containerId);
@@ -596,14 +575,6 @@ var virtualize$1 = /*#__PURE__*/Object.freeze({
596
575
  virtualize: virtualize
597
576
  });
598
577
 
599
- /**
600
- * baremetal.js v1.2.1
601
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
602
- * (c) 2026 dkydivyansh
603
- * Released under the GPL-3.0 License
604
- */
605
-
606
-
607
578
  const BareMetal = {
608
579
  state: stateManager,
609
580
  events: stateManager,
@@ -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,init(){"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")||(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")||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){this.currentAbortController&&this.currentAbortController.abort(),this.currentAbortController=new AbortController;const o=this.currentAbortController.signal,s=window.location.pathname;try{let n;e.log(`Navigating to ${s}`),t.publish("ROUTE_START",{url:s}),e.config.transition&&e.config.transition.simulatedDelay&&(t.publish("ROUTE_PROGRESS",{url:s,progress:10}),await new Promise(t=>setTimeout(t,e.config.transition.simulatedDelay/2)),t.publish("ROUTE_PROGRESS",{url:s,progress:30}),await new Promise(t=>setTimeout(t,e.config.transition.simulatedDelay/2)));const r=new URL(s,document.baseURI).href;if(e.config.hoverPrefetch&&this.htmlCache[r]&&"fetching"!==this.htmlCache[r])n=this.htmlCache[r],e.log(`Used pre-fetched cache for ${s}`);else{const t=await fetch(s,{signal:o});if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);n=await t.text()}t.publish("ROUTE_PROGRESS",{url:s,progress:50});const a=(new DOMParser).parseFromString(n,"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 ${s}. Falling back to native navigation.`),void window.location.assign(s);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[s]||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 ${s} due to new navigation.`);if(console.error("Routing error:",i),t.publish("ROUTE_ERROR",{url:s,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(s)}}}};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=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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baremetal.js",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",
@@ -43,4 +43,4 @@
43
43
  "rollup": "^4.62.0",
44
44
  "vitest": "^4.1.9"
45
45
  }
46
- }
46
+ }
package/src/index.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
1
  import { stateManager } from './state.js';
9
2
  import { Loader, loader } from './loader.js';
10
3
  import { Router } from './router.js';
package/src/loader.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
1
  import { stateManager } from './state.js';
9
2
 
10
3
  export const Loader = {
package/src/router.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
1
  import { Loader } from './loader.js';
9
2
  import { stateManager } from './state.js';
10
3
 
@@ -13,8 +6,10 @@ export const Router = {
13
6
  scrollMemory: {},
14
7
  historyStack: [],
15
8
  currentAbortController: null,
9
+ lastPathAndSearch: '',
16
10
 
17
11
  init() {
12
+ this.lastPathAndSearch = window.location.pathname + window.location.search;
18
13
  if ('scrollRestoration' in history) {
19
14
  history.scrollRestoration = 'manual';
20
15
  }
@@ -29,7 +24,8 @@ export const Router = {
29
24
  anchor.origin !== window.location.origin ||
30
25
  anchor.target === '_blank' ||
31
26
  (anchor.rel && anchor.rel.includes('noreferrer')) ||
32
- anchor.hasAttribute('download')
27
+ anchor.hasAttribute('download') ||
28
+ (anchor.getAttribute('href') || '').startsWith('#')
33
29
  ) {
34
30
  return;
35
31
  }
@@ -51,6 +47,7 @@ export const Router = {
51
47
  anchor.origin === window.location.origin &&
52
48
  anchor.target !== '_blank' &&
53
49
  !anchor.hasAttribute('download') &&
50
+ !(anchor.getAttribute('href') || '').startsWith('#') &&
54
51
  !this.htmlCache[anchor.href]
55
52
  ) {
56
53
  this.htmlCache[anchor.href] = 'fetching';
@@ -81,6 +78,11 @@ export const Router = {
81
78
  },
82
79
 
83
80
  async handleRoute(e) {
81
+ const currentPathAndSearch = window.location.pathname + window.location.search;
82
+ if (this.lastPathAndSearch === currentPathAndSearch) {
83
+ return; // Ignore navigations that only change the #hash
84
+ }
85
+ this.lastPathAndSearch = currentPathAndSearch;
84
86
 
85
87
  if (this.currentAbortController) {
86
88
  this.currentAbortController.abort();
package/src/state.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
1
  class StateManager {
9
2
  constructor() {
10
3
  this.state = {};
@@ -59,7 +52,7 @@ class StateManager {
59
52
  if (window.__baremetal_persist_state) {
60
53
  try {
61
54
  sessionStorage.setItem('baremetal_state', JSON.stringify(this.state));
62
- } catch(e) {}
55
+ } catch (e) { }
63
56
  }
64
57
  }
65
58
 
package/src/transition.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
1
  export function mount({ state }) {
9
2
  console.log('[Transition Module] Mounted');
10
3
 
package/src/virtualize.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * baremetal.js v1.2.1
3
- * A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
4
- * (c) 2026 dkydivyansh
5
- * Released under the GPL-3.0 License
6
- */
7
-
8
1
  export class Virtualizer {
9
2
  constructor(containerId, items, renderRow, itemHeight, visibleCount = 20) {
10
3
  this.container = document.getElementById(containerId);