baremetal.js 1.0.0 → 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 +48 -35
- package/SECURITY.md +2 -2
- package/dist/baremetal.js +637 -0
- package/dist/baremetal.min.js +1 -0
- package/docs/api.md +16 -11
- package/package.json +22 -5
- package/src/index.js +20 -2
- package/src/loader.js +97 -72
- package/src/router.js +62 -47
- 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
package/docs/api.md
CHANGED
|
@@ -7,14 +7,21 @@ BareMetal exports its core engine as a global configuration object. Here are the
|
|
|
7
7
|
### `BareMetal.init(config)`
|
|
8
8
|
Initializes the engine, sets up the Router, and binds global configurations.
|
|
9
9
|
|
|
10
|
-
**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
**Configuration Reference:**
|
|
11
|
+
|
|
12
|
+
| Option | Type | Default | Description |
|
|
13
|
+
|--------|------|---------|-------------|
|
|
14
|
+
| `debug` | boolean | `false` | Enable verbose console logs. |
|
|
15
|
+
| `keepAliveSameModules` | boolean | `true` | Prevent destruction of modules shared between routes. |
|
|
16
|
+
| `autoWrap` | boolean | `true` | Automatically wrap modules that do not export a `mount` function. |
|
|
17
|
+
| `hoverPrefetch` | boolean | `false` | Enable 0ms latency pre-fetching on link hover. |
|
|
18
|
+
| `persistState` | boolean | `false` | Automatically serialize `stateManager` to `sessionStorage` for F5 resilience. |
|
|
19
|
+
| `virtualizeDom` | boolean | `false` | Inject the `Virtualizer` helper into module mount contexts. |
|
|
20
|
+
| `showErrorNotification` | boolean | `false` | Enable floating error boundaries on navigation failures. |
|
|
21
|
+
| `transition.enabled` | boolean | `false` | Enable the protected transition module. |
|
|
22
|
+
| `transition.module` | string | `null` | Path to a custom transition module (defaults to `/src/transition.js`). |
|
|
23
|
+
| `transition.simulatedDelay` | number | `0` | Artificial delay (ms) for testing transitions. |
|
|
24
|
+
| `transition.useViewTransitions` | boolean | `false` | Enables the native View Transitions API for smooth cross-fades during navigation. |
|
|
18
25
|
|
|
19
26
|
---
|
|
20
27
|
|
|
@@ -55,7 +62,7 @@ Listens for a Pub/Sub event.
|
|
|
55
62
|
|
|
56
63
|
---
|
|
57
64
|
|
|
58
|
-
##
|
|
65
|
+
## 4. Writing Custom Page Transitions
|
|
59
66
|
|
|
60
67
|
You can replace the default progress bar and fade overlay by pointing `config.transition.module` to your own JavaScript file. A custom transition is just a standard BareMetal module that listens to routing events.
|
|
61
68
|
|
|
@@ -105,5 +112,3 @@ export function mount({ state }) {
|
|
|
105
112
|
};
|
|
106
113
|
}
|
|
107
114
|
```
|
|
108
|
-
|
|
109
|
-
|
package/package.json
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baremetal.js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"dist",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"CHANGELOG.md",
|
|
12
|
+
"docs/api.md",
|
|
13
|
+
"README.md",
|
|
14
|
+
"SECURITY.md"
|
|
15
|
+
],
|
|
7
16
|
"scripts": {
|
|
8
|
-
"
|
|
17
|
+
"build": "rollup -c",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest"
|
|
9
20
|
},
|
|
10
21
|
"repository": {
|
|
11
22
|
"type": "git",
|
|
@@ -20,10 +31,16 @@
|
|
|
20
31
|
"baremetal",
|
|
21
32
|
"performance"
|
|
22
33
|
],
|
|
23
|
-
"author": "
|
|
34
|
+
"author": "dkydivyansh",
|
|
24
35
|
"license": "GPL-3.0",
|
|
25
36
|
"bugs": {
|
|
26
37
|
"url": "https://github.com/dkydivyansh/BareMetal.js/issues"
|
|
27
38
|
},
|
|
28
|
-
"homepage": "https://BareMetal.dkydivyansh.com"
|
|
29
|
-
|
|
39
|
+
"homepage": "https://BareMetal.dkydivyansh.com",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
42
|
+
"jsdom": "^29.1.1",
|
|
43
|
+
"rollup": "^4.62.0",
|
|
44
|
+
"vitest": "^4.1.9"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
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
|
+
|
|
1
8
|
import { stateManager } from './state.js';
|
|
2
9
|
import { Loader, loader } from './loader.js';
|
|
3
10
|
import { Router } from './router.js';
|
|
11
|
+
import { virtualize } from './virtualize.js';
|
|
4
12
|
|
|
5
13
|
const BareMetal = {
|
|
6
14
|
state: stateManager,
|
|
7
|
-
events: stateManager,
|
|
15
|
+
events: stateManager,
|
|
8
16
|
loader: loader,
|
|
9
17
|
router: Router,
|
|
18
|
+
virtualize: virtualize,
|
|
10
19
|
|
|
11
20
|
init(config = {}) {
|
|
12
21
|
if (config.debug !== undefined) Loader.setConfig({ debug: config.debug });
|
|
@@ -16,7 +25,16 @@ const BareMetal = {
|
|
|
16
25
|
if (config.hoverPrefetch !== undefined) Loader.setConfig({ hoverPrefetch: config.hoverPrefetch });
|
|
17
26
|
if (config.showErrorNotification !== undefined) Loader.setConfig({ showErrorNotification: config.showErrorNotification });
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
if (config.persistState !== undefined) {
|
|
29
|
+
Loader.setConfig({ persistState: config.persistState });
|
|
30
|
+
if (config.persistState) {
|
|
31
|
+
window.__baremetal_persist_state = true;
|
|
32
|
+
stateManager.initPersistence();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.virtualizeDom !== undefined) Loader.setConfig({ virtualizeDom: config.virtualizeDom });
|
|
37
|
+
|
|
20
38
|
Router.init();
|
|
21
39
|
Loader.log("Initialized BareMetal Engine with config:", config);
|
|
22
40
|
}
|
package/src/loader.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
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
|
+
|
|
1
8
|
import { stateManager } from './state.js';
|
|
2
9
|
|
|
3
10
|
export const Loader = {
|
|
4
|
-
activeModules: {},
|
|
5
|
-
config: { keepAliveSameModules: true, debug: false, autoWrap: true, hoverPrefetch: false, showErrorNotification: false, transition: { enabled: false, simulatedDelay: 0, module: null } },
|
|
6
|
-
|
|
11
|
+
activeModules: {},
|
|
12
|
+
config: { keepAliveSameModules: true, debug: false, autoWrap: true, hoverPrefetch: false, showErrorNotification: false, persistState: false, virtualizeDom: false, transition: { enabled: false, simulatedDelay: 0, module: null, useViewTransitions: false } },
|
|
13
|
+
|
|
7
14
|
setConfig(globalConfig) {
|
|
8
15
|
this.config = { ...this.config, ...globalConfig };
|
|
9
16
|
},
|
|
@@ -12,11 +19,8 @@ export const Loader = {
|
|
|
12
19
|
if (this.config.debug) console.log('[BareMetal Loader]', ...args);
|
|
13
20
|
},
|
|
14
21
|
|
|
15
|
-
/**
|
|
16
|
-
* Prepares the transition by destroying obsolete modules and identifying new ones.
|
|
17
|
-
*/
|
|
18
22
|
async prepare(newConfig) {
|
|
19
|
-
|
|
23
|
+
|
|
20
24
|
if (this.config.transition && this.config.transition.enabled) {
|
|
21
25
|
const transitionPath = this.config.transition.module || '/src/transition.js';
|
|
22
26
|
newConfig['__baremetal_transition'] = transitionPath;
|
|
@@ -28,17 +32,17 @@ export const Loader = {
|
|
|
28
32
|
|
|
29
33
|
for (const [key, mod] of Object.entries(this.activeModules)) {
|
|
30
34
|
const isImmortal = key === '__baremetal_transition';
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const newPath = typeof newConfig[key] === 'string' ? newConfig[key] : (newConfig[key] ? newConfig[key].path : null);
|
|
36
|
+
if ((this.config.keepAliveSameModules || isImmortal) && newPath === mod.path) {
|
|
37
|
+
|
|
33
38
|
modulesToKeep[key] = mod;
|
|
34
39
|
this.log(`Keep-Alive: ${key} (${mod.path})`);
|
|
35
40
|
} else {
|
|
36
|
-
|
|
41
|
+
|
|
37
42
|
modulesToDestroy.push(mod);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
// Destroy obsolete modules
|
|
42
46
|
for (const mod of modulesToDestroy) {
|
|
43
47
|
this.log(`Destroying module: ${mod.path}`);
|
|
44
48
|
if (mod.module && typeof mod.module.destroy === 'function') {
|
|
@@ -50,83 +54,108 @@ export const Loader = {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
// Determine what's new
|
|
54
57
|
for (const [key, path] of Object.entries(newConfig)) {
|
|
55
58
|
if (!modulesToKeep[key]) {
|
|
56
59
|
modulesToLoad[key] = path;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
// Temporarily update active modules to only the kept ones
|
|
61
63
|
this.activeModules = modulesToKeep;
|
|
62
64
|
|
|
63
65
|
return modulesToLoad;
|
|
64
66
|
},
|
|
65
67
|
|
|
66
|
-
/**
|
|
67
|
-
* Loads the prepared modules. Called by Router after DOM swap.
|
|
68
|
-
*/
|
|
69
68
|
async loadPrepared(modulesToLoad) {
|
|
70
69
|
const total = Object.keys(modulesToLoad).length;
|
|
71
70
|
let loaded = 0;
|
|
72
71
|
|
|
73
|
-
const loadPromises = Object.entries(modulesToLoad).map(async ([key,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
72
|
+
const loadPromises = Object.entries(modulesToLoad).map(async ([key, modDef]) => {
|
|
73
|
+
const path = typeof modDef === 'string' ? modDef : modDef.path;
|
|
74
|
+
const lazySelector = typeof modDef === 'string' ? null : modDef.lazy;
|
|
75
|
+
|
|
76
|
+
this.log(`Preparing module: ${path}`);
|
|
77
|
+
|
|
78
|
+
const doImport = async () => {
|
|
79
|
+
try {
|
|
80
|
+
const resolvedPath = new URL(path, document.baseURI).href;
|
|
81
|
+
const loadPath = this.config.debug ? `${resolvedPath}?t=${Date.now()}` : resolvedPath;
|
|
82
|
+
|
|
83
|
+
let module;
|
|
84
|
+
|
|
85
|
+
if (this.config.autoWrap) {
|
|
86
|
+
const response = await fetch(loadPath);
|
|
87
|
+
if (!response.ok) throw new Error(`Failed to fetch ${loadPath}`);
|
|
88
|
+
const sourceText = await response.text();
|
|
89
|
+
|
|
90
|
+
const hasMount = /export\s+(function|const|let|var)\s+mount\b/.test(sourceText) || /export\s+\{.*?\bmount\b.*?\}/.test(sourceText);
|
|
91
|
+
|
|
92
|
+
if (!hasMount) {
|
|
93
|
+
console.warn(`[BareMetal] WARNING: Module ${path} does not explicitly export a mount() function. Auto-wrapping it...`);
|
|
94
|
+
const wrappedSource = `export async function mount(context) { const { state } = context; ${sourceText} }`;
|
|
95
|
+
const blob = new Blob([wrappedSource], { type: 'application/javascript' });
|
|
96
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
97
|
+
module = await import(blobUrl);
|
|
98
|
+
URL.revokeObjectURL(blobUrl);
|
|
99
|
+
} else {
|
|
100
|
+
module = await import(loadPath);
|
|
101
|
+
}
|
|
103
102
|
} else {
|
|
104
|
-
module = await import(
|
|
103
|
+
module = await import(loadPath);
|
|
105
104
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
|
|
106
|
+
if (typeof module.mount === 'function') {
|
|
107
|
+
this.log(`Mounting module: ${path}`);
|
|
108
|
+
|
|
109
|
+
const context = { state: stateManager };
|
|
110
|
+
if (this.config.virtualizeDom) {
|
|
111
|
+
const { virtualize } = await import('./virtualize.js');
|
|
112
|
+
context.virtualize = virtualize;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const instance = await module.mount(context);
|
|
116
|
+
|
|
117
|
+
this.activeModules[key] = {
|
|
118
|
+
path: path,
|
|
119
|
+
module: instance ? { destroy: instance.destroy } : module
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`[BareMetal] Module ${path} failed to provide a mount function even after wrapping.`);
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`Failed to load module: ${path}`, err);
|
|
109
126
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (lazySelector) {
|
|
130
|
+
const element = document.querySelector(lazySelector);
|
|
131
|
+
if (element && window.IntersectionObserver) {
|
|
132
|
+
this.log(`Deferred loading of module ${path} until ${lazySelector} is visible`);
|
|
133
|
+
const observer = new IntersectionObserver((entries) => {
|
|
134
|
+
if (entries[0].isIntersecting) {
|
|
135
|
+
observer.disconnect();
|
|
136
|
+
doImport();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
observer.observe(element);
|
|
140
|
+
|
|
141
|
+
loaded++;
|
|
142
|
+
if (total > 0) {
|
|
143
|
+
const progress = 50 + (loaded / total) * 50;
|
|
144
|
+
stateManager.publish('ROUTE_PROGRESS', { url: window.location.pathname, progress });
|
|
145
|
+
}
|
|
146
|
+
return Promise.resolve();
|
|
119
147
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} catch (err) {
|
|
123
|
-
console.error(`Failed to load module: ${path}`, err);
|
|
124
|
-
} finally {
|
|
125
|
-
loaded++;
|
|
126
|
-
if (total > 0) {
|
|
127
|
-
const progress = 50 + (loaded / total) * 50;
|
|
128
|
-
stateManager.publish('ROUTE_PROGRESS', { url: window.location.pathname, progress });
|
|
148
|
+
|
|
149
|
+
await doImport();
|
|
129
150
|
}
|
|
151
|
+
} else {
|
|
152
|
+
await doImport();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
loaded++;
|
|
156
|
+
if (total > 0) {
|
|
157
|
+
const progress = 50 + (loaded / total) * 50;
|
|
158
|
+
stateManager.publish('ROUTE_PROGRESS', { url: window.location.pathname, progress });
|
|
130
159
|
}
|
|
131
160
|
});
|
|
132
161
|
|
|
@@ -134,16 +163,12 @@ export const Loader = {
|
|
|
134
163
|
stateManager.publish('ROUTE_END', { url: window.location.pathname });
|
|
135
164
|
},
|
|
136
165
|
|
|
137
|
-
/**
|
|
138
|
-
* Legacy standalone load (used on first page load directly)
|
|
139
|
-
*/
|
|
140
166
|
async load(config) {
|
|
141
167
|
const modulesToLoad = await this.prepare(config);
|
|
142
168
|
await this.loadPrepared(modulesToLoad);
|
|
143
169
|
}
|
|
144
170
|
};
|
|
145
171
|
|
|
146
|
-
// The user-facing API
|
|
147
172
|
export function loader(config) {
|
|
148
173
|
return Loader.load(config);
|
|
149
174
|
}
|
package/src/router.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
import { Loader } from './loader.js';
|
|
2
9
|
import { stateManager } from './state.js';
|
|
3
10
|
|
|
@@ -5,33 +12,30 @@ export const Router = {
|
|
|
5
12
|
htmlCache: {},
|
|
6
13
|
scrollMemory: {},
|
|
7
14
|
historyStack: [],
|
|
15
|
+
currentAbortController: null,
|
|
8
16
|
|
|
9
17
|
init() {
|
|
10
18
|
if ('scrollRestoration' in history) {
|
|
11
19
|
history.scrollRestoration = 'manual';
|
|
12
20
|
}
|
|
13
21
|
window.addEventListener('popstate', this.handleRoute.bind(this));
|
|
14
|
-
|
|
15
|
-
// Intercept all link clicks
|
|
22
|
+
|
|
16
23
|
document.body.addEventListener('click', e => {
|
|
17
|
-
|
|
24
|
+
|
|
18
25
|
const anchor = e.target.closest('a');
|
|
19
26
|
if (!anchor) return;
|
|
20
27
|
|
|
21
|
-
// Ignore external domains, target="_blank", noreferrer, or downloads
|
|
22
28
|
if (
|
|
23
|
-
anchor.origin !== window.location.origin ||
|
|
29
|
+
anchor.origin !== window.location.origin ||
|
|
24
30
|
anchor.target === '_blank' ||
|
|
25
31
|
(anchor.rel && anchor.rel.includes('noreferrer')) ||
|
|
26
32
|
anchor.hasAttribute('download')
|
|
27
33
|
) {
|
|
28
|
-
return;
|
|
34
|
+
return;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
// Intercept same-origin internal links
|
|
32
37
|
e.preventDefault();
|
|
33
|
-
|
|
34
|
-
// Save current state before navigating away
|
|
38
|
+
|
|
35
39
|
this.historyStack.push(window.location.pathname);
|
|
36
40
|
this.scrollMemory[window.location.pathname] = window.scrollY;
|
|
37
41
|
|
|
@@ -39,18 +43,17 @@ export const Router = {
|
|
|
39
43
|
this.handleRoute();
|
|
40
44
|
});
|
|
41
45
|
|
|
42
|
-
// Hover Pre-fetching
|
|
43
46
|
document.body.addEventListener('mouseover', e => {
|
|
44
47
|
if (!Loader.config.hoverPrefetch) return;
|
|
45
48
|
const anchor = e.target.closest('a');
|
|
46
49
|
if (!anchor) return;
|
|
47
50
|
if (
|
|
48
|
-
anchor.origin === window.location.origin &&
|
|
49
|
-
anchor.target !== '_blank' &&
|
|
51
|
+
anchor.origin === window.location.origin &&
|
|
52
|
+
anchor.target !== '_blank' &&
|
|
50
53
|
!anchor.hasAttribute('download') &&
|
|
51
54
|
!this.htmlCache[anchor.href]
|
|
52
55
|
) {
|
|
53
|
-
this.htmlCache[anchor.href] = 'fetching';
|
|
56
|
+
this.htmlCache[anchor.href] = 'fetching';
|
|
54
57
|
fetch(anchor.href)
|
|
55
58
|
.then(res => {
|
|
56
59
|
if (res.ok) return res.text();
|
|
@@ -63,13 +66,13 @@ export const Router = {
|
|
|
63
66
|
},
|
|
64
67
|
|
|
65
68
|
back() {
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
if (this.historyStack.length > 0) {
|
|
68
71
|
const prevUrl = this.historyStack.pop();
|
|
69
72
|
history.pushState(null, '', prevUrl);
|
|
70
73
|
this.handleRoute();
|
|
71
74
|
} else {
|
|
72
|
-
history.back();
|
|
75
|
+
history.back();
|
|
73
76
|
}
|
|
74
77
|
},
|
|
75
78
|
|
|
@@ -77,7 +80,14 @@ export const Router = {
|
|
|
77
80
|
window.location.reload();
|
|
78
81
|
},
|
|
79
82
|
|
|
80
|
-
async handleRoute() {
|
|
83
|
+
async handleRoute(e) {
|
|
84
|
+
|
|
85
|
+
if (this.currentAbortController) {
|
|
86
|
+
this.currentAbortController.abort();
|
|
87
|
+
}
|
|
88
|
+
this.currentAbortController = new AbortController();
|
|
89
|
+
const signal = this.currentAbortController.signal;
|
|
90
|
+
|
|
81
91
|
const url = window.location.pathname;
|
|
82
92
|
try {
|
|
83
93
|
Loader.log(`Navigating to ${url}`);
|
|
@@ -92,31 +102,30 @@ export const Router = {
|
|
|
92
102
|
|
|
93
103
|
let htmlText;
|
|
94
104
|
const fullUrl = new URL(url, document.baseURI).href;
|
|
95
|
-
|
|
105
|
+
|
|
96
106
|
if (Loader.config.hoverPrefetch && this.htmlCache[fullUrl] && this.htmlCache[fullUrl] !== 'fetching') {
|
|
97
107
|
htmlText = this.htmlCache[fullUrl];
|
|
98
108
|
Loader.log(`Used pre-fetched cache for ${url}`);
|
|
99
109
|
} else {
|
|
100
|
-
const response = await fetch(url);
|
|
110
|
+
const response = await fetch(url, { signal });
|
|
101
111
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
102
112
|
htmlText = await response.text();
|
|
103
113
|
}
|
|
104
|
-
|
|
114
|
+
|
|
105
115
|
stateManager.publish('ROUTE_PROGRESS', { url, progress: 50 });
|
|
106
116
|
|
|
107
117
|
const parser = new DOMParser();
|
|
108
118
|
const doc = parser.parseFromString(htmlText, 'text/html');
|
|
109
|
-
|
|
110
|
-
// 1. Extract Config
|
|
119
|
+
|
|
111
120
|
let config = null;
|
|
112
121
|
const scriptTags = doc.querySelectorAll('script');
|
|
113
122
|
for (const script of scriptTags) {
|
|
114
123
|
if (script.textContent.includes('loader(')) {
|
|
115
|
-
|
|
124
|
+
|
|
116
125
|
const match = script.textContent.match(/loader\s*\(\s*(\{[\s\S]*?\})\s*\)/);
|
|
117
126
|
if (match && match[1]) {
|
|
118
127
|
try {
|
|
119
|
-
|
|
128
|
+
|
|
120
129
|
config = new Function('return ' + match[1])();
|
|
121
130
|
} catch (e) {
|
|
122
131
|
console.error("Failed to parse loader config in new page", e);
|
|
@@ -125,61 +134,53 @@ export const Router = {
|
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
|
|
128
|
-
// If no BareMetal loader config is found, fallback to standard navigation
|
|
129
137
|
if (!config) {
|
|
130
138
|
Loader.log(`No BareMetal config found on ${url}. Falling back to native navigation.`);
|
|
131
139
|
window.location.assign(url);
|
|
132
140
|
return;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
// 2. Prepare transition: identify kept vs destroyed modules
|
|
136
143
|
const modulesToLoad = await Loader.prepare(config);
|
|
137
144
|
|
|
138
|
-
// 3. Swap DOM and Head
|
|
139
145
|
document.title = doc.title;
|
|
140
|
-
|
|
141
|
-
// Swap Head Styles (prevent CSS leak)
|
|
146
|
+
|
|
142
147
|
const oldStyles = document.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]');
|
|
143
148
|
oldStyles.forEach(s => s.remove());
|
|
144
149
|
const newStyles = doc.head.querySelectorAll('link[data-baremetal="style"], style[data-baremetal="style"]');
|
|
145
150
|
newStyles.forEach(s => document.head.appendChild(s.cloneNode(true)));
|
|
146
151
|
|
|
147
|
-
// User Protected Elements (data-baremetal-preserve)
|
|
148
152
|
const preservedNodes = [];
|
|
149
153
|
const persistentElements = document.querySelectorAll('[data-baremetal-preserve]');
|
|
150
|
-
|
|
154
|
+
|
|
151
155
|
persistentElements.forEach(el => {
|
|
152
156
|
if (!el.id) return;
|
|
153
157
|
if (doc.getElementById(el.id)) {
|
|
154
|
-
|
|
158
|
+
|
|
155
159
|
const placeholder = document.createElement('div');
|
|
156
160
|
el.parentNode.replaceChild(placeholder, el);
|
|
157
161
|
preservedNodes.push(el);
|
|
158
162
|
}
|
|
159
163
|
});
|
|
160
164
|
|
|
161
|
-
// Engine Protected Elements (Immortal)
|
|
162
165
|
const transitionRoot = document.getElementById('baremetal-transition-root');
|
|
163
166
|
if (transitionRoot) {
|
|
164
167
|
transitionRoot.parentNode.removeChild(transitionRoot);
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
// The actual synchronous DOM swap and restoration
|
|
168
170
|
const executeDOMSwap = () => {
|
|
169
171
|
document.body.innerHTML = doc.body.innerHTML;
|
|
170
172
|
|
|
171
|
-
// Restore User Protected Elements into their exact new positions
|
|
172
173
|
preservedNodes.forEach(el => {
|
|
173
174
|
const newEl = document.getElementById(el.id);
|
|
174
175
|
if (newEl) {
|
|
175
|
-
|
|
176
|
+
|
|
176
177
|
Array.from(el.attributes).forEach(attr => {
|
|
177
178
|
if (attr.name !== 'id' && attr.name !== 'data-baremetal-preserve') el.removeAttribute(attr.name);
|
|
178
179
|
});
|
|
179
180
|
Array.from(newEl.attributes).forEach(attr => {
|
|
180
181
|
if (attr.name !== 'id') el.setAttribute(attr.name, attr.value);
|
|
181
182
|
});
|
|
182
|
-
|
|
183
|
+
|
|
183
184
|
newEl.parentNode.replaceChild(el, newEl);
|
|
184
185
|
}
|
|
185
186
|
});
|
|
@@ -188,29 +189,39 @@ export const Router = {
|
|
|
188
189
|
document.body.appendChild(transitionRoot);
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
// Notify keep-alive modules that the DOM has been swapped so they can re-bind UI elements
|
|
192
192
|
stateManager.publish('DOM_SWAPPED', null);
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
const doSwap = () => {
|
|
196
|
+
executeDOMSwap();
|
|
197
|
+
window.scrollTo(0, this.scrollMemory[url] || 0);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (Loader.config.transition && Loader.config.transition.useViewTransitions && document.startViewTransition) {
|
|
201
|
+
document.startViewTransition(() => {
|
|
202
|
+
doSwap();
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
doSwap();
|
|
206
|
+
}
|
|
198
207
|
|
|
199
|
-
// 4. Mount new modules (this emits ROUTE_END)
|
|
200
208
|
await Loader.loadPrepared(modulesToLoad);
|
|
201
209
|
|
|
202
210
|
} catch (err) {
|
|
211
|
+
if (err.name === 'AbortError') {
|
|
212
|
+
Loader.log(`Aborted fetch for ${url} due to new navigation.`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
203
215
|
console.error("Routing error:", err);
|
|
204
216
|
stateManager.publish('ROUTE_ERROR', { url, error: err.message });
|
|
205
|
-
|
|
217
|
+
|
|
206
218
|
if (Loader.config.showErrorNotification) {
|
|
207
|
-
|
|
219
|
+
|
|
208
220
|
if (this.historyStack.length > 0) {
|
|
209
221
|
const prev = this.historyStack.pop();
|
|
210
222
|
history.replaceState(null, '', prev);
|
|
211
223
|
}
|
|
212
224
|
|
|
213
|
-
// Show floating notification
|
|
214
225
|
const notif = document.createElement('div');
|
|
215
226
|
notif.style.position = 'fixed';
|
|
216
227
|
notif.style.bottom = '20px';
|
|
@@ -224,15 +235,19 @@ export const Router = {
|
|
|
224
235
|
notif.style.fontFamily = 'sans-serif';
|
|
225
236
|
notif.style.transition = 'opacity 0.3s ease';
|
|
226
237
|
notif.innerHTML = `<strong>Navigation Failed:</strong> ${err.message}`;
|
|
227
|
-
|
|
238
|
+
|
|
228
239
|
document.body.appendChild(notif);
|
|
229
|
-
|
|
240
|
+
|
|
230
241
|
setTimeout(() => {
|
|
231
242
|
notif.style.opacity = '0';
|
|
232
243
|
setTimeout(() => notif.remove(), 300);
|
|
233
244
|
}, 4000);
|
|
234
245
|
} else {
|
|
235
|
-
|
|
246
|
+
|
|
247
|
+
if (this.historyStack.length > 0) {
|
|
248
|
+
const prev = this.historyStack.pop();
|
|
249
|
+
history.replaceState(null, '', prev);
|
|
250
|
+
}
|
|
236
251
|
window.location.assign(url);
|
|
237
252
|
}
|
|
238
253
|
}
|