@mpen/rerouter 0.3.0 → 0.3.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.
Files changed (66) hide show
  1. package/README.md +4 -0
  2. package/{src → cli}/bin.test.ts +24 -2
  3. package/{src → cli}/bin.ts +27 -18
  4. package/cli/tsconfig.json +9 -0
  5. package/dist/acorn-k7ED_tOl.js +4968 -0
  6. package/dist/angular--Iqdw9UJ.js +4057 -0
  7. package/dist/babel-hfWAujRY.js +9878 -0
  8. package/dist/bin.d.ts +1 -1
  9. package/dist/bin.js +28 -23
  10. package/dist/estree-C1Zjnvlw.js +7266 -0
  11. package/dist/flow-BaD9LyIP.js +52912 -0
  12. package/dist/glimmer-CvCjW_1V.js +7541 -0
  13. package/dist/graphql-BdtzBuWh.js +1945 -0
  14. package/dist/html-DkZtUVbo.js +7137 -0
  15. package/dist/index.d.ts +19 -6
  16. package/dist/index.js +135 -27
  17. package/dist/markdown-Z8Vrc69e.js +6876 -0
  18. package/dist/meriyah-DeO4stuH.js +7590 -0
  19. package/dist/postcss-BmgGJ0E5.js +6777 -0
  20. package/dist/prettier-BT_F8kIx.js +15629 -0
  21. package/dist/typescript-DtIxStjy.js +22936 -0
  22. package/dist/yaml-CWOPBY0q.js +5281 -0
  23. package/examples/App.tsx +18 -49
  24. package/examples/dist/BlogPost-c10d9w2p.js +1 -0
  25. package/examples/dist/FetchLoading-534mdrgz.js +1 -0
  26. package/examples/dist/FetchLoading-sbxbdkre.js +1 -0
  27. package/examples/dist/Home-a1258p25.js +1 -0
  28. package/examples/dist/KitchenSink-821mjg0h.js +1 -0
  29. package/examples/dist/Login-wywx6bp7.js +1 -0
  30. package/examples/dist/Match-1e72jm5w.js +1 -0
  31. package/examples/dist/NotFound-smxj24jw.js +1 -0
  32. package/examples/dist/SlowLoading-59xxmbfk.js +1 -0
  33. package/examples/dist/index-0d4kj0rv.js +2 -0
  34. package/examples/dist/index-3x197sbt.js +9 -0
  35. package/examples/dist/index-a2hkfx1n.js +9 -0
  36. package/examples/dist/index-d21me1mc.js +9 -0
  37. package/examples/dist/index-ktqdknsn.js +2 -0
  38. package/examples/dist/index-p53qxxzd.js +2 -0
  39. package/examples/dist/index.html +67 -0
  40. package/examples/routes.gen.ts +66 -86
  41. package/examples/routes.ts +2 -2
  42. package/examples/server/serve-dist.ts +33 -0
  43. package/examples/server/tsconfig.json +9 -0
  44. package/package.json +11 -6
  45. package/src/components/Link.tsx +8 -6
  46. package/src/components/Router.test.tsx +183 -0
  47. package/src/components/Router.tsx +161 -29
  48. package/src/lib/routes.ts +2 -0
  49. package/tsconfig.json +3 -2
  50. package/tsdown.config.ts +3 -4
  51. package/dist/hooks-Dlwcb0sV.js +0 -20
  52. package/dist/hooks.d.ts +0 -2
  53. package/dist/hooks.js +0 -2
  54. package/dist/index-BYXpNitc.d.ts +0 -5
  55. /package/{src → cli}/fixtures/bin/kitchen-sink.tsx +0 -0
  56. /package/{src → cli}/fixtures/bin/optional.tsx +0 -0
  57. /package/{src → cli}/fixtures/bin/pages/Home.tsx +0 -0
  58. /package/{src → cli}/fixtures/bin/pages/KitchenSink.tsx +0 -0
  59. /package/{src → cli}/fixtures/bin/pages/Login.tsx +0 -0
  60. /package/{src → cli}/fixtures/bin/pages/Match.tsx +0 -0
  61. /package/{src → cli}/fixtures/bin/pages/NotFound.tsx +0 -0
  62. /package/{src → cli}/fixtures/bin/pages/Optional.tsx +0 -0
  63. /package/{src → cli}/fixtures/bin/regexp-groups.tsx +0 -0
  64. /package/{src → cli}/fixtures/bin/simple.tsx +0 -0
  65. /package/{src → cli}/fixtures/bin/unnamed.tsx +0 -0
  66. /package/dist/{routes-Hpf6cwcZ.js → routes-PW-bNm8e.js} +0 -0
