@flight-framework/router 0.0.2 → 0.0.4

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/dist/index.js CHANGED
@@ -104,26 +104,192 @@ if (typeof globalThis !== "undefined") {
104
104
  }
105
105
  }
106
106
 
107
- // src/link.ts
107
+ // src/prefetch.ts
108
108
  var isBrowser2 = typeof window !== "undefined";
109
+ var supportsIntersectionObserver = isBrowser2 && "IntersectionObserver" in window;
110
+ var prefetchedUrls = /* @__PURE__ */ new Set();
111
+ var prefetchingUrls = /* @__PURE__ */ new Set();
112
+ var viewportObservers = /* @__PURE__ */ new Map();
113
+ function prefetch(href, options = {}) {
114
+ if (!isBrowser2) return;
115
+ const {
116
+ priority = "auto",
117
+ includeModules = true,
118
+ includeData = false
119
+ } = options;
120
+ const url = normalizeUrl(href);
121
+ if (prefetchedUrls.has(url) || prefetchingUrls.has(url)) {
122
+ return;
123
+ }
124
+ prefetchingUrls.add(url);
125
+ createPrefetchLink(url, "document", priority);
126
+ if (includeModules) {
127
+ prefetchModules(url, priority);
128
+ }
129
+ if (includeData) {
130
+ prefetchData(url, priority);
131
+ }
132
+ prefetchedUrls.add(url);
133
+ prefetchingUrls.delete(url);
134
+ }
135
+ function prefetchAll(hrefs, options = {}) {
136
+ for (const href of hrefs) {
137
+ prefetch(href, options);
138
+ }
139
+ }
140
+ function isPrefetched(href) {
141
+ return prefetchedUrls.has(normalizeUrl(href));
142
+ }
143
+ function clearPrefetchCache() {
144
+ prefetchedUrls.clear();
145
+ prefetchingUrls.clear();
146
+ }
147
+ function createPrefetchLink(href, as, priority) {
148
+ if (!isBrowser2) return null;
149
+ const existing = document.querySelector(
150
+ `link[rel="prefetch"][href="${href}"], link[rel="modulepreload"][href="${href}"]`
151
+ );
152
+ if (existing) return existing;
153
+ const link = document.createElement("link");
154
+ if (as === "script") {
155
+ link.rel = "modulepreload";
156
+ } else {
157
+ link.rel = "prefetch";
158
+ link.as = as;
159
+ }
160
+ link.href = href;
161
+ if (priority !== "auto" && "fetchPriority" in link) {
162
+ link.fetchPriority = priority;
163
+ }
164
+ if (priority === "low" && "requestIdleCallback" in window) {
165
+ window.requestIdleCallback(() => {
166
+ document.head.appendChild(link);
167
+ });
168
+ } else {
169
+ document.head.appendChild(link);
170
+ }
171
+ return link;
172
+ }
173
+ function prefetchModules(href, priority) {
174
+ const manifest = window.__FLIGHT_MANIFEST__;
175
+ if (!manifest?.routes) return;
176
+ const routeModules = manifest.routes[href];
177
+ if (!routeModules) return;
178
+ for (const module of routeModules) {
179
+ createPrefetchLink(module, "script", priority);
180
+ }
181
+ }
182
+ function prefetchData(href, priority) {
183
+ const dataUrl = `/_flight/data${href === "/" ? "/index" : href}.json`;
184
+ createPrefetchLink(dataUrl, "fetch", priority);
185
+ }
186
+ var sharedObserver = null;
187
+ var observerCallbacks = /* @__PURE__ */ new Map();
188
+ function getViewportObserver() {
189
+ if (!supportsIntersectionObserver) return null;
190
+ if (!sharedObserver) {
191
+ sharedObserver = new IntersectionObserver(
192
+ (entries) => {
193
+ for (const entry of entries) {
194
+ if (entry.isIntersecting) {
195
+ const callback = observerCallbacks.get(entry.target);
196
+ if (callback) {
197
+ callback();
198
+ sharedObserver?.unobserve(entry.target);
199
+ observerCallbacks.delete(entry.target);
200
+ }
201
+ }
202
+ }
203
+ },
204
+ {
205
+ // Start prefetching when link is 25% visible or within 100px of viewport
206
+ rootMargin: "100px",
207
+ threshold: 0.25
208
+ }
209
+ );
210
+ }
211
+ return sharedObserver;
212
+ }
213
+ function observeForPrefetch(element, href) {
214
+ if (!supportsIntersectionObserver) {
215
+ return () => {
216
+ };
217
+ }
218
+ const observer = getViewportObserver();
219
+ if (!observer) return () => {
220
+ };
221
+ const callback = () => {
222
+ prefetch(href, { priority: "low" });
223
+ };
224
+ observerCallbacks.set(element, callback);
225
+ observer.observe(element);
226
+ const cleanup = () => {
227
+ observer.unobserve(element);
228
+ observerCallbacks.delete(element);
229
+ viewportObservers.delete(element);
230
+ };
231
+ viewportObservers.set(element, cleanup);
232
+ return cleanup;
233
+ }
234
+ function setupIntentPrefetch(element, href) {
235
+ if (!isBrowser2) return () => {
236
+ };
237
+ let prefetchTriggered = false;
238
+ const handleIntent = () => {
239
+ if (!prefetchTriggered) {
240
+ prefetchTriggered = true;
241
+ prefetch(href, { priority: "auto" });
242
+ }
243
+ };
244
+ element.addEventListener("mouseenter", handleIntent, { passive: true });
245
+ element.addEventListener("focus", handleIntent, { passive: true });
246
+ element.addEventListener("touchstart", handleIntent, { passive: true });
247
+ return () => {
248
+ element.removeEventListener("mouseenter", handleIntent);
249
+ element.removeEventListener("focus", handleIntent);
250
+ element.removeEventListener("touchstart", handleIntent);
251
+ };
252
+ }
253
+ function normalizeUrl(href) {
254
+ if (isBrowser2 && !href.startsWith("http")) {
255
+ try {
256
+ const url = new URL(href, window.location.origin);
257
+ return url.pathname + url.search;
258
+ } catch {
259
+ return href;
260
+ }
261
+ }
262
+ return href;
263
+ }
264
+
265
+ // src/link.ts
266
+ var isBrowser3 = typeof window !== "undefined";
109
267
  function handleLinkClick(href, options, event) {
110
268
  if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
111
269
  return;
112
270
  }
