@flight-framework/router 0.3.0 → 0.3.2

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
@@ -1,28 +1,326 @@
1
- import {
2
- PrefetchPageLinks,
3
- RouterContext,
4
- RouterProvider,
5
- clearPrefetchCache,
6
- findRoute,
7
- generatePath,
8
- getRouterContext,
9
- isActive,
10
- isPrefetched,
11
- matchRoute,
12
- navigate,
13
- observeForPrefetch,
14
- parseParams,
15
- prefetch,
16
- prefetchAll,
17
- prefetchPages,
18
- prefetchWhenIdle,
19
- redirect,
20
- setupIntentPrefetch,
21
- useRouter
22
- } from "./chunk-YXMDNDIZ.js";
1
+ // src/context.ts
2
+ var isBrowser = typeof window !== "undefined";
3
+ var currentContext = {
4
+ path: "/",
5
+ searchParams: new URLSearchParams(),
6
+ navigate: () => {
7
+ },
8
+ back: () => {
9
+ },
10
+ forward: () => {
11
+ }
12
+ };
13
+ var subscribers = /* @__PURE__ */ new Set();
14
+ function subscribe(callback) {
15
+ subscribers.add(callback);
16
+ return () => subscribers.delete(callback);
17
+ }
18
+ function getRouterContext() {
19
+ return currentContext;
20
+ }
21
+ function updateContext(updates) {
22
+ currentContext = { ...currentContext, ...updates };
23
+ subscribers.forEach((cb) => cb(currentContext));
24
+ }
25
+ function navigateTo(to, options = {}) {
26
+ if (!isBrowser) return;
27
+ const { replace = false, scroll = true, state } = options;
28
+ if (replace) {
29
+ window.history.replaceState(state ?? null, "", to);
30
+ } else {
31
+ window.history.pushState(state ?? null, "", to);
32
+ }
33
+ const url = new URL(to, window.location.origin);
34
+ updateContext({
35
+ path: url.pathname,
36
+ searchParams: url.searchParams
37
+ });
38
+ if (scroll) {
39
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
40
+ }
41
+ }
42
+ function initRouter(options = {}) {
43
+ const { initialPath, basePath = "" } = options;
44
+ let path;
45
+ let searchParams;
46
+ if (isBrowser) {
47
+ path = window.location.pathname;
48
+ searchParams = new URLSearchParams(window.location.search);
49
+ } else {
50
+ path = initialPath || "/";
51
+ searchParams = new URLSearchParams();
52
+ }
53
+ if (basePath && path.startsWith(basePath)) {
54
+ path = path.slice(basePath.length) || "/";
55
+ }
56
+ currentContext = {
57
+ path,
58
+ searchParams,
59
+ navigate: navigateTo,
60
+ back: () => isBrowser && window.history.back(),
61
+ forward: () => isBrowser && window.history.forward()
62
+ };
63
+ if (isBrowser) {
64
+ window.addEventListener("popstate", () => {
65
+ updateContext({
66
+ path: window.location.pathname,
67
+ searchParams: new URLSearchParams(window.location.search)
68
+ });
69
+ });
70
+ const originalPushState = history.pushState.bind(history);
71
+ const originalReplaceState = history.replaceState.bind(history);
72
+ history.pushState = function(state, unused, url) {
73
+ originalPushState(state, unused, url);
74
+ if (url) {
75
+ const newUrl = new URL(url.toString(), window.location.origin);
76
+ updateContext({
77
+ path: newUrl.pathname,
78
+ searchParams: newUrl.searchParams
79
+ });
80
+ }
81
+ };
82
+ history.replaceState = function(state, unused, url) {
83
+ originalReplaceState(state, unused, url);
84
+ if (url) {
85
+ const newUrl = new URL(url.toString(), window.location.origin);
86
+ updateContext({
87
+ path: newUrl.pathname,
88
+ searchParams: newUrl.searchParams
89
+ });
90
+ }
91
+ };
92
+ }
93
+ }
94
+ var initialized = false;
95
+ if (isBrowser && !initialized) {
96
+ initialized = true;
97
+ initRouter();
98
+ }
99
+ var RouterContext = null;
100
+ var RouterProvider = null;
101
+ var useRouter = getRouterContext;
102
+ if (typeof globalThis !== "undefined") {
103
+ try {
104
+ const React = globalThis.React;
105
+ if (React?.createContext) {
106
+ const { createContext, useState, useEffect, useContext } = React;
107
+ const ReactRouterContext = createContext(currentContext);
108
+ RouterContext = ReactRouterContext;
109
+ RouterProvider = function FlightRouterProvider({
110
+ children,
111
+ initialPath,
112
+ basePath = ""
113
+ }) {
114
+ const [routerState, setRouterState] = useState(() => {
115
+ const path = isBrowser ? window.location.pathname : initialPath || "/";
116
+ const searchParams = isBrowser ? new URLSearchParams(window.location.search) : new URLSearchParams();
117
+ return {
118
+ path: basePath && path.startsWith(basePath) ? path.slice(basePath.length) || "/" : path,
119
+ searchParams,
120
+ navigate: navigateTo,
121
+ back: () => isBrowser && window.history.back(),
122
+ forward: () => isBrowser && window.history.forward()
123
+ };
124
+ });
125
+ useEffect(() => {
126
+ if (!isBrowser) return;
127
+ const handlePopState = () => {
128
+ let path = window.location.pathname;
129
+ if (basePath && path.startsWith(basePath)) {
130
+ path = path.slice(basePath.length) || "/";
131
+ }
132
+ setRouterState((prev) => ({
133
+ ...prev,
134
+ path,
135
+ searchParams: new URLSearchParams(window.location.search)
136
+ }));
137
+ };
138
+ window.addEventListener("popstate", handlePopState);
139
+ return () => window.removeEventListener("popstate", handlePopState);
140
+ }, [basePath]);
141
+ useEffect(() => {
142
+ return subscribe((ctx) => {
143
+ setRouterState((prev) => ({
144
+ ...prev,
145
+ path: ctx.path,
146
+ searchParams: ctx.searchParams
147
+ }));
148
+ });
149
+ }, []);
150
+ return React.createElement(
151
+ ReactRouterContext.Provider,
152
+ { value: routerState },
153
+ children
154
+ );
155
+ };
156
+ useRouter = function useFlightRouter() {
157
+ return useContext(ReactRouterContext);
158
+ };
159
+ }
160
+ } catch {
161
+ }
162
+ }
163
+
164
+ // src/prefetch.ts
165
+ var isBrowser2 = typeof window !== "undefined";
166
+ var supportsIntersectionObserver = isBrowser2 && "IntersectionObserver" in window;
167
+ var prefetchedUrls = /* @__PURE__ */ new Set();
168
+ var prefetchingUrls = /* @__PURE__ */ new Set();
169
+ var viewportObservers = /* @__PURE__ */ new Map();
170
+ function prefetch(href, options = {}) {
171
+ if (!isBrowser2) return;
172
+ const {
173
+ priority = "auto",
174
+ includeModules = true,
175
+ includeData = false
176
+ } = options;
177
+ const url = normalizeUrl(href);
178
+ if (prefetchedUrls.has(url) || prefetchingUrls.has(url)) {
179
+ return;
180
+ }
181
+ prefetchingUrls.add(url);
182
+ createPrefetchLink(url, "document", priority);
183
+ if (includeModules) {
184
+ prefetchModules(url, priority);
185
+ }
186
+ if (includeData) {
187
+ prefetchData(url, priority);
188
+ }
189
+ prefetchedUrls.add(url);
190
+ prefetchingUrls.delete(url);
191
+ }
192
+ function prefetchAll(hrefs, options = {}) {
193
+ for (const href of hrefs) {
194
+ prefetch(href, options);
195
+ }
196
+ }
197
+ function isPrefetched(href) {
198
+ return prefetchedUrls.has(normalizeUrl(href));
199
+ }
200
+ function clearPrefetchCache() {
201
+ prefetchedUrls.clear();
202
+ prefetchingUrls.clear();
203
+ }
204
+ function createPrefetchLink(href, as, priority) {
205
+ if (!isBrowser2) return null;
206
+ const existing = document.querySelector(
207
+ `link[rel="prefetch"][href="${href}"], link[rel="modulepreload"][href="${href}"]`
208
+ );
209
+ if (existing) return existing;
210
+ const link = document.createElement("link");
211
+ if (as === "script") {
212
+ link.rel = "modulepreload";
213
+ } else {
214
+ link.rel = "prefetch";
215
+ link.as = as;
216
+ }
217
+ link.href = href;
218
+ if (priority !== "auto" && "fetchPriority" in link) {
219
+ link.fetchPriority = priority;
220
+ }
221
+ if (priority === "low" && "requestIdleCallback" in window) {
222
+ window.requestIdleCallback(() => {
223
+ document.head.appendChild(link);
224
+ });
225
+ } else {
226
+ document.head.appendChild(link);
227
+ }
228
+ return link;
229
+ }
230
+ function prefetchModules(href, priority) {
231
+ const manifest = window.__FLIGHT_MANIFEST__;
232
+ if (!manifest?.routes) return;
233
+ const routeModules = manifest.routes[href];
234
+ if (!routeModules) return;
235
+ for (const module of routeModules) {
236
+ createPrefetchLink(module, "script", priority);
237
+ }
238
+ }
239
+ function prefetchData(href, priority) {
240
+ const dataUrl = `/_flight/data${href === "/" ? "/index" : href}.json`;
241
+ createPrefetchLink(dataUrl, "fetch", priority);
242
+ }
243
+ var sharedObserver = null;
244
+ var observerCallbacks = /* @__PURE__ */ new Map();
245
+ function getViewportObserver() {
246
+ if (!supportsIntersectionObserver) return null;
247
+ if (!sharedObserver) {
248
+ sharedObserver = new IntersectionObserver(
249
+ (entries) => {
250
+ for (const entry of entries) {
251
+ if (entry.isIntersecting) {
252
+ const callback = observerCallbacks.get(entry.target);
253
+ if (callback) {
254
+ callback();
255
+ sharedObserver?.unobserve(entry.target);
256
+ observerCallbacks.delete(entry.target);
257
+ }
258
+ }
259
+ }
260
+ },
261
+ {
262
+ // Start prefetching when link is 25% visible or within 100px of viewport
263
+ rootMargin: "100px",
264
+ threshold: 0.25
265
+ }
266
+ );
267
+ }
268
+ return sharedObserver;
269
+ }
270
+ function observeForPrefetch(element, href) {
271
+ if (!supportsIntersectionObserver) {
272
+ return () => {
273
+ };
274
+ }
275
+ const observer = getViewportObserver();
276
+ if (!observer) return () => {
277
+ };
278
+ const callback = () => {
279
+ prefetch(href, { priority: "low" });
280
+ };
281
+ observerCallbacks.set(element, callback);
282
+ observer.observe(element);
283
+ const cleanup = () => {
284
+ observer.unobserve(element);
285
+ observerCallbacks.delete(element);
286
+ viewportObservers.delete(element);
287
+ };
288
+ viewportObservers.set(element, cleanup);
289
+ return cleanup;
290
+ }
291
+ function setupIntentPrefetch(element, href) {
292
+ if (!isBrowser2) return () => {
293
+ };
294
+ let prefetchTriggered = false;
295
+ const handleIntent = () => {
296
+ if (!prefetchTriggered) {
297
+ prefetchTriggered = true;
298
+ prefetch(href, { priority: "auto" });
299
+ }
300
+ };
301
+ element.addEventListener("mouseenter", handleIntent, { passive: true });
302
+ element.addEventListener("focus", handleIntent, { passive: true });
303
+ element.addEventListener("touchstart", handleIntent, { passive: true });
304
+ return () => {
305
+ element.removeEventListener("mouseenter", handleIntent);
306
+ element.removeEventListener("focus", handleIntent);
307
+ element.removeEventListener("touchstart", handleIntent);
308
+ };
309
+ }
310
+ function normalizeUrl(href) {
311
+ if (isBrowser2 && !href.startsWith("http")) {
312
+ try {
313
+ const url = new URL(href, window.location.origin);
314
+ return url.pathname + url.search;
315
+ } catch {
316
+ return href;
317
+ }
318
+ }
319
+ return href;
320
+ }
23
321
 
