lightweight-router 1.0.7 → 1.0.8

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Lightweight Router
1
+ # PageFlick
2
2
 
3
- A minimal lightweight client-side router with intelligent prefetching capabilities for faster websites. This tool can turn any Multi-Page Application (MPA) into a Single-Page Application (SPA) very easily and with just ~1.5KB byte (gzipped).
3
+ PageFlick is a minimal lightweight client-side router with intelligent prefetching capabilities for faster websites. This tool can turn any Multi-Page Application (MPA) into a Single-Page Application (SPA) very easily and with just ~1.5KB byte (gzipped).
4
4
 
5
5
  ## Features
6
6
 
@@ -12,6 +12,7 @@ A minimal lightweight client-side router with intelligent prefetching capabiliti
12
12
  - 📱 Mobile-friendly with data-saver mode support
13
13
  - 🎨 Built-in loading animations
14
14
  - 🕰️ Based on History API so you can use native browser navigation
15
+ - 🤖 Automatic title change
15
16
 
16
17
 
17
18
  ## Installation
@@ -107,11 +108,11 @@ If you have too many links at once or too many requests, you can add the `prefet
107
108
  P.S. you can easily test in your website by pasting the ultra minified version into the console.
108
109
  The minified version was created with uglify-js, clean.css and then ultra minified with https://packjs.com
109
110
  The size of the gzipped version was calculated with: https://dafrok.github.io/gzip-size-online/
110
- It's worth to note that nonetheless Terser give better results than uglify-js. The final uglify version packed by packjs.com was even smaller.
111
+ It's worth to note that nonetheless Terser give better results than uglify-js. The final uglify version packed by packjs.com was the smallest.
111
112
 
112
113
  ## Browser Support
113
114
 
114
- The router supports all modern browsers. Required features:
115
+ The router is intended for modern browsers. Required features:
115
116
 
116
117
  - IntersectionObserver
117
118
  - Fetch API
@@ -122,7 +123,7 @@ For older browsers, consider using the following polyfills:
122
123
  - intersection-observer
123
124
  - whatwg-fetch
124
125
 
125
- ## Server Configuration
126
+ ## Optional Server Configuration
126
127
 
127
128
  Configuring your server to return only the route content can make the router much more efficient. Instead of returning the entire page, the server could return only the content for the requested route when it detects a request with the message "onlyRoute".
128
129
 
@@ -137,17 +138,17 @@ Once you configured your server to respond to this type of request, wrap the par
137
138
  ```html
138
139
  <-- Header menu and parts that don't change -->
139
140
  <router>
140
- <route path="/" style="content-visibility: auto">home content</route>
141
+ <route path="/">home content</route>
141
142
  </router>
142
143
  <-- footer etc.. -->
143
144
  ```
144
145
 
145
- You can also prerender other important routes by rendering them inside the `router` tag in their appropriate `route` tags for faster loading times:
146
+ You can also prerender most visited routes by rendering them inside the `router` tag in their appropriate `route` tags for faster loading times:
146
147
 
147
148
  ```html
148
149
  <router>
149
- <route path="/" style="content-visibility: auto">home content</route>
150
- <route path="/about" style="content-visibility: auto; display:none;">about content</route>
150
+ <route path="/">home content</route>
151
+ <route path="/about" style="display:none;">about content</route>
151
152
  </router>