271
+ if (event && event.button !== 0) {
272
+ return;
273
+ }
113
274
  event?.preventDefault();
114
275
  const { navigate: navigate2 } = getRouterContext();
115
276
  navigate2(href, options);
116
277
  }
117
278
  function isExternalUrl(href) {
118
279
  if (!href) return false;
119
- return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:");
280
+ return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("javascript:") || href.startsWith("#");
281
+ }
282
+ function normalizePrefetchStrategy(prefetchProp) {
283
+ if (prefetchProp === true) {
284
+ return "intent";
285
+ }
286
+ if (prefetchProp === false || prefetchProp === void 0) {
287
+ return "none";
288
+ }
289
+ return prefetchProp;
120
290
  }
121
291
  function prefetchRoute(href) {
122
- if (!isBrowser2) return;
123
- const link = document.createElement("link");
124
- link.rel = "prefetch";
125
- link.href = href;
126
- document.head.appendChild(link);
292
+ prefetch(href);
127
293
  }
128
294
  var Link = null;
129
295
  if (typeof globalThis !== "undefined") {
@@ -137,58 +303,196 @@ if (typeof globalThis !== "undefined") {
137
303
  className,
138
304
  target,
139
305
  rel,
140
- prefetch: prefetch2 = false,
306
+ prefetch: prefetchProp = "none",
141
307
  replace = false,
142
308
  scroll = true,
143
309
  onClick,
310
+ "aria-label": ariaLabel,
144
311
  ...props
145
312
  }) {
146
313
  const linkRef = useRef(null);
147
314
  const isExternal = isExternalUrl(href);
148
- const handleClick = useCallback((e) => {
149
- if (onClick) {
150
- onClick(e);
151
- if (e.defaultPrevented) return;
152
- }
153
- if (isExternal || target === "_blank") return;
154
- handleLinkClick(href, { replace, scroll }, e);
155
- }, [href, isExternal, target, replace, scroll, onClick]);
315
+ const prefetchStrategy = normalizePrefetchStrategy(prefetchProp);
316
+ const handleClick = useCallback(
317
+ (e) => {
318
+ if (onClick) {
319
+ onClick(e);
320
+ if (e.defaultPrevented) return;
321
+ }
322
+ if (isExternal || target === "_blank") return;
323
+ handleLinkClick(href, { replace, scroll }, e);
324
+ },
325
+ [href, isExternal, target, replace, scroll, onClick]
326
+ );
156
327
  useEffect(() => {
157
- if (!prefetch2 || isExternal || !isBrowser2) return;
328
+ if (isExternal || !isBrowser3 || prefetchStrategy === "none") {
329
+ return;
330
+ }
158
331
  const link = linkRef.current;
159
332
  if (!link) return;
160
- let prefetched = false;
161
- const doPrefetch = () => {
162
- if (!prefetched) {
163
- prefetched = true;
164
- prefetchRoute(href);
165
- }
166
- };
167
- link.addEventListener("mouseenter", doPrefetch);
168
- link.addEventListener("focus", doPrefetch);
169
- return () => {
170
- link.removeEventListener("mouseenter", doPrefetch);
171
- link.removeEventListener("focus", doPrefetch);
172
- };
173
- }, [href, prefetch2, isExternal]);
333
+ switch (prefetchStrategy) {
334
+ case "render":
335
+ prefetch(href, { priority: "low" });
336
+ break;
337
+ case "viewport":
338
+ return observeForPrefetch(link, href);
339
+ case "intent":
340
+ default:
341
+ return setupIntentPrefetch(link, href);
342
+ }
343
+ }, [href, prefetchStrategy, isExternal]);
174
344
  const computedRel = isExternal && target === "_blank" ? rel || "noopener noreferrer" : rel;
