baremetal.js 1.0.1 → 1.2.1
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 +9 -0
- package/README.md +32 -37
- package/SECURITY.md +2 -2
- package/dist/baremetal.js +637 -0
- package/dist/baremetal.min.js +1 -0
- package/docs/api.md +17 -35
- package/package.json +21 -4
- package/src/index.js +20 -2
- package/src/loader.js +96 -71
- package/src/router.js +46 -45
- package/src/state.js +27 -20
- package/src/transition.js +14 -12
- package/src/virtualize.js +65 -0
- package/.gitattributes +0 -2
- package/.github/workflows/npm-publish.yml +0 -34
- package/CODE_OF_CONDUCT.md +0 -122
- package/CONTRIBUTING.md +0 -53
- package/demo/assets/audio/darren_hirst-20-474737.mp3 +0 -0
- package/demo/assets/js/media_player.js +0 -9
- package/demo/assets/js/page1_specific.js +0 -23
- package/demo/assets/js/page2_specific.js +0 -15
- package/demo/assets/js/shared.js +0 -56
- package/demo/assets/js/sidebar.js +0 -49
- package/demo/main.js +0 -18
- package/demo/page1.html +0 -139
- package/demo/page2.html +0 -132
- package/demo/page3_normal.html +0 -26
|
@@ -0,0 +1,637 @@
|
|
|
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
|
|
10
|
+
* A lightweight, dependency-free Vanilla JavaScript SPA engine prioritizing extreme performance, native browser features, and explicit lifecycle management.
|
|
11
|
+
* (c) 2026 dkydivyansh
|
|
12
|
+
* Released under the GPL-3.0 License
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class StateManager {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.state = {};
|
|
18
|
+
this.listeners = {};
|
|
19
|
+
this.eventBus = {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
initPersistence() {
|
|
23
|
+
try {
|
|
24
|
+
const saved = sessionStorage.getItem('baremetal_state');
|
|
25
|
+
if (saved) {
|
|
26
|
+
const parsed = JSON.parse(saved);
|
|
27
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
28
|
+
this.state[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.warn('Failed to hydrate state', e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
init(key, defaultValue) {
|
|
37
|
+
if (this.state[key] === undefined) {
|
|
38
|
+
this.state[key] = defaultValue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
subscribe(key, callback) {
|
|
43
|
+
if (!this.listeners[key]) {
|
|
44
|
+
this.listeners[key] = [];
|
|
45
|
+
}
|
|
46
|
+
this.listeners[key].push(callback);
|
|
47
|
+
|
|
48
|
+
if (this.state[key] !== undefined) {
|
|
49
|
+
callback(this.state[key]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return () => this.unsubscribe(key, callback);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
unsubscribe(key, callback) {
|
|
56
|
+
if (!this.listeners[key]) return;
|
|
57
|
+
this.listeners[key] = this.listeners[key].filter(cb => cb !== callback);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update(key, value) {
|
|
61
|
+
this.state[key] = value;
|
|
62
|
+
if (this.listeners[key]) {
|
|
63
|
+
this.listeners[key].forEach(callback => callback(value));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (window.__baremetal_persist_state) {
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem('baremetal_state', JSON.stringify(this.state));
|
|
69
|
+
} catch(e) {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get(key) {
|
|
74
|
+
return this.state[key];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
on(event, callback) {
|
|
78
|
+
if (!this.eventBus[event]) {
|
|
79
|
+
this.eventBus[event] = [];
|
|
80
|
+
}
|
|
81
|
+
this.eventBus[event].push(callback);
|
|
82
|
+
return () => this.off(event, callback);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
off(event, callback) {
|
|
86
|
+
if (!this.eventBus[event]) return;
|
|
87
|
+
this.eventBus[event] = this.eventBus[event].filter(cb => cb !== callback);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
publish(event, data) {
|
|
91
|
+
if (this.eventBus[event]) {
|
|
92
|
+
this.eventBus[event].forEach(callback => callback(data));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const stateManager = new StateManager();
|
|
98
|
+
|
|
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
|
+
const Loader = {
|
|
108
|
+
activeModules: {},
|
|
109
|
+
config: { keepAliveSameModules: true, debug: false, autoWrap: true, hoverPrefetch: false, showErrorNotification: false, persistState: false, virtualizeDom: false, transition: { enabled: false, simulatedDelay: 0, module: null, useViewTransitions: false } },
|
|
110
|
+
|
|
111
|
+
setConfig(globalConfig) {
|
|
112
|
+
this.config = { ...this.config, ...globalConfig };
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
log(...args) {
|
|
116
|
+
if (this.config.debug) console.log('[BareMetal Loader]', ...args);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async prepare(newConfig) {
|
|
120
|
+
|
|
121
|
+
if (this.config.transition && this.config.transition.enabled) {
|
|
122
|
+
const transitionPath = this.config.transition.module || '/src/transition.js';
|
|
123
|
+
newConfig['__baremetal_transition'] = transitionPath;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const modulesToKeep = {};
|
|
127
|
+
const modulesToDestroy = [];
|
|
128
|
+
const modulesToLoad = {};
|
|
129
|
+
|
|
130
|
+
for (const [key, mod] of Object.entries(this.activeModules)) {
|
|
131
|
+
const isImmortal = key === '__baremetal_transition';
|
|
132
|
+
const newPath = typeof newConfig[key] === 'string' ? newConfig[key] : (newConfig[key] ? newConfig[key].path : null);
|
|
133
|
+
if ((this.config.keepAliveSameModules || isImmortal) && newPath === mod.path) {
|
|
134
|
+
|
|
135
|
+
modulesToKeep[key] = mod;
|
|
136
|
+
this.log(`Keep-Alive: ${key} (${mod.path})`);
|
|
137
|
+
} else {
|
|
138
|
+
|
|
139
|
+
modulesToDestroy.push(mod);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const mod of modulesToDestroy) {
|
|
144
|
+
this.log(`Destroying module: ${mod.path}`);
|
|
145
|
+
if (mod.module && typeof mod.module.destroy === 'function') {
|
|
146
|
+
try {
|
|
147
|
+
mod.module.destroy();
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(`Error destroying module ${mod.path}`, e);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const [key, path] of Object.entries(newConfig)) {
|
|
155
|
+
if (!modulesToKeep[key]) {
|
|
156
|
+
modulesToLoad[key] = path;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.activeModules = modulesToKeep;
|
|
161
|
+
|
|
162
|
+
return modulesToLoad;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async loadPrepared(modulesToLoad) {
|
|
166
|
+
const total = Object.keys(modulesToLoad).length;
|
|
167
|
+
let loaded = 0;
|
|
168
|
+
|
|
169
|
+
const loadPromises = Object.entries(modulesToLoad).map(async ([key, modDef]) => {
|
|
170
|
+
const path = typeof modDef === 'string' ? modDef : modDef.path;
|
|
171
|
+
const lazySelector = typeof modDef === 'string' ? null : modDef.lazy;
|
|
172
|
+
|
|
173
|
+
this.log(`Preparing module: ${path}`);
|
|
174
|
+
|
|
175
|
+
const doImport = async () => {
|
|
176
|
+
try {
|
|
177
|
+
const resolvedPath = new URL(path, document.baseURI).href;
|
|
178
|
+
const loadPath = this.config.debug ? `${resolvedPath}?t=${Date.now()}` : resolvedPath;
|
|
179
|
+
|
|
180
|
+
let module;
|
|
181
|
+
|
|
182
|
+
if (this.config.autoWrap) {
|
|
183
|
+
const response = await fetch(loadPath);
|
|
184
|
+
if (!response.ok) throw new Error(`Failed to fetch ${loadPath}`);
|
|
185
|
+
const sourceText = await response.text();
|
|
186
|
+
|
|
187
|
+
const hasMount = /export\s+(function|const|let|var)\s+mount\b/.test(sourceText) || /export\s+\{.*?\bmount\b.*?\}/.test(sourceText);
|
|
188
|
+
|
|
189
|
+
if (!hasMount) {
|
|
190
|
+
console.warn(`[BareMetal] WARNING: Module ${path} does not explicitly export a mount() function. Auto-wrapping it...`);
|
|
191
|
+
const wrappedSource = `export async function mount(context) { const { state } = context; ${sourceText} }`;
|
|
192
|
+
const blob = new Blob([wrappedSource], { type: 'application/javascript' });
|
|
193
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
194
|
+
module = await import(blobUrl);
|
|
195
|
+
URL.revokeObjectURL(blobUrl);
|
|
196
|
+
} else {
|
|
197
|
+
module = await import(loadPath);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
module = await import(loadPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (typeof module.mount === 'function') {
|
|
204
|
+
this.log(`Mounting module: ${path}`);
|
|
205
|
+
|
|
206
|
+
const context = { state: stateManager };
|
|
207
|
+
if (this.config.virtualizeDom) {
|
|
208
|
+
const { virtualize } = await Promise.resolve().then(function () { return virtualize$1; });
|
|
209
|
+
context.virtualize = virtualize;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const instance = await module.mount(context);
|
|
213
|
+
|
|
214
|
+
this.activeModules[key] = {
|
|
215
|
+
path: path,
|
|
216
|
+
module: instance ? { destroy: instance.destroy } : module
|
|
217
|
+
};
|
|
218
|
+
} else {
|
|
219
|
+
console.error(`[BareMetal] Module ${path} failed to provide a mount function even after wrapping.`);
|
|
220
|
+
}
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error(`Failed to load module: ${path}`, err);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (lazySelector) {
|
|
227
|
+
const element = document.querySelector(lazySelector);
|
|
228
|
+
if (element && window.IntersectionObserver) {
|
|
229
|
+
this.log(`Deferred loading of module ${path} until ${lazySelector} is visible`);
|
|
230
|
+
const observer = new IntersectionObserver((entries) => {
|
|
231
|
+
if (entries[0].isIntersecting) {
|
|
232
|
+
observer.disconnect();
|
|
233
|
+
doImport();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
observer.observe(element);
|
|
237
|
+
|
|
238
|
+
loaded++;
|
|
239
|
+
if (total > 0) {
|
|
240
|
+
const progress = 50 + (loaded / total) * 50;
|
|
241
|
+
stateManager.publish('ROUTE_PROGRESS', { url: window.location.pathname, progress });
|
|
242
|
+
}
|
|
243
|
+
return Promise.resolve();
|
|
244
|
+
} else {
|
|
245
|
+
|
|
246
|
+
await doImport();
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
await doImport();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
loaded++;
|
|
253
|
+
if (total > 0) {
|
|
254
|
+
const progress = 50 + (loaded / total) * 50;
|
|
255
|
+
stateManager.publish('ROUTE_PROGRESS', { url: window.location.pathname, progress });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await Promise.all(loadPromises);
|
|
260
|
+
stateManager.publish('ROUTE_END', { url: window.location.pathname });
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async load(config) {
|
|
264
|
+
const modulesToLoad = await this.prepare(config);
|
|
265
|
+
await this.loadPrepared(modulesToLoad);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function loader(config) {
|
|
270
|
+
return Loader.load(config);
|
|
271
|
+
}
|
|
272
|
+
|
|
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
|
+
const Router = {
|
|
282
|
+
htmlCache: {},
|
|
283
|
+
scrollMemory: {},
|
|
284
|
+
historyStack: [],
|
|
285
|
+
currentAbortController: null,
|
|
286
|
+
|
|
287
|
+
init() {
|
|
288
|
+
if ('scrollRestoration' in history) {
|
|
289
|
+
history.scrollRestoration = 'manual';
|
|
290
|
+
}
|
|
291
|
+
window.addEventListener('popstate', this.handleRoute.bind(this));
|
|
292
|
+
|
|
293
|
+
document.body.addEventListener('click', e => {
|
|
294
|
+
|
|
295
|
+
const anchor = e.target.closest('a');
|
|
296
|
+
if (!anchor) return;
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
anchor.origin !== window.location.origin ||
|
|
300
|
+
anchor.target === '_blank' ||
|
|
301
|
+
(anchor.rel && anchor.rel.includes('noreferrer')) ||
|
|
302
|
+
anchor.hasAttribute('download')
|
|
303
|
+
) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
|
|
309
|
+
this.historyStack.push(window.location.pathname);
|
|
310
|
+
this.scrollMemory[window.location.pathname] = window.scrollY;
|
|
311
|
+
|
|
312
|
+
history.pushState(null, '', anchor.href);
|
|
313
|
+
this.handleRoute();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
document.body.addEventListener('mouseover', e => {
|
|
317
|
+
if (!Loader.config.hoverPrefetch) return;
|
|
318
|
+
const anchor = e.target.closest('a');
|
|
319
|
+
if (!anchor) return;
|
|
320
|
+
if (
|
|
321
|
+
anchor.origin === window.location.origin &&
|
|
322
|
+
anchor.target !== '_blank' &&
|
|
323
|
+
!anchor.hasAttribute('download') &&
|
|
324
|
+
!this.htmlCache[anchor.href]
|
|
325
|
+
) {
|
|
326
|
+
this.htmlCache[anchor.href] = 'fetching';
|
|
327
|
+
fetch(anchor.href)
|
|
328
|
+
.then(res => {
|
|
329
|
+
if (res.ok) return res.text();
|
|
330
|
+
throw new Error('Failed to prefetch');
|
|
331
|
+
})
|
|
332
|
+
.then(html => this.htmlCache[anchor.href] = html)
|
|
333
|
+
.catch(() => delete this.htmlCache[anchor.href]);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
back() {
|
|
339
|
+
|
|
340
|
+
if (this.historyStack.length > 0) {
|
|
341
|
+
const prevUrl = this.historyStack.pop();
|
|
342
|
+
history.pushState(null, '', prevUrl);
|
|
343
|
+
this.handleRoute();
|
|
344
|
+
} else {
|
|
345
|
+
history.back();
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
reload() {
|
|
350
|
+
window.location.reload();
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
async handleRoute(e) {
|
|
354
|
+
|
|
355
|
+
if (this.currentAbortController) {
|
|
356
|
+
this.currentAbortController.abort();
|
|
357
|
+
}
|
|
358
|
+
this.currentAbortController = new AbortController();
|
|
359
|
+
const signal = this.currentAbortController.signal;
|
|
360
|
+
|
|
361
|
+
const url = window.location.pathname;
|
|
362
|
+
try {
|
|
363
|
+
Loader.log(`Navigating to ${url}`);
|
|
364
|
+
stateManager.publish('ROUTE_START', { url });
|
|
365
|
+
|
|
366
|
+
if (Loader.config.transition && Loader.config.transition.simulatedDelay) {
|
|
367
|
+
stateManager.publish('ROUTE_PROGRESS', { url, progress: 10 });
|
|
368
|
+
await new Promise(r => setTimeout(r, Loader.config.transition.simulatedDelay / 2));
|
|
369
|
+
stateManager.publish('ROUTE_PROGRESS', { url, progress: 30 });
|
|
370
|
+
await new Promise(r => setTimeout(r, Loader.config.transition.simulatedDelay / 2));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let htmlText;
|
|
374
|
+
const fullUrl = new URL(url, document.baseURI).href;
|
|
375
|
+
|
|
376
|
+
if (Loader.config.hoverPrefetch && this.htmlCache[fullUrl] && this.htmlCache[fullUrl] !== 'fetching') {
|
|
377
|
+
htmlText = this.htmlCache[fullUrl];
|
|
378
|
+
Loader.log(`Used pre-fetched cache for ${url}`);
|
|
379
|
+
} else {
|
|
380
|
+
const response = await fetch(url, { signal });
|
|
381
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
382
|
+
htmlText = await response.text();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
stateManager.publish('ROUTE_PROGRESS', { url, progress: 50 });
|
|
386
|
+
|
|
387
|
+
const parser = new DOMParser();
|
|
388
|
+
const doc = parser.parseFromString(htmlText, 'text/html');
|
|
389
|
+
|
|
390
|
+
let config = null;
|
|
391
|
+
const scriptTags = doc.querySelectorAll('script');
|
|
392
|
+
for (const script of scriptTags) {
|
|
393
|
+
if (script.textContent.includes('loader(')) {
|
|
394
|
+
|
|
395
|
+
const match = script.textContent.match(/loader\s*\(\s*(\{[\s\S]*?\})\s*\)/);
|
|
396
|
+
if (match && match[1]) {
|
|
397
|
+
try {
|
|
398
|
+
|
|
399
|
+
config = new Function('return ' + match[1])();
|
|
400
|
+
} catch (e) {
|
|
401
|
+
console.error("Failed to parse loader config in new page", e);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!config) {
|
|
408
|
+
Loader.log(`No BareMetal config found on ${url}. Falling back to native navigation.`);
|
|
409
|
+
window.location.assign(url);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const modulesToLoad = await Loader.prepare(config);
|
|
414
|
+
|
|
415
|
+
document.title = doc.title;
|
|
416
|
+
|
|
417
|
+
const oldStyles = document.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]');
|
|
418
|
+
oldStyles.forEach(s => s.remove());
|
|
419
|
+
const newStyles = doc.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]');
|
|
420
|
+
newStyles.forEach(s => document.head.appendChild(s.cloneNode(true)));
|
|
421
|
+
|
|
422
|
+
const preservedNodes = [];
|
|
423
|
+
const persistentElements = document.querySelectorAll('[data-baremetal-preserve]');
|
|
424
|
+
|
|
425
|
+
persistentElements.forEach(el => {
|
|
426
|
+
if (!el.id) return;
|
|
427
|
+
if (doc.getElementById(el.id)) {
|
|
428
|
+
|
|
429
|
+
const placeholder = document.createElement('div');
|
|
430
|
+
el.parentNode.replaceChild(placeholder, el);
|
|
431
|
+
preservedNodes.push(el);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const transitionRoot = document.getElementById('baremetal-transition-root');
|
|
436
|
+
if (transitionRoot) {
|
|
437
|
+
transitionRoot.parentNode.removeChild(transitionRoot);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const executeDOMSwap = () => {
|
|
441
|
+
document.body.innerHTML = doc.body.innerHTML;
|
|
442
|
+
|
|
443
|
+
preservedNodes.forEach(el => {
|
|
444
|
+
const newEl = document.getElementById(el.id);
|
|
445
|
+
if (newEl) {
|
|
446
|
+
|
|
447
|
+
Array.from(el.attributes).forEach(attr => {
|
|
448
|
+
if (attr.name !== 'id' && attr.name !== 'data-baremetal-preserve') el.removeAttribute(attr.name);
|
|
449
|
+
});
|
|
450
|
+
Array.from(newEl.attributes).forEach(attr => {
|
|
451
|
+
if (attr.name !== 'id') el.setAttribute(attr.name, attr.value);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
newEl.parentNode.replaceChild(el, newEl);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (transitionRoot) {
|
|
459
|
+
document.body.appendChild(transitionRoot);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
stateManager.publish('DOM_SWAPPED', null);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const doSwap = () => {
|
|
466
|
+
executeDOMSwap();
|
|
467
|
+
window.scrollTo(0, this.scrollMemory[url] || 0);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (Loader.config.transition && Loader.config.transition.useViewTransitions && document.startViewTransition) {
|
|
471
|
+
document.startViewTransition(() => {
|
|
472
|
+
doSwap();
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
doSwap();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await Loader.loadPrepared(modulesToLoad);
|
|
479
|
+
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err.name === 'AbortError') {
|
|
482
|
+
Loader.log(`Aborted fetch for ${url} due to new navigation.`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
console.error("Routing error:", err);
|
|
486
|
+
stateManager.publish('ROUTE_ERROR', { url, error: err.message });
|
|
487
|
+
|
|
488
|
+
if (Loader.config.showErrorNotification) {
|
|
489
|
+
|
|
490
|
+
if (this.historyStack.length > 0) {
|
|
491
|
+
const prev = this.historyStack.pop();
|
|
492
|
+
history.replaceState(null, '', prev);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const notif = document.createElement('div');
|
|
496
|
+
notif.style.position = 'fixed';
|
|
497
|
+
notif.style.bottom = '20px';
|
|
498
|
+
notif.style.left = '20px';
|
|
499
|
+
notif.style.background = '#e74c3c';
|
|
500
|
+
notif.style.color = 'white';
|
|
501
|
+
notif.style.padding = '15px 20px';
|
|
502
|
+
notif.style.borderRadius = '8px';
|
|
503
|
+
notif.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
504
|
+
notif.style.zIndex = '999999';
|
|
505
|
+
notif.style.fontFamily = 'sans-serif';
|
|
506
|
+
notif.style.transition = 'opacity 0.3s ease';
|
|
507
|
+
notif.innerHTML = `<strong>Navigation Failed:</strong> ${err.message}`;
|
|
508
|
+
|
|
509
|
+
document.body.appendChild(notif);
|
|
510
|
+
|
|
511
|
+
setTimeout(() => {
|
|
512
|
+
notif.style.opacity = '0';
|
|
513
|
+
setTimeout(() => notif.remove(), 300);
|
|
514
|
+
}, 4000);
|
|
515
|
+
} else {
|
|
516
|
+
|
|
517
|
+
if (this.historyStack.length > 0) {
|
|
518
|
+
const prev = this.historyStack.pop();
|
|
519
|
+
history.replaceState(null, '', prev);
|
|
520
|
+
}
|
|
521
|
+
window.location.assign(url);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
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
|
+
class Virtualizer {
|
|
535
|
+
constructor(containerId, items, renderRow, itemHeight, visibleCount = 20) {
|
|
536
|
+
this.container = document.getElementById(containerId);
|
|
537
|
+
if (!this.container) return;
|
|
538
|
+
|
|
539
|
+
this.items = items;
|
|
540
|
+
this.renderRow = renderRow;
|
|
541
|
+
this.itemHeight = itemHeight;
|
|
542
|
+
this.visibleCount = visibleCount;
|
|
543
|
+
|
|
544
|
+
this.totalHeight = this.items.length * this.itemHeight;
|
|
545
|
+
this.container.style.overflowY = 'auto';
|
|
546
|
+
this.container.style.position = 'relative';
|
|
547
|
+
|
|
548
|
+
this.innerWrapper = document.createElement('div');
|
|
549
|
+
this.innerWrapper.style.height = `${this.totalHeight}px`;
|
|
550
|
+
this.innerWrapper.style.position = 'relative';
|
|
551
|
+
this.container.appendChild(this.innerWrapper);
|
|
552
|
+
|
|
553
|
+
this.startIndex = 0;
|
|
554
|
+
this.onScroll = this.onScroll.bind(this);
|
|
555
|
+
this.container.addEventListener('scroll', this.onScroll);
|
|
556
|
+
this.render();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
onScroll() {
|
|
560
|
+
const scrollTop = this.container.scrollTop;
|
|
561
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - 2);
|
|
562
|
+
if (startIndex !== this.startIndex) {
|
|
563
|
+
this.startIndex = startIndex;
|
|
564
|
+
this.render();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
render() {
|
|
569
|
+
this.innerWrapper.innerHTML = '';
|
|
570
|
+
const endIndex = Math.min(this.items.length - 1, this.startIndex + this.visibleCount + 4);
|
|
571
|
+
|
|
572
|
+
for (let i = this.startIndex; i <= endIndex; i++) {
|
|
573
|
+
const node = this.renderRow(this.items[i], i);
|
|
574
|
+
node.style.position = 'absolute';
|
|
575
|
+
node.style.top = `${i * this.itemHeight}px`;
|
|
576
|
+
node.style.width = '100%';
|
|
577
|
+
this.innerWrapper.appendChild(node);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
destroy() {
|
|
582
|
+
if (this.container) {
|
|
583
|
+
this.container.removeEventListener('scroll', this.onScroll);
|
|
584
|
+
this.container.innerHTML = '';
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const virtualize = (containerId, items, renderRow, itemHeight, visibleCount) => {
|
|
590
|
+
return new Virtualizer(containerId, items, renderRow, itemHeight, visibleCount);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
var virtualize$1 = /*#__PURE__*/Object.freeze({
|
|
594
|
+
__proto__: null,
|
|
595
|
+
Virtualizer: Virtualizer,
|
|
596
|
+
virtualize: virtualize
|
|
597
|
+
});
|
|
598
|
+
|
|
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
|
+
const BareMetal = {
|
|
608
|
+
state: stateManager,
|
|
609
|
+
events: stateManager,
|
|
610
|
+
loader: loader,
|
|
611
|
+
router: Router,
|
|
612
|
+
virtualize: virtualize,
|
|
613
|
+
|
|
614
|
+
init(config = {}) {
|
|
615
|
+
if (config.debug !== undefined) Loader.setConfig({ debug: config.debug });
|
|
616
|
+
if (config.keepAliveSameModules !== undefined) Loader.setConfig({ keepAliveSameModules: config.keepAliveSameModules });
|
|
617
|
+
if (config.transition !== undefined) Loader.setConfig({ transition: config.transition });
|
|
618
|
+
if (config.autoWrap !== undefined) Loader.setConfig({ autoWrap: config.autoWrap });
|
|
619
|
+
if (config.hoverPrefetch !== undefined) Loader.setConfig({ hoverPrefetch: config.hoverPrefetch });
|
|
620
|
+
if (config.showErrorNotification !== undefined) Loader.setConfig({ showErrorNotification: config.showErrorNotification });
|
|
621
|
+
|
|
622
|
+
if (config.persistState !== undefined) {
|
|
623
|
+
Loader.setConfig({ persistState: config.persistState });
|
|
624
|
+
if (config.persistState) {
|
|
625
|
+
window.__baremetal_persist_state = true;
|
|
626
|
+
stateManager.initPersistence();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (config.virtualizeDom !== undefined) Loader.setConfig({ virtualizeDom: config.virtualizeDom });
|
|
631
|
+
|
|
632
|
+
Router.init();
|
|
633
|
+
Loader.log("Initialized BareMetal Engine with config:", config);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
export { BareMetal, loader };
|
|
@@ -0,0 +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};
|