lightweight-router 1.0.0 → 1.0.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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "files.exclude": {
3
+ "**/.git": true,
4
+ "**/.svn": true,
5
+ "**/.hg": true,
6
+ "**/CVS": true,
7
+ "**/.DS_Store": true,
8
+ "**/Thumbs.db": true
9
+ },
10
+ "hide-files.files": []
11
+ }
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
- # lightweight-router
1
+ # Lightweight Router
2
2
 
3
- A lightweight client-side router with prefetching capabilities.
3
+ A lightweight client-side router with intelligent prefetching capabilities for modern web applications.
4
4
 
5
- ## Installation
5
+ ## Features
6
6
 
7
- ```bash
8
- npm install lightweight-router
9
- ```
7
+ - 🚀 Zero dependencies
8
+ - 🔄 Smooth client-side navigation
9
+ - 📥 Intelligent link prefetching
10
+ - 🎯 Multiple prefetching strategies
11
+ - 🔍 SEO-friendly
12
+ - 📱 Mobile-friendly with data-saver mode support
13
+ - ⚡ Automatic script execution
14
+ - 🎨 Built-in loading animations
10
15
 
11
- ## Usage
16
+ ## Installation
12
17
 
13
- Refer to the documentation for more details.
18
+ ### NPM
package/build.js ADDED
@@ -0,0 +1,51 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const uglifyJS = require("uglify-js");
4
+ const CleanCSS = require("clean-css");
5
+
6
+ // Read the router source
7
+ const routerSource = fs.readFileSync(path.join(__dirname, "src/router.js"), "utf8");
8
+
9
+ // Extract CSS from the source
10
+ const cssRegex = /style\.textContent = `\s*([\s\S]*?)\s*`;/;
11
+ const cssMatch = routerSource.match(cssRegex);
12
+ const css = cssMatch ? cssMatch[1] : "";
13
+
14
+ // Minify CSS
15
+ const minifiedCSS = new CleanCSS({}).minify(css).styles;
16
+
17
+ // Replace original CSS with minified version
18
+ const updatedSource = routerSource.replace(cssRegex, `style.textContent = '${minifiedCSS}';`);
19
+
20
+ // Convert ES module to self-initializing IIFE
21
+ const iife = `
22
+ (function() {
23
+ ${updatedSource.replace("export { startRouter };", "")}
24
+
25
+ // Auto-initialize when DOM is ready
26
+ if (document.readyState === 'loading') {
27
+ document.addEventListener('DOMContentLoaded', () => startRouter());
28
+ } else {
29
+ startRouter();
30
+ }
31
+ })();
32
+ `;
33
+
34
+ // Minify the JavaScript
35
+ const minified = uglifyJS.minify(iife);
36
+
37
+ if (minified.error) {
38
+ console.error("Minification error:", minified.error);
39
+ process.exit(1);
40
+ }
41
+
42
+ // Create dist directory if it doesn't exist
43
+ const distDir = path.join(__dirname, "dist");
44
+ if (!fs.existsSync(distDir)) {
45
+ fs.mkdirSync(distDir);
46
+ }
47
+
48
+ // Write minified file
49
+ fs.writeFileSync(path.join(distDir, "router.min.js"), minified.code);
50
+
51
+ console.log("Build complete! Output saved to dist/router.min.js");
@@ -0,0 +1 @@
1
+ (()=>{let c={},n=async()=>{var e=document.querySelector("router"),t=globalThis.location.pathname;let o=e.querySelector(`route[path="${t}"]`),n=(o||((o=document.createElement("route")).setAttribute("path",globalThis.location.pathname),e.appendChild(o)),document.body.classList.add("loading"),c[globalThis.location.href]);n||(n=await d(globalThis.location.href),c[globalThis.location.href]=n);var r,a=(new DOMParser).parseFromString(n,"text/html"),i=a.querySelector("title"),i=(i&&(document.title=i.textContent),o.innerHTML=a.body.innerHTML,Array.from(o.querySelectorAll("script")));for(r of i){var l=document.createElement("script");r.src?l.src=r.src:l.textContent=r.textContent,r.parentNode.replaceChild(l,r)}e.querySelectorAll("route").forEach(e=>e.style.display="none"),o.style.display="contents",document.body.classList.remove("loading"),window.scrollTo(0,0),s&&s(t)},r=async e=>{c[e.href]||(c[e.href]=await d(e.href))},a=(e,t)=>{e.forEach(e=>{e.isIntersecting&&(e=e.target,c[e.href]||(r(e),t.unobserve(e)))})},i=e=>{var t=e.target.closest("A");t&&t.href&&l(t.href)&&t.origin===location.origin&&(e.preventDefault(),globalThis.history.pushState(null,null,t.href),globalThis.dispatchEvent(new Event("popstate")))};function l(e){if(e&&!e.startsWith("#")&&!e.startsWith("javascript:")){if(e.startsWith("/"))return 1;try{var t=new URL(e,window.location.origin),o=new URL(window.location.href);return o.hostname.replace(/^www\./,"")===t.hostname.replace(/^www\./,"")?o.pathname!==t.pathname||!t.hash:void 0}catch{}}}let s,d=async e=>{e=await t(e);return e.ok?e.text():"Couldn't fetch the route - HTTP error! status: "+e.status},t=async e=>{if(!h){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},h=!1,e=(e={})=>{var t,e=e.onRouteChange,e=(e&&(e=e,s=e),document.createElement("style")),e=(e.textContent="body.loading{animation:pulseOpacity 1s infinite alternate}@keyframes pulseOpacity{from{opacity:.8}to{opacity:.3}}",document.head.appendChild(e),document.querySelector("router")),o=globalThis.location.pathname,o=(e||(e=document.createElement("router"),(t=document.createElement("route")).setAttribute("path",o),t.style.contentVisibility="auto",t.innerHTML=document.body.innerHTML,e.appendChild(t),document.body.innerHTML="",document.body.appendChild(e),h=!0),globalThis.addEventListener("popstate",n),document.addEventListener("click",i),document.body.addEventListener("mouseover",e=>{"A"===e.target.tagName&&"onHover"===e.target.getAttribute("prefetch")&&(async e=>{e=e.target;!c[e.href]&&l(e.href)&&await r(e)})(e)}),new IntersectionObserver(a,{root:null,threshold:.5}));(t=>{let o=navigator.connection&&navigator.connection.saveData;document.querySelectorAll("a").forEach(e=>{"onHover"===e.getAttribute("prefetch")||o||l(e.href)||t.observe(e)})})(o)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ testEnvironment: "jsdom",
3
+ moduleFileExtensions: ["js", "jsx"],
4
+ transform: {},
5
+ testMatch: ["**/__tests__/**/*.test.js"],
6
+ setupFiles: ["<rootDir>/jest.setup.js"],
7
+ moduleDirectories: ["node_modules", "dist"],
8
+ };
package/jest.setup.js ADDED
@@ -0,0 +1,32 @@
1
+ // Mock window.scrollTo
2
+ window.scrollTo = jest.fn();
3
+
4
+ // Mock IntersectionObserver
5
+ global.IntersectionObserver = class IntersectionObserver {
6
+ constructor(callback) {
7
+ this.callback = callback;
8
+ }
9
+
10
+ observe = jest.fn();
11
+ unobserve = jest.fn();
12
+ disconnect = jest.fn();
13
+ };
14
+
15
+ // Mock URL API if needed
16
+ global.URL = class URL {
17
+ constructor(url) {
18
+ this.url = url;
19
+ this.pathname = url;
20
+ this.origin = "http://localhost";
21
+ this.hash = "";
22
+ this.hostname = "localhost";
23
+ }
24
+ };
25
+
26
+ // Mock window.location
27
+ const mockLocation = new URL("http://localhost/");
28
+ delete window.location;
29
+ window.location = mockLocation;
30
+
31
+ // Mock history API
32
+ window.history.pushState = jest.fn();
package/package.json CHANGED
@@ -1,12 +1,23 @@
1
1
  {
2
2
  "name": "lightweight-router",
3
- "version": "1.0.0",
4
- "main": "index.js",
3
+ "version": "1.0.1",
4
+ "main": "dist/router.min.js",
5
5
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
6
+ "dev": "npx http-server src -o",
7
+ "build": "node build.js",
8
+ "deploy": "npm run build && npm version patch && git publish && npm publish",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch"
7
11
  },
8
12
  "keywords": [],
9
13
  "author": "",
10
14
  "license": "ISC",
11
- "description": ""
15
+ "description": "A lightweight client-side router with prefetching capabilities.",
16
+ "devDependencies": {
17
+ "http-server": "^14.1.1",
18
+ "uglify-js": "^3.17.4",
19
+ "clean-css": "^5.3.1",
20
+ "jest": "^29.7.0",
21
+ "jest-environment-jsdom": "^29.7.0"
22
+ }
12
23
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ describe("Router", () => {
6
+ beforeEach(async () => {
7
+ // Setup a clean DOM environment before each test
8
+ document.body.innerHTML = `
9
+ <div>
10
+ <h1>Test Page</h1>
11
+ <a href="/about">About</a>
12
+ <a href="/contact" prefetch="onHover">Contact</a>
13
+ </div>
14
+ `;
15
+
16
+ // Mock fetch API
17
+ global.fetch = jest.fn(() =>
18
+ Promise.resolve({
19
+ ok: true,
20
+ text: () => Promise.resolve("<div>New Content</div>"),
21
+ })
22
+ );
23
+
24
+ // Load the router after DOM is set up
25
+ require("../../dist/router.min.js");
26
+
27
+ // Wait for next tick to allow router initialization
28
+ await new Promise(resolve => setTimeout(resolve, 0));
29
+ });
30
+
31
+ afterEach(() => {
32
+ jest.clearAllMocks();
33
+ // Clear the require cache so we can reload the router in next test
34
+ jest.resetModules();
35
+ });
36
+
37
+ test("initializes router and wraps existing content", async () => {
38
+ const router = document.querySelector("router");
39
+ const route = document.querySelector("route");
40
+
41
+ expect(router).toBeTruthy();
42
+ expect(route).toBeTruthy();
43
+ expect(route.getAttribute("path")).toBe(window.location.pathname);
44
+ });
45
+
46
+ test("handles internal navigation", async () => {
47
+ // Mock the window.location object
48
+ delete window.location;
49
+ window.location = new URL("http://localhost/");
50
+
51
+ // Mock history.pushState to actually update the location
52
+ const originalPushState = window.history.pushState;
53
+ window.history.pushState = jest.fn((state, title, url) => {
54
+ window.location = new URL(url, "http://localhost");
55
+ originalPushState.call(window.history, state, title, url);
56
+ });
57
+
58
+ await new Promise(resolve => setTimeout(resolve, 0));
59
+
60
+ const link = document.querySelector('a[href="/about"]');
61
+ link.click();
62
+
63
+ // Wait for async operations
64
+ await new Promise(resolve => setTimeout(resolve, 0));
65
+
66
+ expect(window.location.pathname).toBe("/about");
67
+ expect(fetch).toHaveBeenCalledWith("/about", expect.any(Object));
68
+ });
69
+
70
+ test("prefetches content on hover for marked links", async () => {
71
+ await new Promise(resolve => setTimeout(resolve, 0));
72
+
73
+ const link = document.querySelector('a[prefetch="onHover"]');
74
+ link.dispatchEvent(new MouseEvent("mouseover"));
75
+
76
+ // Wait for async operations
77
+ await new Promise(resolve => setTimeout(resolve, 0));
78
+
79
+ expect(fetch).toHaveBeenCalledWith("/contact", expect.any(Object));
80
+ });
81
+
82
+ test("updates document title when navigating", async () => {
83
+ await new Promise(resolve => setTimeout(resolve, 0));
84
+
85
+ // Mock fetch to return content with a title
86
+ global.fetch = jest.fn(() =>
87
+ Promise.resolve({
88
+ ok: true,
89
+ text: () =>
90
+ Promise.resolve(`
91
+ <html>
92
+ <head>
93
+ <title>New Page</title>
94
+ </head>
95
+ <body>
96
+ <div>New Content</div>
97
+ </body>
98
+ </html>
99
+ `),
100
+ })
101
+ );
102
+
103
+ const link = document.querySelector('a[href="/about"]');
104
+ link.click();
105
+
106
+ // Wait for async operations
107
+ await new Promise(resolve => setTimeout(resolve, 0));
108
+
109
+ expect(document.title).toBe("New Page");
110
+ });
111
+
112
+ test("handles navigation errors gracefully", async () => {
113
+ await new Promise(resolve => setTimeout(resolve, 0));
114
+
115
+ // Mock fetch to simulate an error
116
+ global.fetch = jest.fn(() =>
117
+ Promise.resolve({
118
+ ok: false,
119
+ status: 404,
120
+ })
121
+ );
122
+
123
+ const link = document.querySelector('a[href="/about"]');
124
+ link.click();
125
+
126
+ // Wait for async operations
127
+ await new Promise(resolve => setTimeout(resolve, 0));
128
+
129
+ const route = document.querySelector('route[path="/about"]');
130
+ expect(route.innerHTML).toContain("Couldn't fetch the route - HTTP error! status: 404");
131
+ });
132
+ });
package/src/about.html ADDED
@@ -0,0 +1,24 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
5
+ <title>Router manual test About</title>
6
+ </head>
7
+ <body style="background-color: bisque">
8
+ <menu class="flex m-0 p-0"
9
+ ><a prefetch="onHover" href="/">Home</a><a href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
+ ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a>
11
+ </menu>
12
+ ABOUT
13
+ <script type="module">
14
+ import { startRouter } from "./router.js";
15
+ // Initialize the router with a simple onRouteChange callback to test functionality
16
+ startRouter({
17
+ onRouteChange: currentRoute => {
18
+ console.log("Route changed:", currentRoute);
19
+ currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
20
+ },
21
+ });
22
+ </script>
23
+ </body>
24
+ </html>
package/src/index.html ADDED
@@ -0,0 +1,24 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
5
+ <title>Router manual test Index</title>
6
+ </head>
7
+ <body>
8
+ <menu class="flex m-0 p-0"
9
+ ><a prefetch="onHover" href="/">Home</a><a preFetch="onHover" href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
+ ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a>
11
+ </menu>
12
+ HOME
13
+ <script type="module">
14
+ import { startRouter } from "./router.js";
15
+ // Initialize the router with a simple onRouteChange callback to test functionality
16
+ startRouter({
17
+ onRouteChange: currentRoute => {
18
+ console.log("Route changed:", currentRoute);
19
+ currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
20
+ },
21
+ });
22
+ </script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,25 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
5
+ <title>Router manual test About</title>
6
+ </head>
7
+ <body style="background-color: bisque">
8
+ <menu class="flex m-0 p-0"
9
+ ><a prefetch="onHover" href="/">Home</a><a href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
+ ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a> </menu
11
+ ><router>
12
+ <route path="/about.html" style="content-visibility: auto"><h1>About</h1></route><route path="/onlyServer" style="content-visibility: auto"></route
13
+ ></router>
14
+ <script type="module">
15
+ import { startRouter } from "./router.js";
16
+ // Initialize the router with a simple onRouteChange callback to test functionality
17
+ startRouter({
18
+ onRouteChange: currentRoute => {
19
+ console.log("Route changed:", currentRoute);
20
+ currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
21
+ },
22
+ });
23
+ </script>
24
+ </body>
25
+ </html>
package/src/router.js CHANGED
@@ -1,34 +1,62 @@
1
- import { fetchContent } from './utils';
2
-
3
- let linkData = new Proxy({}, {
4
- set: (target, property, value) => {
5
- target[property] = value;
6
- const router = document.querySelector("router");
7
- const currentRoute = router?.querySelector(`route[path="${globalThis.location.pathname}"]`) ||
8
- router?.querySelector(`route[path="/default"]`);
9
-
10
- if (currentRoute && !currentRoute.innerHTML && property === globalThis.location.href) {
11
- currentRoute.innerHTML = value;
12
- if (typeof onRouteChange === 'function') onRouteChange(currentRoute);
1
+ let linkData = {};
2
+ const handlePopState = async () => {
3
+ const router = document.querySelector("router");
4
+ const currentPath = globalThis.location.pathname;
5
+
6
+ let currentRoute = router.querySelector(`route[path="${currentPath}"]`);
7
+
8
+ if (!currentRoute) {
9
+ currentRoute = document.createElement("route");
10
+ currentRoute.setAttribute("path", globalThis.location.pathname);
11
+ router.appendChild(currentRoute);
12
+ }
13
+
14
+ document.body.classList.add("loading");
15
+
16
+ let content = linkData[globalThis.location.href];
17
+ if (!content) {
18
+ content = await fetchContent(globalThis.location.href);
19
+ linkData[globalThis.location.href] = content;
20
+ }
21
+
22
+ const parser = new DOMParser();
23
+ const doc = parser.parseFromString(content, "text/html");
24
+
25
+ // Update the page title with the new content's title
26
+ const newTitle = doc.querySelector("title");
27
+ if (newTitle) document.title = newTitle.textContent;
28
+
29
+ currentRoute.innerHTML = doc.body.innerHTML;
30
+
31
+ // Execute scripts from the fetched content
32
+ const scripts = Array.from(currentRoute.querySelectorAll("script"));
33
+ for (const oldScript of scripts) {
34
+ const newScript = document.createElement("script");
35
+ if (oldScript.src) {
36
+ newScript.src = oldScript.src;
37
+ } else {
38
+ newScript.textContent = oldScript.textContent;
13
39
  }
14
- return true;
15
- },
16
- });
40
+ oldScript.parentNode.replaceChild(newScript, oldScript);
41
+ }
17
42
 
18
- let onRouteChange;
43
+ router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
44
+ currentRoute.style.display = "contents";
19
45
 
20
- export const setRouteChangeHandler = (handler) => {
21
- onRouteChange = handler;
22
- };
46
+ document.body.classList.remove("loading");
47
+ // Reset scroll position to the top
48
+ window.scrollTo(0, 0);
23
49
 
24
- const fetchAndSaveContent = async (link) => {
25
- linkData[link.href] = "";
26
- linkData[link.href] = await fetchContent(link.href);
50
+ // Call the route change handler if it's set
51
+ if (onRouteChange) onRouteChange(currentPath);
27
52
  };
28
53
 
29
- const handleLinkHover = async (event) => {
30
- const link = event.target;
31
- if (!linkData[link.href]) await fetchAndSaveContent(link);
54
+ //link management
55
+
56
+ const fetchAndSaveContent = async link => {
57
+ if (!linkData[link.href]) {
58
+ linkData[link.href] = await fetchContent(link.href);
59
+ }
32
60
  };
33
61
 
34
62
  const handleLinkIntersection = (entries, observer) => {
@@ -43,49 +71,113 @@ const handleLinkIntersection = (entries, observer) => {
43
71
  });
44
72
  };
45
73
 
46
- const handlePopState = async () => {
47
- const router = document.querySelector("router");
48
- if (!router) return;
49
-
50
- const currentRoute = router.querySelector(`route[path="${globalThis.location.pathname}"]`) ||
51
- router.querySelector(`route[path="/default"]`);
52
-
53
- router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
54
- currentRoute.style.display = "contents";
55
-
56
- if (!currentRoute.innerHTML) {
57
- currentRoute.innerHTML = await linkData[globalThis.location.href];
58
- if (typeof onRouteChange === 'function') onRouteChange(currentRoute);
74
+ const handleLinkHover = async event => {
75
+ const link = event.target;
76
+ if (!linkData[link.href] && isInternalLink(link.href)) {
77
+ await fetchAndSaveContent(link);
59
78
  }
60
79
  };
61
80
 
62
- const handleLinkClick = (e) => {
63
- if (e.target.tagName === "A" && e.target.href) {
64
- const href = new URL(e.target.href).pathname;
65
- if (href.startsWith("/")) {
66
- e.preventDefault();
67
- globalThis.history.pushState(null, null, e.target.href);
68
- globalThis.dispatchEvent(new Event("popstate"));
69
- }
70
- }
81
+ const handleLinkClick = e => {
82
+ const link = e.target.closest("A");
83
+ if (!link || !link.href || !isInternalLink(link.href) || link.origin !== location.origin) return;
84
+ else e.preventDefault();
85
+ globalThis.history.pushState(null, null, link.href);
86
+ globalThis.dispatchEvent(new Event("popstate"));
71
87
  };
72
88
 
73
- export const observeLinks = (observer) => {
89
+ const observeLinks = observer => {
74
90
  const saveDataOn = navigator.connection && navigator.connection.saveData;
75
91
  const links = document.querySelectorAll("a");
76
-
92
+
77
93
  links.forEach(link => {
78
- if (link.getAttribute("prefetch") !== "onHover" && !saveDataOn) observer.observe(link);
94
+ if (link.getAttribute("prefetch") !== "onHover" && !saveDataOn && !isInternalLink(link.href)) observer.observe(link);
79
95
  });
80
96
  };
81
97
 
82
- export const initializeRouter = () => {
83
- if (typeof document === "undefined") return;
98
+ function isInternalLink(href) {
99
+ if (!href || href.startsWith("#") || href.startsWith("javascript:")) return false;
100
+ if (href.startsWith("/")) return true;
101
+
102
+ try {
103
+ const url = new URL(href, window.location.origin);
104
+ const currentUrl = new URL(window.location.href);
105
+
106
+ const currentHost = currentUrl.hostname.replace(/^www\./, "");
107
+ const targetHost = url.hostname.replace(/^www\./, "");
108
+
109
+ // Compare hosts
110
+ if (currentHost !== targetHost) return false;
111
+
112
+ // Compare paths (ignoring parameters and fragments)
113
+ const currentPath = currentUrl.pathname;
114
+ const targetPath = url.pathname;
115
+
116
+ return currentPath !== targetPath || !url.hash;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ let onRouteChange;
123
+
124
+ const setRouteChangeHandler = handler => {
125
+ onRouteChange = handler;
126
+ };
127
+
128
+ const fetchContent = async url => {
129
+ const response = await fetchWithFallback(url);
130
+ if (!response.ok) {
131
+ return `Couldn't fetch the route - HTTP error! status: ${response.status}`;
132
+ }
133
+ return await response.text();
134
+ };
135
+
136
+ // Updated fetchWithFallback to check the flag
137
+ const fetchWithFallback = async url => {
138
+ if (!routerCreatedManually) {
139
+ const res = await fetch(url, { method: "POST", body: "onlyRoute" });
140
+ if (res.ok) return res;
141
+ }
142
+ return await fetch(url);
143
+ };
144
+
145
+ let routerCreatedManually = false;
146
+ const startRouter = (options = {}) => {
147
+ const { onRouteChange } = options;
148
+ if (onRouteChange) setRouteChangeHandler(onRouteChange);
149
+ const style = document.createElement("style");
150
+ style.textContent = `
151
+ body.loading {
152
+ animation: pulseOpacity 1s infinite alternate;
153
+ }
154
+
155
+ @keyframes pulseOpacity {
156
+ from { opacity: 0.8; }
157
+ to { opacity: 0.3; }
158
+ }
159
+ `;
160
+ document.head.appendChild(style);
161
+
162
+ let router = document.querySelector("router");
163
+ const currentPath = globalThis.location.pathname;
164
+
165
+ if (!router) {
166
+ router = document.createElement("router");
167
+ const route = document.createElement("route");
168
+ route.setAttribute("path", currentPath);
169
+ route.style.contentVisibility = "auto";
170
+ route.innerHTML = document.body.innerHTML;
171
+ router.appendChild(route);
172
+ document.body.innerHTML = "";
173
+ document.body.appendChild(router);
174
+ routerCreatedManually = true;
175
+ }
84
176
 
85
177
  globalThis.addEventListener("popstate", handlePopState);
86
178
  document.addEventListener("click", handleLinkClick);
87
-
88
- document.body.addEventListener("mouseover", (event) => {
179
+
180
+ document.body.addEventListener("mouseover", event => {
89
181
  if (event.target.tagName === "A" && event.target.getAttribute("prefetch") === "onHover") {
90
182
  handleLinkHover(event);
91
183
  }
@@ -94,3 +186,5 @@ export const initializeRouter = () => {
94
186
  const observer = new IntersectionObserver(handleLinkIntersection, { root: null, threshold: 0.5 });
95
187
  observeLinks(observer);
96
188
  };
189
+
190
+ export { startRouter };
package/package.tmp.json DELETED
File without changes
package/src/index.js DELETED
@@ -1,7 +0,0 @@
1
- import { initializeRouter, setRouteChangeHandler } from './router';
2
-
3
- export const startRouter = (options = {}) => {
4
- const { onRouteChange } = options;
5
- if (onRouteChange) setRouteChangeHandler(onRouteChange);
6
- initializeRouter();
7
- };
package/src/utils.js DELETED
@@ -1,20 +0,0 @@
1
- export const getPathname = () => {
2
- const { pathname } = globalThis.location;
3
- return pathname.startsWith("/") ? pathname.slice(1) : pathname;
4
- };
5
-
6
- export const fetchContent = async (url) => {
7
- const response = await fetch(url, { method: "POST", body: "onlyRoute" });
8
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
9
-
10
- const reader = response.body.getReader();
11
- const decoder = new TextDecoder("utf-8");
12
- let content = "";
13
-
14
- while (true) {
15
- const { done, value } = await reader.read();
16
- if (done) break;
17
- content += decoder.decode(value);
18
- }
19
- return content;
20
- };