175
- return React.createElement("a", {
176
- ref: linkRef,
177
- href,
178
- className,
179
- target,
180
- rel: computedRel,
181
- onClick: handleClick,
182
- ...props
183
- }, children);
345
+ return React.createElement(
346
+ "a",
347
+ {
348
+ ref: linkRef,
349
+ href,
350
+ className,
351
+ target,
352
+ rel: computedRel,
353
+ "aria-label": ariaLabel,
354
+ onClick: handleClick,
355
+ ...props
356
+ },
357
+ children
358
+ );
184
359
  };
185
360
  }
186
361
  } catch {
187
362
  }
188
363
  }
364
+ function createLink(props) {
365
+ const {
366
+ href,
367
+ children,
368
+ className,
369
+ target,
370
+ rel,
371
+ prefetch: prefetchProp = "none",
372
+ replace = false,
373
+ scroll = true
374
+ } = props;
375
+ const anchor = document.createElement("a");
376
+ anchor.href = href;
377
+ anchor.className = className || "";
378
+ if (target) anchor.target = target;
379
+ if (rel) anchor.rel = rel;
380
+ if (typeof children === "string") {
381
+ anchor.textContent = children;
382
+ }
383
+ const isExternal = isExternalUrl(href);
384
+ const prefetchStrategy = normalizePrefetchStrategy(prefetchProp);
385
+ if (!isExternal && target !== "_blank") {
386
+ anchor.addEventListener("click", (e) => {
387
+ handleLinkClick(href, { replace, scroll }, e);
388
+ });
389
+ }
390
+ if (!isExternal && prefetchStrategy !== "none") {
391
+ switch (prefetchStrategy) {
392
+ case "render":
393
+ prefetch(href, { priority: "low" });
394
+ break;
395
+ case "viewport":
396
+ observeForPrefetch(anchor, href);
397
+ break;
398
+ case "intent":
399
+ default:
400
+ setupIntentPrefetch(anchor, href);
401
+ break;
402
+ }
403
+ }
404
+ return anchor;
405
+ }
406
+ function useLinkProps(href, options = {}) {
407
+ const { replace = false, scroll = true, prefetch: prefetchProp = "none" } = options;
408
+ const isExternal = isExternalUrl(href);
409
+ const prefetchStrategy = normalizePrefetchStrategy(prefetchProp);
410
+ const result = {
411
+ href,
412
+ onClick: (e) => {
413
+ if (!isExternal) {
414
+ handleLinkClick(href, { replace, scroll }, e);
415
+ }
416
+ }
417
+ };
418
+ if (prefetchStrategy === "intent" && !isExternal) {
419
+ let prefetched = false;
420
+ const doPrefetch = () => {
421
+ if (!prefetched) {
422
+ prefetched = true;
423
+ prefetch(href);
424
+ }
425
+ };
426
+ result.onMouseenter = doPrefetch;
427
+ result.onFocus = doPrefetch;
428
+ }
429
+ if (prefetchStrategy === "render" && !isExternal && isBrowser3) {
430
+ prefetch(href, { priority: "low" });
431
+ }
432
+ return result;
433
+ }
434
+
435
+ // src/prefetch-links.ts
436
+ var isBrowser4 = typeof window !== "undefined";
437
+ var PrefetchPageLinks = null;
438
+ if (typeof globalThis !== "undefined") {
439
+ try {
440
+ const React = globalThis.React;
441
+ if (React?.createElement && "useEffect" in React) {
442
+ const { useEffect, useState } = React;
443
+ PrefetchPageLinks = function FlightPrefetchPageLinks({
444
+ page,
445
+ options = {}
446
+ }) {
447
+ const [shouldRender, setShouldRender] = useState(false);
448
+ useEffect(() => {
449
+ if (!isBrowser4) return;
450
+ if (isPrefetched(page)) {
451
+ return;
452
+ }
453
+ prefetch(page, {
454
+ priority: "low",
455
+ includeModules: true,
456
+ ...options
457
+ });
458
+ setShouldRender(false);
459
+ }, [page, options]);
460
+ return null;
461
+ };
462
+ }
463
+ } catch {
464
+ }
465
+ }
466
+ function prefetchPages(pages, options = {}) {
467
+ if (!isBrowser4) return;
468
+ for (const page of pages) {
469
+ if (!isPrefetched(page)) {
470
+ prefetch(page, {
471
+ priority: "low",
472
+ ...options
473
+ });
474
+ }
475
+ }
476
+ }
477
+ function prefetchWhenIdle(page, options = {}) {
478
+ if (!isBrowser4) return;
479
+ const doPrefetch = () => {
480
+ if (!isPrefetched(page)) {
481
+ prefetch(page, {
482
+ priority: "low",
483
+ ...options
484
+ });
485
+ }
486
+ };
487
+ if ("requestIdleCallback" in window) {
488
+ window.requestIdleCallback(doPrefetch, { timeout: 3e3 });
489
+ } else {
490
+ setTimeout(doPrefetch, 100);
491
+ }
492
+ }
189
493
 
190
494
  // src/hooks.ts
191
- var isBrowser3 = typeof window !== "undefined";
495
+ var isBrowser5 = typeof window !== "undefined";
192
496
  var useParams = () => ({});
193
497
  var useSearchParams = () => [new URLSearchParams(), () => {
194
498
  }];
@@ -210,10 +514,10 @@ if (typeof globalThis !== "undefined") {
210
514
  };
211
515
  useSearchParams = function useFlightSearchParams() {
212
516
  const [searchParams, setSearchParamsState] = useState(
213
- () => isBrowser3 ? new URLSearchParams(window.location.search) : new URLSearchParams()
517
+ () => isBrowser5 ? new URLSearchParams(window.location.search) : new URLSearchParams()
214
518
  );
215
519
  useEffect(() => {
216
- if (!isBrowser3) return;
520
+ if (!isBrowser5) return;
217
521
  const handleChange = () => {
218
522
  setSearchParamsState(new URLSearchParams(window.location.search));
219
523
  };
@@ -221,7 +525,7 @@ if (typeof globalThis !== "undefined") {
221
525
  return () => window.removeEventListener("popstate", handleChange);
222
526
  }, []);
223
527
  const setSearchParams = useCallback((newParams) => {
224
- if (!isBrowser3) return;
528
+ if (!isBrowser5) return;
225
529
  let params;
226
530
  if (newParams instanceof URLSearchParams) {
227
531
  params = newParams;
@@ -245,7 +549,7 @@ if (typeof globalThis !== "undefined") {
245
549
  const { path } = getRouterContext();
246
550
  const [pathname, setPathname] = useState(path);
247
551
  useEffect(() => {
248
- if (!isBrowser3) return;
552
+ if (!isBrowser5) return;
249
553
  const handleChange = () => {
250
554
  setPathname(window.location.pathname);
251
555
  };
@@ -260,20 +564,11 @@ if (typeof globalThis !== "undefined") {
260
564
  }
261
565
 
262
566
  // src/navigate.ts
263
- var isBrowser4 = typeof window !== "undefined";
567
+ var isBrowser6 = typeof window !== "undefined";
264
568
  function navigate(to, options = {}) {
265
569
  const { navigate: routerNavigate } = getRouterContext();
266
570
  routerNavigate(to, options);
267
571
  }
268
- function prefetch(href) {
269
- if (!isBrowser4) return;
270
- const existing = document.querySelector(`link[rel="prefetch"][href="${href}"]`);
271
- if (existing) return;
272
- const link = document.createElement("link");
273
- link.rel = "prefetch";
274
- link.href = href;
275
- document.head.appendChild(link);
276
- }
277
572
  function patternToRegex(pattern) {
278
573
  const paramNames = [];
279
574
  let regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\[\.\.\.(\w+)\\\]/g, (_, name) => {
@@ -308,14 +603,63 @@ function parseParams(pathname, pattern) {
308
603
  const { params } = matchRoute(pathname, pattern);
309
604
  return params;
310
605
  }
606
+ function findRoute(pathname, routes) {
607
+ for (const route of routes) {
608
+ const { matched, params } = matchRoute(pathname, route.path);
609
+ if (matched) {
610
+ return {
611
+ route,
612
+ params,
613
+ pathname
614
+ };
615
+ }
616
+ }
617
+ return null;
618
+ }
619
+ function generatePath(pattern, params = {}) {
620
+ let path = pattern;
621
+ path = path.replace(/\[(\w+)\]/g, (_, name) => {
622
+ return params[name] || "";
623
+ });
624
+ path = path.replace(/:(\w+)/g, (_, name) => {
625
+ return params[name] || "";
626
+ });
627
+ return path;
628
+ }
629
+ function isActive(pattern) {
630
+ const { path } = getRouterContext();
631
+ const { matched } = matchRoute(path, pattern);
632
+ return matched;
633
+ }
634
+ function redirect(url) {
635
+ if (isBrowser6) {
636
+ window.location.href = url;
637
+ }
638
+ throw new Error(`Redirect to: ${url}`);
639
+ }
311
640
  export {
312
641
  Link,
642
+ PrefetchPageLinks,
313
643
  RouterContext,
314
644
  RouterProvider,
645
+ clearPrefetchCache,
646
+ createLink,
647
+ findRoute,
648
+ generatePath,
649
+ isActive,
650
+ isPrefetched,
315
651
  matchRoute,
316
652
  navigate,
653
+ observeForPrefetch,
317
654
  parseParams,
318
655
  prefetch,
656
+ prefetchAll,
657
+ prefetchPages,
658
+ prefetchRoute,
659
+ prefetchWhenIdle,
660
+ redirect,
661
+ setupIntentPrefetch,
662
+ useLinkProps,
319
663
  useParams,
320
664
  usePathname,
321
665
  useRouter,
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "@flight-framework/router",
3
- "version": "0.0.2",
4
- "description": "Agnostic client-side routing primitives for Flight Framework",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
13
- }
14
- },
15
- "files": [
16
- "dist"
17
- ],
18
- "keywords": [
19
- "flight",
20
- "router",
21
- "spa",
22
- "navigation",
23
- "ssr"
24
- ],
25
- "author": "Flight Framework",
26
- "license": "MIT",
27
- "repository": {
28
- "type": "git",
29
- "url": "https://github.com/EliosLT/Flight-framework",
30
- "directory": "packages/router"
31
- },
32
- "devDependencies": {
33
- "tsup": "^8.0.0",
34
- "typescript": "^5.3.0"
35
- },
36
- "peerDependencies": {
37
- "react": ">=18.0.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "react": {
41
- "optional": true
42
- }
43
- },
44
- "scripts": {
45
- "build": "tsup src/index.ts --format esm --dts --clean",
46
- "dev": "tsup src/index.ts --format esm --dts --watch"
47
- }
48
- }
1
+ {
2
+ "name": "@flight-framework/router",
3
+ "version": "0.0.4",
4
+ "description": "Agnostic client-side routing primitives for Flight Framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format esm --dts --watch"
21
+ },
22
+ "keywords": [
23
+ "flight",
24
+ "router",
25
+ "spa",
26
+ "navigation",
27
+ "ssr"
28
+ ],
29
+ "author": "Flight Framework",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/EliosLT/Flight-framework",
34
+ "directory": "packages/router"
35
+ },
36
+ "devDependencies": {
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.3.0"
39
+ },
40
+ "peerDependencies": {
41
+ "react": ">=18.0.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "react": {
45
+ "optional": true
46
+ }
47
+ }
48
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024-2026 Flight Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.