@@ -1,18 +1,141 @@
1
- import { lazy, Suspense, useMemo, type ReactNode } from 'react'
1
+ import { startTransition, Suspense, useEffect, useMemo, useState, type ReactNode } from 'react'
2
2
  import { useUrlPath } from '../hooks'
3
- import { normalizeRoutes, type Route } from '../lib/routes'
3
+ import {
4
+ normalizeRoutes,
5
+ type NormalizedRoute,
6
+ type Route,
7
+ type RouteComponent,
8
+ } from '../lib/routes'
4
9
 
5
- type LazyRouteComponent = ReturnType<typeof lazy>
10
+ const DEFAULT_LOADING_DELAY_MS = 400
11
+ const loadedRouteComponents = new WeakMap<Route['component'], RouteComponent<any>>()
12
+ const loadingRouteComponents = new WeakMap<Route['component'], Promise<RouteComponent<any>>>()
6
13
 
7
- const lazyRouteComponents = new WeakMap<Route['component'], LazyRouteComponent>()
14
+ type RouteMatch = {
15
+ route: NormalizedRoute
16
+ params: Record<string, string | undefined>
17
+ pathname: string
18
+ }
19
+
20
+ type RenderedRoute = RouteMatch & {
21
+ Component: RouteComponent<any>
22
+ }
23
+
24
+ function loadRouteComponent(component: Route['component']): Promise<RouteComponent<any>> {
25
+ const loaded = loadedRouteComponents.get(component)
26
+ if (loaded) return Promise.resolve(loaded)
27
+
28
+ let loading = loadingRouteComponents.get(component)
29
+ if (!loading) {
30
+ loading = component().then((module) => {
31
+ loadedRouteComponents.set(component, module.default)
32
+ loadingRouteComponents.delete(component)
33
+ return module.default
34
+ })
35
+ loadingRouteComponents.set(component, loading)
36
+ }
37
+
38
+ return loading
39
+ }
8
40
 
