@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/vue/index.js CHANGED
@@ -1,27 +1,461 @@
1
- import {
2
- clearPrefetchCache,
3
- findRoute,
4
- generatePath,
5
- getRouterContext,
6
- initRouter,
7
- isActive,
8
- isPrefetched,
9
- matchRoute,
10
- navigate,
11
- observeForPrefetch,
12
- parseParams,
13
- prefetch,
14
- prefetchAll,
15
- prefetchPages,
16
- prefetchWhenIdle,
17
- redirect,
18
- setupIntentPrefetch,
19
- subscribe
20
- } 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/navigate.ts
165
+ var isBrowser2 = typeof window !== "undefined";
166
+ function navigate(to, options = {}) {
167
+ const { navigate: routerNavigate } = getRouterContext();
168
+ routerNavigate(to, options);
169
+ }
170
+ function patternToRegex(pattern) {
171
+ const paramNames = [];
172
+ let regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\[\.\.\.(\w+)\\\]/g, (_, name) => {
173
+ paramNames.push(name);
174
+ return "(.+)";
175
+ }).replace(/\\\[(\w+)\\\]/g, (_, name) => {
176
+ paramNames.push(name);
177
+ return "([^/]+)";
178
+ }).replace(/:(\w+)/g, (_, name) => {
179
+ paramNames.push(name);
180
+ return "([^/]+)";
181
+ });
182
+ regexStr = `^${regexStr}$`;
183
+ return {
184
+ regex: new RegExp(regexStr),
185
+ paramNames
186
+ };
187
+ }
188
+ function matchRoute(pathname, pattern) {
189
+ const { regex, paramNames } = patternToRegex(pattern);
190
+ const match = pathname.match(regex);
191
+ if (!match) {
192
+ return { matched: false, params: {} };
193
+ }
194
+ const params = {};
195
+ paramNames.forEach((name, index) => {
196
+ params[name] = match[index + 1] || "";
197
+ });
198
+ return { matched: true, params };
199
+ }
200
+ function parseParams(pathname, pattern) {
201
+ const { params } = matchRoute(pathname, pattern);
202
+ return params;
203
+ }
204
+ function findRoute(pathname, routes) {
205
+ for (const route of routes) {
206
+ const { matched, params } = matchRoute(pathname, route.path);
207
+ if (matched) {
208
+ return {
209
+ route,
210
+ params,
211
+ pathname
212
+ };
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ function generatePath(pattern, params = {}) {
218
+ let path = pattern;
219
+ path = path.replace(/\[(\w+)\]/g, (_, name) => {
220
+ return params[name] || "";
221
+ });
222
+ path = path.replace(/:(\w+)/g, (_, name) => {
223
+ return params[name] || "";
224
+ });
225
+ return path;
226
+ }
227
+ function isActive(pattern) {
228
+ const { path } = getRouterContext();
229
+ const { matched } = matchRoute(path, pattern);
230
+ return matched;
231
+ }
232
+ function redirect(url) {
233
+ if (isBrowser2) {
234
+ window.location.href = url;
235
+ }
236
+ throw new Error(`Redirect to: ${url}`);
237
+ }
238
+
239
+ // src/prefetch.ts
240
+ var isBrowser3 = typeof window !== "undefined";
241
+ var supportsIntersectionObserver = isBrowser3 && "IntersectionObserver" in window;
242
+ var prefetchedUrls = /* @__PURE__ */ new Set();
243
+ var prefetchingUrls = /* @__PURE__ */ new Set();
244
+ var viewportObservers = /* @__PURE__ */ new Map();
245
+ function prefetch(href, options = {}) {
246
+ if (!isBrowser3) return;
247
+ const {
248
+ priority = "auto",
249
+ includeModules = true,
250
+ includeData = false
251
+ } = options;
252
+ const url = normalizeUrl(href);
253
+ if (prefetchedUrls.has(url) || prefetchingUrls.has(url)) {
254
+ return;
255
+ }
256
+ prefetchingUrls.add(url);
257
+ createPrefetchLink(url, "document", priority);
258
+ if (includeModules) {
259
+ prefetchModules(url, priority);
260
+ }
261
+ if (includeData) {
262
+ prefetchData(url, priority);
263
+ }
264
+ prefetchedUrls.add(url);
265
+ prefetchingUrls.delete(url);
266
+ }
267
+ function prefetchAll(hrefs, options = {}) {
268
+ for (const href of hrefs) {
269
+ prefetch(href, options);
270
+ }
271
+ }
272
+ function isPrefetched(href) {
273
+ return prefetchedUrls.has(normalizeUrl(href));
274
+ }
275
+ function clearPrefetchCache() {
276
+ prefetchedUrls.clear();
277
+ prefetchingUrls.clear();
278
+ }
279
+ function createPrefetchLink(href, as, priority) {
280
+ if (!isBrowser3) return null;
281
+ const existing = document.querySelector(
282
+ `link[rel="prefetch"][href="${href}"], link[rel="modulepreload"][href="${href}"]`
283
+ );
284
+ if (existing) return existing;
285
+ const link = document.createElement("link");
286
+ if (as === "script") {
287
+ link.rel = "modulepreload";
288
+ } else {
289
+ link.rel = "prefetch";
290
+ link.as = as;
291
+ }
292
+ link.href = href;
293
+ if (priority !== "auto" && "fetchPriority" in link) {
294
+ link.fetchPriority = priority;
295
+ }
296
+ if (priority === "low" && "requestIdleCallback" in window) {
297
+ window.requestIdleCallback(() => {
298
+ document.head.appendChild(link);
299
+ });
300
+ } else {
301
+ document.head.appendChild(link);
302
+ }
303
+ return link;
304
+ }
305
+ function prefetchModules(href, priority) {
306
+ const manifest = window.__FLIGHT_MANIFEST__;
307
+ if (!manifest?.routes) return;
308
+ const routeModules = manifest.routes[href];
309
+ if (!routeModules) return;
310
+ for (const module of routeModules) {
311
+ createPrefetchLink(module, "script", priority);
312
+ }
313
+ }
314
+ function prefetchData(href, priority) {
315
+ const dataUrl = `/_flight/data${href === "/" ? "/index" : href}.json`;
316
+ createPrefetchLink(dataUrl, "fetch", priority);
317
+ }
318
+ var sharedObserver = null;
319
+ var observerCallbacks = /* @__PURE__ */ new Map();
320
+ function getViewportObserver() {
321
+ if (!supportsIntersectionObserver) return null;
322
+ if (!sharedObserver) {
323
+ sharedObserver = new IntersectionObserver(
324
+ (entries) => {
325
+ for (const entry of entries) {
326
+ if (entry.isIntersecting) {
327
+ const callback = observerCallbacks.get(entry.target);
328
+ if (callback) {
329
+ callback();
330
+ sharedObserver?.unobserve(entry.target);
331
+ observerCallbacks.delete(entry.target);
332
+ }
333
+ }
334
+ }
335
+ },
336
+ {
337
+ // Start prefetching when link is 25% visible or within 100px of viewport
338
+ rootMargin: "100px",
339
+ threshold: 0.25
340
+ }
341
+ );
342
+ }
343
+ return sharedObserver;
344
+ }
345
+ function observeForPrefetch(element, href) {
346
+ if (!supportsIntersectionObserver) {
347
+ return () => {
348
+ };
349
+ }
350
+ const observer = getViewportObserver();
351
+ if (!observer) return () => {
352
+ };
353
+ const callback = () => {
354
+ prefetch(href, { priority: "low" });
355
+ };
356
+ observerCallbacks.set(element, callback);
357
+ observer.observe(element);
358
+ const cleanup = () => {
359
+ observer.unobserve(element);
360
+ observerCallbacks.delete(element);
361
+ viewportObservers.delete(element);
362
+ };
363
+ viewportObservers.set(element, cleanup);
364
+ return cleanup;
365
+ }
366
+ function setupIntentPrefetch(element, href) {
367
+ if (!isBrowser3) return () => {
368
+ };
369
+ let prefetchTriggered = false;
370
+ const handleIntent = () => {
371
+ if (!prefetchTriggered) {
372
+ prefetchTriggered = true;
373
+ prefetch(href, { priority: "auto" });
374
+ }
375
+ };
376
+ element.addEventListener("mouseenter", handleIntent, { passive: true });
377
+ element.addEventListener("focus", handleIntent, { passive: true });
378
+ element.addEventListener("touchstart", handleIntent, { passive: true });
379
+ return () => {
380
+ element.removeEventListener("mouseenter", handleIntent);
381
+ element.removeEventListener("focus", handleIntent);
382
+ element.removeEventListener("touchstart", handleIntent);
383
+ };
384
+ }
385
+ function normalizeUrl(href) {
386
+ if (isBrowser3 && !href.startsWith("http")) {
387
+ try {
388
+ const url = new URL(href, window.location.origin);
389
+ return url.pathname + url.search;
390
+ } catch {
391
+ return href;
392
+ }
393
+ }
394
+ return href;
395
+ }
396
+
397
+ // src/prefetch-links.ts
398
+ var isBrowser4 = typeof window !== "undefined";
399
+ var PrefetchPageLinks = null;
400
+ if (typeof globalThis !== "undefined") {
401
+ try {
402
+ const React = globalThis.React;
403
+ if (React?.createElement && "useEffect" in React) {
404
+ const { useEffect, useState } = React;
405
+ PrefetchPageLinks = function FlightPrefetchPageLinks({
406
+ page,
407
+ options = {}
408
+ }) {
409
+ const [shouldRender, setShouldRender] = useState(false);
410
+ useEffect(() => {
411
+ if (!isBrowser4) return;
412
+ if (isPrefetched(page)) {
413
+ return;
414
+ }
415
+ prefetch(page, {
416
+ priority: "low",
417
+ includeModules: true,
418
+ ...options
419
+ });
420
+ setShouldRender(false);
421
+ }, [page, options]);
422
+ return null;
423
+ };
424
+ }
425
+ } catch {
426
+ }
427
+ }
428
+ function prefetchPages(pages, options = {}) {
429
+ if (!isBrowser4) return;
430
+ for (const page of pages) {
431
+ if (!isPrefetched(page)) {
432
+ prefetch(page, {
433
+ priority: "low",
434
+ ...options
435
+ });
436
+ }
437
+ }
438
+ }
439
+ function prefetchWhenIdle(page, options = {}) {
440
+ if (!isBrowser4) return;
441
+ const doPrefetch = () => {
442
+ if (!isPrefetched(page)) {
443
+ prefetch(page, {
444
+ priority: "low",
445
+ ...options
446
+ });
447
+ }
448
+ };
449
+ if ("requestIdleCallback" in window) {
450
+ window.requestIdleCallback(doPrefetch, { timeout: 3e3 });
451
+ } else {
452
+ setTimeout(doPrefetch, 100);
453
+ }
454
+ }
21
455
 
22
456
  // src/vue/Link.ts
23
457
  import { defineComponent, ref, onMounted, onUnmounted, h } from "vue";
24
- var isBrowser = typeof window !== "undefined";
458
+ var isBrowser5 = typeof window !== "undefined";
25
459
  function isExternalUrl(href) {
26
460
  if (!href) return false;
27
461
  return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("javascript:") || href.startsWith("#");
@@ -69,7 +503,7 @@ var Link = defineComponent({
69
503
  const prefetchStrategy = normalizePrefetchStrategy(props.prefetch);
70
504
  let cleanup;
71
505
  onMounted(() => {
72
- if (isExternal || !isBrowser || prefetchStrategy === "none") {
506
+ if (isExternal || !isBrowser5 || prefetchStrategy === "none") {
73
507
  return;
74
508
  }
75
509
  const link = linkRef.value;
@@ -118,10 +552,10 @@ var Link = defineComponent({
118
552
 
119
553
  // src/vue/composables.ts
120
554
  import { ref as ref2, provide, inject, onMounted as onMounted2, onUnmounted as onUnmounted2 } from "vue";
121
- var isBrowser2 = typeof window !== "undefined";
555
+ var isBrowser6 = typeof window !== "undefined";
122
556
  var RouterKey = /* @__PURE__ */ Symbol("FlightRouter");
123
- function navigateTo(to, options = {}) {
124
- if (!isBrowser2) return;
557
+ function navigateTo2(to, options = {}) {
558
+ if (!isBrowser6) return;
125
559
  const { replace = false, scroll = true, state } = options;
126
560
  if (replace) {
127
561
  window.history.replaceState(state ?? null, "", to);
@@ -132,18 +566,18 @@ function navigateTo(to, options = {}) {
132
566
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
133
567
  }
134
568
  }
135
- function RouterProvider(props, { slots }) {
136
- const path = ref2(isBrowser2 ? window.location.pathname : props.initialPath || "/");
137
- const searchParams = ref2(isBrowser2 ? new URLSearchParams(window.location.search) : new URLSearchParams());
569
+ function RouterProvider2(props, { slots }) {
570
+ const path = ref2(isBrowser6 ? window.location.pathname : props.initialPath || "/");
571
+ const searchParams = ref2(isBrowser6 ? new URLSearchParams(window.location.search) : new URLSearchParams());
138
572
  const routerState = ref2({
139
573
  path: path.value,
140
574
  searchParams: searchParams.value,
141
- navigate: navigateTo,
142
- back: () => isBrowser2 && window.history.back(),
143
- forward: () => isBrowser2 && window.history.forward()
575
+ navigate: navigateTo2,
576
+ back: () => isBrowser6 && window.history.back(),
577
+ forward: () => isBrowser6 && window.history.forward()
144
578
  });
145
579
  provide(RouterKey, routerState);
146
- if (isBrowser2) {
580
+ if (isBrowser6) {
147
581
  onMounted2(() => {
148
582
  const handlePopState = () => {
149
583
  path.value = window.location.pathname;
@@ -172,7 +606,7 @@ function RouterProvider(props, { slots }) {
172
606
  }
173
607
  return slots.default?.();
174
608
  }
175
- function useRouter() {
609
+ function useRouter2() {
176
610
  const routerState = inject(RouterKey);
177
611
  if (routerState) {
178
612
  return routerState.value;
@@ -184,21 +618,21 @@ function usePathname() {
184
618
  if (routerState) {
185
619
  return ref2(routerState.value.path);
186
620
  }
187
- return ref2(isBrowser2 ? window.location.pathname : "/");
621
+ return ref2(isBrowser6 ? window.location.pathname : "/");
188
622
  }
189
623
  function useSearchParams() {
190
624
  const routerState = inject(RouterKey);
191
625
  if (routerState) {
192
626
  return ref2(routerState.value.searchParams);
193
627
  }
194
- return ref2(isBrowser2 ? new URLSearchParams(window.location.search) : new URLSearchParams());
628
+ return ref2(isBrowser6 ? new URLSearchParams(window.location.search) : new URLSearchParams());
195
629
  }
196
630
  function useParams() {
197
631
  return ref2({});
198
632
  }
199
633
  export {
200
634
  Link,
201
- RouterProvider,
635
+ RouterProvider2 as RouterProvider,
202
636
  clearPrefetchCache,
203
637
  findRoute,
204
638
  generatePath,
@@ -219,6 +653,6 @@ export {
219
653
  subscribe,
220
654
  useParams,
221
655
  usePathname,
222
- useRouter,
656
+ useRouter2 as useRouter,
223
657
  useSearchParams
224
658
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flight-framework/router",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Agnostic client-side routing primitives for Flight Framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,11 +33,12 @@
33
33
  }
34
34
  },
35
35
  "files": [
36
- "dist"
36
+ "dist",
37
+ "README.md"
37
38
  ],
38
39
  "scripts": {
39
- "build": "tsup src/index.ts src/react/index.ts src/vue/index.ts src/svelte/index.ts src/solid/index.ts src/preact/index.ts --format esm --dts --clean",
40
- "dev": "tsup src/index.ts src/react/index.ts src/vue/index.ts src/svelte/index.ts src/solid/index.ts src/preact/index.ts --format esm --dts --watch"
40
+ "build": "tsup",
41
+ "dev": "tsup --watch"
41
42
  },
42
43
  "keywords": [
43
44
  "flight",