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 +11 -0
- package/README.md +16 -6
- package/dist/baremetal.js +12 -41
- package/dist/baremetal.min.js +1 -1
- package/package.json +2 -2
- package/src/index.js +0 -7
- package/src/loader.js +0 -7
- package/src/router.js +10 -8
- package/src/state.js +1 -8
- package/src/transition.js +0 -7
- package/src/virtualize.js +0 -7
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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,
|
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,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.
|
|
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);
|