24
322
  // src/link.ts
25
- var isBrowser = typeof window !== "undefined";
323
+ var isBrowser3 = typeof window !== "undefined";
26
324
  function handleLinkClick(href, options, event) {
27
325
  if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
28
326
  return;
@@ -84,7 +382,7 @@ if (typeof globalThis !== "undefined") {
84
382
  [href, isExternal, target, replace, scroll, onClick]
85
383
  );
86
384
  useEffect(() => {
87
- if (isExternal || !isBrowser || prefetchStrategy === "none") {
385
+ if (isExternal || !isBrowser3 || prefetchStrategy === "none") {
88
386
  return;
89
387
  }
90
388
  const link = linkRef.current;
@@ -185,21 +483,80 @@ function useLinkProps(href, options = {}) {
185
483
  result.onMouseenter = doPrefetch;
186
484
  result.onFocus = doPrefetch;
187
485
  }
188
- if (prefetchStrategy === "render" && !isExternal && isBrowser) {
486
+ if (prefetchStrategy === "render" && !isExternal && isBrowser3) {
189
487
  prefetch(href, { priority: "low" });
190
488
  }
191
489
  return result;
192
490
  }
193
491
 
492
+ // src/prefetch-links.ts
493
+ var isBrowser4 = typeof window !== "undefined";
494
+ var PrefetchPageLinks = null;
495
+ if (typeof globalThis !== "undefined") {
496
+ try {
497
+ const React = globalThis.React;
498
+ if (React?.createElement && "useEffect" in React) {
499
+ const { useEffect, useState } = React;
500
+ PrefetchPageLinks = function FlightPrefetchPageLinks({
501
+ page,
502
+ options = {}
503
+ }) {
504
+ const [shouldRender, setShouldRender] = useState(false);
505
+ useEffect(() => {
506
+ if (!isBrowser4) return;
507
+ if (isPrefetched(page)) {
508
+ return;
509
+ }
510
+ prefetch(page, {
511
+ priority: "low",
512
+ includeModules: true,
513
+ ...options
514
+ });
515
+ setShouldRender(false);
516
+ }, [page, options]);
517
+ return null;
518
+ };
519
+ }
520
+ } catch {
521
+ }
522
+ }
523
+ function prefetchPages(pages, options = {}) {
524
+ if (!isBrowser4) return;
525
+ for (const page of pages) {
526
+ if (!isPrefetched(page)) {
527
+ prefetch(page, {
528
+ priority: "low",
529
+ ...options
530
+ });
531
+ }
532
+ }
533
+ }
534
+ function prefetchWhenIdle(page, options = {}) {
535
+ if (!isBrowser4) return;
536
+ const doPrefetch = () => {
537
+ if (!isPrefetched(page)) {
538
+ prefetch(page, {
539
+ priority: "low",
540
+ ...options
541
+ });
542
+ }
543
+ };
544
+ if ("requestIdleCallback" in window) {
545
+ window.requestIdleCallback(doPrefetch, { timeout: 3e3 });
546
+ } else {
547
+ setTimeout(doPrefetch, 100);
548
+ }
549
+ }
550
+
194
551
  // src/hooks.ts
195
- var isBrowser2 = typeof window !== "undefined";
552
+ var isBrowser5 = typeof window !== "undefined";
196
553
  var pathSubscribers = /* @__PURE__ */ new Set();
197
554
  function notifyPathChange() {
198
555
  pathSubscribers.forEach((fn) => fn());
199
556
  }
200
557
  var historyIntercepted = false;
201
558
  function interceptHistory() {
202
- if (!isBrowser2 || historyIntercepted) return;
559
+ if (!isBrowser5 || historyIntercepted) return;
203
560
  historyIntercepted = true;
204
561
  const originalPushState = history.pushState.bind(history);
205
562
  const originalReplaceState = history.replaceState.bind(history);
@@ -213,7 +570,7 @@ function interceptHistory() {
213
570
  };
214
571
  window.addEventListener("popstate", notifyPathChange);
215
572
  }
216
- if (isBrowser2) {
573
+ if (isBrowser5) {
217
574
  interceptHistory();
218
575
  }
219
576
  function subscribeToPathname(callback) {
@@ -221,7 +578,7 @@ function subscribeToPathname(callback) {
221
578
  return () => pathSubscribers.delete(callback);
222
579
  }
223
580
  function getPathnameSnapshot() {
224
- return isBrowser2 ? window.location.pathname : "/";
581
+ return isBrowser5 ? window.location.pathname : "/";
225
582
  }
226
583
  function getPathnameServerSnapshot() {
227
584
  return "/";
@@ -239,7 +596,7 @@ var useParams = () => ({});
239
596
  var useSearchParams = () => [new URLSearchParams(), () => {
240
597
  }];
241
598
  var usePathname = () => {
242
- if (isBrowser2) {
599
+ if (isBrowser5) {
243
600
  return window.location.pathname;
244
601
  }
245
602
  return "/";
@@ -264,10 +621,10 @@ if (typeof globalThis !== "undefined") {
264
621
  };
265
622
  useSearchParams = function useFlightSearchParams() {
266
623
  const [searchParams, setSearchParamsState] = useState(
267
- () => isBrowser2 ? new URLSearchParams(window.location.search) : new URLSearchParams()
624
+ () => isBrowser5 ? new URLSearchParams(window.location.search) : new URLSearchParams()
268
625
  );
269
626
  useEffect(() => {
270
- if (!isBrowser2) return;
627
+ if (!isBrowser5) return;
271
628
  const handleChange = () => {
272
629
  setSearchParamsState(new URLSearchParams(window.location.search));
273
630
  };
@@ -275,7 +632,7 @@ if (typeof globalThis !== "undefined") {
275
632
  return () => window.removeEventListener("popstate", handleChange);
276
633
  }, []);
277
634
  const setSearchParams = useCallback((newParams) => {
278
- if (!isBrowser2) return;
635
+ if (!isBrowser5) return;
279
636
  let params;
280
637
  if (newParams instanceof URLSearchParams) {
281
638
  params = newParams;
@@ -299,6 +656,81 @@ if (typeof globalThis !== "undefined") {
299
656
  } catch {
300
657
  }
301
658
  }
659
+
660
+ // src/navigate.ts
661
+ var isBrowser6 = typeof window !== "undefined";
662
+ function navigate(to, options = {}) {
663
+ const { navigate: routerNavigate } = getRouterContext();
664
+ routerNavigate(to, options);
665
+ }
666
+ function patternToRegex(pattern) {
667
+ const paramNames = [];
668
+ let regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\[\.\.\.(\w+)\\\]/g, (_, name) => {
669
+ paramNames.push(name);
670
+ return "(.+)";
671
+ }).replace(/\\\[(\w+)\\\]/g, (_, name) => {
672
+ paramNames.push(name);
673
+ return "([^/]+)";
674
+ }).replace(/:(\w+)/g, (_, name) => {
675
+ paramNames.push(name);
676
+ return "([^/]+)";
677
+ });
678
+ regexStr = `^${regexStr}$`;
679
+ return {
680
+ regex: new RegExp(regexStr),
681
+ paramNames
682
+ };
683
+ }
684
+ function matchRoute(pathname, pattern) {
685
+ const { regex, paramNames } = patternToRegex(pattern);
686
+ const match = pathname.match(regex);
687
+ if (!match) {
688
+ return { matched: false, params: {} };
689
+ }
690
+ const params = {};
691
+ paramNames.forEach((name, index) => {
692
+ params[name] = match[index + 1] || "";
693
+ });
694
+ return { matched: true, params };
695
+ }
696
+ function parseParams(pathname, pattern) {
697
+ const { params } = matchRoute(pathname, pattern);
698
+ return params;
699
+ }
700
+ function findRoute(pathname, routes) {
701
+ for (const route of routes) {
702
+ const { matched, params } = matchRoute(pathname, route.path);
703
+ if (matched) {
704
+ return {
705
+ route,
706
+ params,
707
+ pathname
708
+ };
709
+ }
710
+ }
711
+ return null;
712
+ }
713
+ function generatePath(pattern, params = {}) {
714
+ let path = pattern;
715
+ path = path.replace(/\[(\w+)\]/g, (_, name) => {
716
+ return params[name] || "";
717
+ });
718
+ path = path.replace(/:(\w+)/g, (_, name) => {
719
+ return params[name] || "";
720
+ });
721
+ return path;
722
+ }
723
+ function isActive(pattern) {
724
+ const { path } = getRouterContext();
725
+ const { matched } = matchRoute(path, pattern);
726
+ return matched;
727
+ }
728
+ function redirect(url) {
729
+ if (isBrowser6) {
730
+ window.location.href = url;
731
+ }
732
+ throw new Error(`Redirect to: ${url}`);
733
+ }
302
734
  export {
303
735
  Link,
304
736
  PrefetchPageLinks,