9
- function getLazyRouteComponent(component: Route['component']): LazyRouteComponent {
10
- let LazyComponent = lazyRouteComponents.get(component)
11
- if (!LazyComponent) {
12
- LazyComponent = lazy(component)
13
- lazyRouteComponents.set(component, LazyComponent)
41
+ function findRouteMatch(routes: readonly NormalizedRoute[], pathname: string): RouteMatch | null {
42
+ for (const route of routes) {
43
+ const params = route.matches(pathname)
44
+ if (!params) continue
45
+ return { route, params, pathname }
14
46
  }
15
- return LazyComponent
47
+
48
+ return null
49
+ }
50
+
51
+ function getLoadedRoute(match: RouteMatch | null): RenderedRoute | null {
52
+ if (!match) return null
53
+
54
+ const Component = loadedRouteComponents.get(match.route.component)
55
+ if (!Component) return null
56
+
57
+ return {
58
+ ...match,
59
+ Component,
60
+ }
61
+ }
62
+
63
+ function scheduleTransition(cb: () => void): void {
64
+ queueMicrotask(() => {
65
+ startTransition(cb)
66
+ })
67
+ }
68
+
69
+ function useRenderedRoute(
70
+ match: RouteMatch | null,
71
+ loadingDelayMs: number,
72
+ ): { renderedRoute: RenderedRoute | null; showLoading: boolean } {
73
+ const [renderedRoute, setRenderedRoute] = useState(() => getLoadedRoute(match))
74
+ const [showLoading, setShowLoading] = useState(false)
75
+ const [loadError, setLoadError] = useState<unknown>(null)
76
+
77
+ useEffect(() => {
78
+ if (!match) {
79
+ scheduleTransition(() => {
80
+ setRenderedRoute(null)
81
+ setShowLoading(false)
82
+ setLoadError(null)
83
+ })
84
+ return
85
+ }
86
+
87
+ const loaded = getLoadedRoute(match)
88
+ if (loaded) {
89
+ scheduleTransition(() => {
90
+ setRenderedRoute(loaded)
91
+ setShowLoading(false)
92
+ setLoadError(null)
93
+ })
94
+ return
95
+ }
96
+
97
+ let active = true
98
+ let timeout: ReturnType<typeof setTimeout> | undefined
99
+
100
+ scheduleTransition(() => {
101
+ setShowLoading(false)
102
+ })
103
+
104
+ if (loadingDelayMs <= 0) {
105
+ timeout = setTimeout(() => {
106
+ if (active) setShowLoading(true)
107
+ }, 0)
108
+ } else {
109
+ timeout = setTimeout(() => {
110
+ if (active) setShowLoading(true)
111
+ }, loadingDelayMs)
112
+ }
113
+
114
+ loadRouteComponent(match.route.component)
115
+ .then((Component) => {
116
+ if (!active) return
117
+ if (timeout) clearTimeout(timeout)
118
+
119
+ startTransition(() => {
120
+ setRenderedRoute({ ...match, Component })
121
+ setShowLoading(false)
122
+ setLoadError(null)
123
+ })
124
+ })
125
+ .catch((error: unknown) => {
126
+ if (!active) return
127
+ setLoadError(error)
128
+ })
129
+
130
+ return () => {
131
+ active = false
132
+ if (timeout) clearTimeout(timeout)
133
+ }
134
+ }, [loadingDelayMs, match])
135
+
136
+ if (loadError) throw loadError
137
+
138
+ return { renderedRoute, showLoading }
16
139
  }
17
140
 
18
141
  /**
@@ -32,6 +155,14 @@ export interface RouterProps {
32
155
  * Optional fallback rendered while a matched route component module is loading.
33
156
  */
34
157
  loading?: ReactNode
158
+
159
+ /**
160
+ * Delay before rendering [`RouterProps.loading`]{@link RouterProps#loading} for a suspended
161
+ * route, in milliseconds.
162
+ *
163
+ * @defaultValue `400`
164
+ */
165
+ loadingDelayMs?: number
35
166
  }
36
167
 
37
168
  /**
@@ -45,31 +176,32 @@ export interface RouterProps {
45
176
  * import routes from './routes'
46
177
  *
47
178
  * function App() {
48
- * return <Router routes={routes} loading={<div>Loading...</div>} />
179
+ * return <Router routes={routes} loading={<div>Loading...</div>} loadingDelayMs={400} />
49
180
  * }
50
181
  * ```
51
182
  */
52
- export function Router({ routes, loading = null }: RouterProps) {
183
+ export function Router({
184
+ routes,
185
+ loading = null,
186
+ loadingDelayMs = DEFAULT_LOADING_DELAY_MS,
187
+ }: RouterProps) {
53
188
  const pathname = useUrlPath()
54
189
 
55
- const normalizedRoutes = useMemo(
56
- () =>
57
- normalizeRoutes(routes).map((route) => ({
58
- ...route,
59
- Component: getLazyRouteComponent(route.component),
60
- })),
61
- [routes],
190
+ const normalizedRoutes = useMemo(() => normalizeRoutes(routes), [routes])
191
+ const match = useMemo(
192
+ () => findRouteMatch(normalizedRoutes, pathname),
193
+ [normalizedRoutes, pathname],
62
194
  )
195
+ const { renderedRoute, showLoading } = useRenderedRoute(match, loadingDelayMs)
63
196
 
64
- for (const { matches, Component } of normalizedRoutes) {
65
- const params = matches(pathname)
66
- if (!params) continue
67
- return (
68
- <Suspense fallback={loading}>
69
- <Component {...(params as any)} />
70
- </Suspense>
71
- )
72
- }
197
+ if (showLoading) return loading
198
+ if (!renderedRoute) return null
73
199
 
74
- return null
200
+ const { Component, params } = renderedRoute
201
+
202
+ return (
203
+ <Suspense fallback={loading}>
204
+ <Component {...(params as any)} />
205
+ </Suspense>
206
+ )
75
207
  }
package/src/lib/routes.ts CHANGED
@@ -66,6 +66,8 @@ export type RouteObject = {
66
66
  component: RouteComponentLoader<any>
67
67
  }
68
68
 
69
+ export type Routes = RouteObject[]
70
+
69
71
  /**
70
72
  * Route definition consumed by [`Router`]{@link Router}.
71
73
  *
package/tsconfig.json CHANGED
@@ -3,7 +3,8 @@
3
3
  "compilerOptions": {
4
4
  "jsx": "react-jsx",
5
5
  "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
- "types": ["bun", "node", "react", "react-dom"]
6
+ "types": ["react", "react-dom"]
7
7
  },
8
- "include": ["src/**/*", "examples/**/*"]
8
+ "include": ["src/**/*", "examples/**/*.ts", "examples/**/*.tsx"],
9
+ "exclude": ["**/*.test.*", "cli/**/*", "examples/dist/**/*", "examples/server/**/*"]
9
10
  }
package/tsdown.config.ts CHANGED
@@ -4,18 +4,17 @@ import { defineConfig } from 'tsdown'
4
4
  export default defineConfig({
5
5
  entry: {
6
6
  index: 'src/index.ts',
7
- hooks: 'src/hooks/index.ts',
8
- bin: 'src/bin.ts',
7
+ bin: 'cli/bin.ts',
9
8
  },
10
9
  format: 'esm',
11
- platform: 'browser',
10
+ platform: 'neutral',
12
11
  deps: {
13
12
  neverBundle: [/^(node|bun):/, 'react'],
14
13
  },
15
14
  exports: {
16
15
  legacy: true,
17
16
  bin: {
18
- rerouter: './src/bin.ts',
17
+ rerouter: './cli/bin.ts',
19
18
  },
20
19
  },
21
20
  dts: true, // The client must use "moduleResolution": "bundler", "node16" or "nodenext". "node" will not resolve the types properly.
@@ -1,20 +0,0 @@
1
- import { useMemo, useSyncExternalStore } from "react";
2
- //#region src/hooks/useUrl.ts
3
- const getPathname = () => window.location.pathname;
4
- const getSearch = () => window.location.search;
5
- function subscribe(cb) {
6
- const handler = () => cb();
7
- window.addEventListener("popstate", handler);
8
- return () => {
9
- window.removeEventListener("popstate", handler);
10
- };
11
- }
12
- function useUrlPath() {
13
- return useSyncExternalStore(subscribe, getPathname, getPathname);
14
- }
15
- function useUrlSearchParams() {
16
- const search = useSyncExternalStore(subscribe, getSearch, getSearch);
17
- return useMemo(() => new URLSearchParams(search), [search]);
18
- }
19
- //#endregion
20
- export { useUrlSearchParams as n, useUrlPath as t };
package/dist/hooks.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import { n as useUrlSearchParams, t as useUrlPath } from "./index-BYXpNitc.js";
2
- export { useUrlPath, useUrlSearchParams };
package/dist/hooks.js DELETED
@@ -1,2 +0,0 @@
1
- import { n as useUrlSearchParams, t as useUrlPath } from "./hooks-Dlwcb0sV.js";
2
- export { useUrlPath, useUrlSearchParams };
@@ -1,5 +0,0 @@
1
- //#region src/hooks/useUrl.d.ts
2
- declare function useUrlPath(): string;
3
- declare function useUrlSearchParams(): URLSearchParams;
4
- //#endregion
5
- export { useUrlSearchParams as n, useUrlPath as t };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes