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/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
- **Parameters:**
11
- - `config.debug` *(boolean)*: Enable verbose console logs (default: `false`).
12
- - `config.keepAliveSameModules` *(boolean)*: If true, modules shared between routes are not destroyed during navigation (default: `true`).
13
- - `config.autoWrap` *(boolean)*: If true, modules that do not export a `mount` function are automatically wrapped in a dynamic Blob URL to prevent syntax errors (default: `true`).
14
- - `config.transition` *(object)*: Configuration for the native page transition API.
15
- - `config.transition.enabled` *(boolean)*: Enables the protected transition module.
16
- - `config.transition.module` *(string)*: Optional path to a custom transition module (defaults to `/src/transition.js`).
17
- - `config.transition.simulatedDelay` *(number)*: Optional artificial delay in milliseconds (useful for testing animations locally).
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
- ## 5. Writing Custom Page Transitions
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.0.0",
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
- "test": "echo \"No tests written yet.\" && exit 0"
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": "Divyansh",
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, // Aliased for backward compatibility with Pub/Sub mental model
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
- // Initialize Router
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: {}, // { key: { path: "./dash.js", module: exportedModule } }
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
- // Auto-inject transition module if enabled
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
- if ((this.config.keepAliveSameModules || isImmortal) && newConfig[key] === mod.path) {
32
- // Keep alive
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
- // Mark for destruction
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, path]) => {
74
- this.log(`Importing module: ${path}`);
75
- try {
76
- const resolvedPath = new URL(path, document.baseURI).href;
77
- const noCachePath = `${resolvedPath}?t=${Date.now()}`;
78
-
79
- let module;
80
-
81
- if (this.config.autoWrap) {
82
- // Fetch source to check if it needs auto-wrapping
83
- const response = await fetch(noCachePath);
84
- if (!response.ok) throw new Error(`Failed to fetch ${noCachePath}`);
85
- const sourceText = await response.text();
86
-
87
- const hasMount = /export\s+(function|const|let|var)\s+mount\b/.test(sourceText) || /export\s+\{.*?\bmount\b.*?\}/.test(sourceText);
88
-
89
- if (!hasMount) {
90
- console.warn(`[BareMetal] WARNING: Module ${path} does not explicitly export a mount() function. Auto-wrapping it...`);
91
-
92
- const wrappedSource = `
93
- export async function mount(context) {
94
- const { state } = context;
95
- ${sourceText}
96
- }
97
- `;
98
-
99
- const blob = new Blob([wrappedSource], { type: 'application/javascript' });
100
- const blobUrl = URL.createObjectURL(blob);
101
- module = await import(blobUrl);
102
- URL.revokeObjectURL(blobUrl);
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(noCachePath);
103
+ module = await import(loadPath);
105
104
  }
106
- } else {
107
- // Auto-wrapping disabled, load natively
108
- module = await import(noCachePath);
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
- if (typeof module.mount === 'function') {
112
- this.log(`Mounting module: ${path}`);
113
- const instance = await module.mount({ state: stateManager });
114
-
115
- this.activeModules[key] = {
116
- path: path,
117
- module: instance ? { destroy: instance.destroy } : module
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
- console.error(`[BareMetal] Module ${path} failed to provide a mount function even after wrapping.`);
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
- // Find closest anchor tag
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; // Let browser handle it naturally
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'; // Prevent duplicate fetches
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
- // Custom programmatic back button
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(); // Fallback to browser's native 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
- // Extract the JSON-like object from loader({...})
124
+
116
125
  const match = script.textContent.match(/loader\s*\(\s*(\{[\s\S]*?\})\s*\)/);
117
126
  if (match && match[1]) {
118
127
  try {
119
- // Using Function to safely parse object string that might not be strict JSON
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
- // Synchronously detach the live node
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
- // Sync attributes from the new HTML node so classes/styles update
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
- // Execute DOM swap and restore scroll synchronously
196
- executeDOMSwap();
197
- window.scrollTo(0, this.scrollMemory[url] || 0);
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
- // Revert URL bar since we are aborting
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
- // Formal redirect to let server handle the error
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
  }