152
153
  ```
153
154
 
@@ -155,10 +156,23 @@ In the future you will also be able to pre-render a default route that will be u
155
156
 
156
157
  Right now errors are shown without styling as the content of the page.
157
158
 
158
- Soon there will be a DenoJS library that will help you deal with all these routes stuff. It will also come with api routes functionality 🔥
159
+ If you like to use Preact and Deno for easy server config consider using the server side router present in [Singularity](https://github.com/andreafuturi/Singularity/) framework.
160
+ It does exactly what we're talking about automatically by returning full html renders on normal request (sorrounded by an Index.jsx) and returing only partial route html when route is requested from inside the page (with onlyRoute param).
159
161
 
160
- ## Performance Tips
161
162
 
162
- - Use `content-visibility: auto` on route elements to improve rendering performance
163
+ ## Performance Tips
163
164
  - Implement server-side partial responses for better bandwidth usage
164
165
  - Consider using the `prefetch="onHover"` attribute for less important links
166
+
167
+ ## Future Development
168
+ - Delay router intialization on first link hover for better performances?
169
+ - Implement html streaming for faster page load
170
+ - Cooler Error handling
171
+ - Disable caching on certain links/routes (that needs to be always up to date)
172
+ - Cache limiting
173
+
174
+
175
+
176
+
177
+
178
+
@@ -1 +1 @@
1
- (()=>{let l={},n=async()=>{document.body.classList.add("loading");var e=globalThis.location.pathname,t=document.querySelector("router");let o=t.querySelector(`route[path="${e}"]`);if(o||((o=document.createElement("route")).setAttribute("path",e),t.appendChild(o)),!o.innerHTML){let e=l[globalThis.location.href];e||(e=await d(globalThis.location.href),l[globalThis.location.href]=e);var n,r=(new DOMParser).parseFromString(e,"text/html"),a=r.querySelector("title"),a=(a&&(document.title=a.textContent),o.innerHTML=r.body.innerHTML,Array.from(o.querySelectorAll("script")));for(n of a){var i=document.createElement("script");n.src?i.src=n.src:i.textContent=n.textContent,n.parentNode.replaceChild(i,n)}}t.querySelectorAll("route").forEach(e=>e.style.display="none"),o.style.display="contents",document.body.classList.remove("loading"),window.scrollTo(0,0),s&&s(e)},r=async e=>{l[e.href]||(l[e.href]=await d(e.href))},a=(e,t)=>{e.forEach(e=>{e.isIntersecting&&(e=e.target,l[e.href]||(r(e),t.unobserve(e)))})},i=e=>{var t=e.target.closest("A");t&&t.href&&c(t.href)&&t.origin===location.origin&&(e.preventDefault(),globalThis.history.pushState(null,null,t.href),globalThis.dispatchEvent(new Event("popstate")))};function c(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=".loading{animation:pulse 1s infinite alternate}@keyframes pulse{from{opacity:.6}to{opacity:.1}}route{content-visibility:auto}",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.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;!l[e.href]&&c(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||c(e.href)||t.observe(e)})})(o)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
1
+ (()=>{let l={},o=!1,c=(...e)=>o&&console.log("🚦 Router:",...e),r=async()=>{c("Navigation triggered to:",globalThis.location.pathname),document.body.classList.add("loading");var e=globalThis.location.pathname,t=document.querySelector("router");let n=t.querySelector(`route[path="${e}"]`);if(n||(c("Creating new route element for:",e),(n=document.createElement("route")).setAttribute("path",e),t.appendChild(n)),!n.innerHTML){c("Fetching content for:",globalThis.location.href);let e=l[globalThis.location.href];e||(e=await u(globalThis.location.href),l[globalThis.location.href]=e);var o,r=(new DOMParser).parseFromString(e,"text/html"),a=r.querySelector("title"),a=(a&&(c("Updating page title to:",a.textContent),document.title=a.textContent),n.innerHTML=r.body.innerHTML,Array.from(n.querySelectorAll("script")));c("Executing",a.length,"scripts from fetched content");for(o of a){var i=document.createElement("script");o.src?i.src=o.src:i.textContent=o.textContent,o.parentNode.replaceChild(i,o)}}t.querySelectorAll("route").forEach(e=>e.style.display="none"),n.style.display="contents",document.body.classList.remove("loading"),window.scrollTo(0,0),h&&h(e),c("Route change completed")},a=async e=>{l[e.href]||(c("Prefetching content for:",e.href),l[e.href]=await u(e.href))},i=(e,t)=>{e.forEach(e=>{e.isIntersecting&&(e=e.target,l[e.href]||(a(e),t.unobserve(e)))})},s=e=>{var t=e.target.closest("A");t&&t.href&&d(t.href)&&t.origin===location.origin?(c("Internal link clicked:",t.href),e.preventDefault(),globalThis.history.pushState(null,null,t.href),globalThis.dispatchEvent(new Event("popstate"))):c("Invalid link click:",t?.href)};function d(e){if(e&&!e.startsWith("#")&&!e.startsWith("javascript:")){if(e.startsWith("/"))return 1;try{var t=new URL(e,window.location.origin),n=new URL(window.location.href);return n.hostname.replace(/^www\./,"")===t.hostname.replace(/^www\./,"")?n.pathname!==t.pathname||!t.hash:void 0}catch{}}}let h,u=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(!f){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},f=!1,e=(e={})=>{var{onRouteChange:t,debug:n}=e,e=(o=n,c("Router starting...",e),t&&(n=t,h=n),document.createElement("style")),t=(e.textContent=".loading{animation:pulse 1s infinite alternate}@keyframes pulse{from{opacity:.6}to{opacity:.1}}route{content-visibility:auto}",document.head.appendChild(e),document.querySelector("router")),n=globalThis.location.pathname,n=(t||(c("Creating new router element"),t=document.createElement("router"),(e=document.createElement("route")).setAttribute("path",n),e.innerHTML=document.body.innerHTML,t.appendChild(e),document.body.innerHTML="",document.body.appendChild(t),f=!0),globalThis.addEventListener("popstate",r),document.addEventListener("click",s),document.body.addEventListener("mouseover",e=>{"A"===e.target.tagName&&"onHover"===e.target.getAttribute("prefetch")&&(async e=>{e=e.target;!l[e.href]&&d(e.href)&&await a(e)})(e)}),new IntersectionObserver(i,{root:null,threshold:.5}));(t=>{let n=navigator.connection&&navigator.connection.saveData;document.querySelectorAll("a").forEach(e=>{"onHover"===e.getAttribute("prefetch")||n||d(e.href)||t.observe(e)})})(n)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightweight-router",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "main": "src/router.js",
5
5
  "scripts": {
6
6
  "dev": "npx http-server src -o",
@@ -129,4 +129,30 @@ describe("Router", () => {
129
129
  const route = document.querySelector('route[path="/about"]');
130
130
  expect(route.innerHTML).toContain("Couldn't fetch the route - HTTP error! status: 404");
131
131
  });
132
+
133
+ test("considers paths with and without trailing slashes as the same route", async () => {
134
+ // Mock the window.location object
135
+ delete window.location;
136
+ window.location = new URL("http://localhost/");
137
+
138
+ // Mock history.pushState to actually update the location
139
+ const originalPushState = window.history.pushState;
140
+ window.history.pushState = jest.fn((state, title, url) => {
141
+ window.location = new URL(url, "http://localhost");
142
+ originalPushState.call(window.history, state, title, url);
143
+ });
144
+
145
+ await new Promise(resolve => setTimeout(resolve, 0));
146
+
147
+ const linkWithSlash = document.createElement("a");
148
+ linkWithSlash.href = "/about/";
149
+ document.body.appendChild(linkWithSlash);
150
+ linkWithSlash.click();
151
+
152
+ // Wait for async operations
153
+ await new Promise(resolve => setTimeout(resolve, 0));
154
+
155
+ expect(window.location.pathname).toBe("/about");
156
+ expect(fetch).toHaveBeenCalledWith("/about", expect.any(Object));
157
+ });
132
158
  });
package/src/router.js CHANGED
@@ -1,13 +1,17 @@
1
1
  let linkData = {};
2
+ let debugMode = false;
3
+ const log = (...args) => debugMode && console.log("🚦 Router:", ...args);
2
4
  const handlePopState = async () => {
5
+ log("Navigation triggered to:", globalThis.location.pathname);
3
6
  document.body.classList.add("loading");
4
- const currentPath = globalThis.location.pathname;
7
+ const currentPath = globalThis.location.pathname.replace(/\/$/, ""); // Normalize path by removing trailing slash
5
8
  const router = document.querySelector("router");
6
9
 
7
10
  let currentRoute = router.querySelector(`route[path="${currentPath}"]`);
8
11
 
9
12
  // If the route doesn't exist in DOM, create and append it
10
13
  if (!currentRoute) {
14
+ log("Creating new route element for:", currentPath);
11
15
  currentRoute = document.createElement("route");
12
16
  currentRoute.setAttribute("path", currentPath);
13
17
  router.appendChild(currentRoute);
@@ -15,6 +19,7 @@ const handlePopState = async () => {
15
19
 
16
20
  // Only fetch and render content if the route is empty
17
21
  if (!currentRoute.innerHTML) {
22
+ log("Fetching content for:", globalThis.location.href);
18
23
  let content = linkData[globalThis.location.href];
19
24
 
20
25
  // Fetch content if it's not already cached
@@ -28,12 +33,16 @@ const handlePopState = async () => {
28
33
 
29
34
  // Update the page title with the new content's title
30
35
  const newTitle = doc.querySelector("title");
31
- if (newTitle) document.title = newTitle.textContent;
36
+ if (newTitle) {
37
+ log("Updating page title to:", newTitle.textContent);
38
+ document.title = newTitle.textContent;
39
+ }
32
40
 
33
41
  currentRoute.innerHTML = doc.body.innerHTML;
34
42
 
35
43
  // Execute scripts from the fetched content
36
44
  const scripts = Array.from(currentRoute.querySelectorAll("script"));
45
+ log("Executing", scripts.length, "scripts from fetched content");
37
46
  for (const oldScript of scripts) {
38
47
  const newScript = document.createElement("script");
39
48
  if (oldScript.src) {
@@ -54,12 +63,14 @@ const handlePopState = async () => {
54
63
 
55
64
  // Call the route change handler if it's set
56
65
  if (onRouteChange) onRouteChange(currentPath);
66
+ log("Route change completed");
57
67
  };
58
68
 
59
69
  //link management
60
70
 
61
71
  const fetchAndSaveContent = async link => {
62
72
  if (!linkData[link.href]) {
73
+ log("Prefetching content for:", link.href);
63
74
  linkData[link.href] = await fetchContent(link.href);
64
75
  }
65
76
  };
@@ -85,8 +96,12 @@ const handleLinkHover = async event => {
85
96
 
86
97
  const handleLinkClick = e => {
87
98
  const link = e.target.closest("A");
88
- if (!link || !link.href || !isInternalLink(link.href) || link.origin !== location.origin) return;
89
- else e.preventDefault();
99
+ if (!link || !link.href || !isInternalLink(link.href) || link.origin !== location.origin) {
100
+ log("Invalid link click:", link?.href);
101
+ return;
102
+ }
103
+ log("Internal link clicked:", link.href);
104
+ e.preventDefault();
90
105
  globalThis.history.pushState(null, null, link.href);
91
106
  globalThis.dispatchEvent(new Event("popstate"));
92
107
  };
@@ -149,7 +164,9 @@ const fetchWithFallback = async url => {
149
164
 
150
165
  let routerCreatedManually = false;
151
166
  const startRouter = (options = {}) => {
152
- const { onRouteChange } = options;
167
+ const { onRouteChange, debug } = options;
168
+ debugMode = debug;
169
+ log("Router starting...", options);
153
170
  if (onRouteChange) setRouteChangeHandler(onRouteChange);
154
171
  const style = document.createElement("style");
155
172
  style.textContent = `
@@ -170,6 +187,7 @@ const startRouter = (options = {}) => {
170
187
  const currentPath = globalThis.location.pathname;
171
188
 
172
189
  if (!router) {
190
+ log("Creating new router element");
173
191
  router = document.createElement("router");
174
192
  const route = document.createElement("route");
175
193
  route.setAttribute("path", currentPath);
@@ -197,8 +215,3 @@ export { startRouter };
197
215
 
198
216
  // TODO: create ultra minified version or deploy
199
217
  // TODO: write proper automated tests
200
- // - add support for prefetching on hover
201
- // - add support for prefetching on click
202
- // - add support for prefetching on scroll
203
- // - add support for prefetching on focus
204
- // - add support for prefetching on touch