@buzola/router 0.0.21 → 0.0.23

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.
@@ -0,0 +1,20 @@
1
+ import { type ComponentType } from 'react';
2
+ type RouteModuleComponent = ComponentType<Record<string, never>>;
3
+ export interface LazyRoute {
4
+ /** Renders the loaded component, synchronously once the module has loaded. */
5
+ component: RouteModuleComponent;
6
+ /** Starts loading the module. Calls share one request; a failed one is retried by the next call. */
7
+ preload: () => Promise<RouteModuleComponent>;
8
+ }
9
+ /**
10
+ * A route component loaded on demand, like `React.lazy`, but sharing its request with `preload`.
11
+ *
12
+ * `React.lazy` starts its own `import()` the first time it renders, even when the module is already
13
+ * loaded, and the browser resolves that a task later. React then commits the Suspense fallback and
14
+ * holds the page back ~300 ms before revealing it. Once loaded, this component renders without suspending.
15
+ */
16
+ export declare function lazyRoute(load: () => Promise<{
17
+ default: RouteModuleComponent;
18
+ }>): LazyRoute;
19
+ export {};
20
+ //# sourceMappingURL=lazy-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy-route.d.ts","sourceRoot":"","sources":["../../src/define/lazy-route.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAyC,MAAM,OAAO,CAAA;AAEjF,KAAK,oBAAoB,GAAG,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;AAEhE,MAAM,WAAW,SAAS;IACzB,8EAA8E;IAC9E,SAAS,EAAE,oBAAoB,CAAA;IAC/B,oGAAoG;IACpG,OAAO,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAA;CAC5C;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,oBAAoB,CAAA;CAAE,CAAC,GAAG,SAAS,CA4B3F"}
@@ -0,0 +1,31 @@
1
+ import { createElement, use } from 'react';
2
+ /**
3
+ * A route component loaded on demand, like `React.lazy`, but sharing its request with `preload`.
4
+ *
5
+ * `React.lazy` starts its own `import()` the first time it renders, even when the module is already
6
+ * loaded, and the browser resolves that a task later. React then commits the Suspense fallback and
7
+ * holds the page back ~300 ms before revealing it. Once loaded, this component renders without suspending.
8
+ */
9
+ export function lazyRoute(load) {
10
+ let loaded = null;
11
+ let request = null;
12
+ let failed = false;
13
+ const start = () => {
14
+ failed = false;
15
+ request = load().then(module => {
16
+ loaded = module.default;
17
+ return module.default;
18
+ }, (error) => {
19
+ failed = true;
20
+ throw error;
21
+ });
22
+ return request;
23
+ };
24
+ const preload = () => (request === null || failed ? start() : request);
25
+ function LazyRouteComponent() {
26
+ // A failed request stays failed for rendering, as with `React.lazy`; only `preload` asks again.
27
+ return createElement(loaded ?? use(request ?? start()));
28
+ }
29
+ return { component: LazyRouteComponent, preload };
30
+ }
31
+ //# sourceMappingURL=lazy-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lazy-route.js","sourceRoot":"","sources":["../../src/define/lazy-route.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,aAAa,EAAqB,GAAG,EAAE,MAAM,OAAO,CAAA;AAWjF;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAsD;IAC/E,IAAI,MAAM,GAAgC,IAAI,CAAA;IAC9C,IAAI,OAAO,GAAyC,IAAI,CAAA;IACxD,IAAI,MAAM,GAAG,KAAK,CAAA;IAElB,MAAM,KAAK,GAAG,GAAkC,EAAE;QACjD,MAAM,GAAG,KAAK,CAAA;QACd,OAAO,GAAG,IAAI,EAAE,CAAC,IAAI,CACpB,MAAM,CAAC,EAAE;YACR,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;YACvB,OAAO,MAAM,CAAC,OAAO,CAAA;QACtB,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YAClB,MAAM,GAAG,IAAI,CAAA;YACb,MAAM,KAAK,CAAA;QACZ,CAAC,CACD,CAAA;QACD,OAAO,OAAO,CAAA;IACf,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,GAAkC,EAAE,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;IAErG,SAAS,kBAAkB;QAC1B,gGAAgG;QAChG,OAAO,aAAa,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAA;IACxD,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAA;AAClD,CAAC"}
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export type { BlockerFn, BuzolaNavigateEvent, BuzolaPageMap, BuzolaPersistentPar
10
10
  export { s } from './engine/schema.js';
11
11
  export { BuzolaRedirect, createPage, redirect } from './define/create-page.js';
12
12
  export type { CatchContext, PageDefinition, PageProps, RedirectFn } from './define/create-page.js';
13
+ export { lazyRoute } from './define/lazy-route.js';
14
+ export type { LazyRoute } from './define/lazy-route.js';
13
15
  export { BuzolaProvider, ErrorBoundary, Link, Outlet, RouteContext, RouterContext, useBlocker, useInvalidate, useNavigate, useNavigationState, useParams, useRoute, useRouter, useRouterState, useSearchParams, } from './react/index.js';
14
16
  export type { BlockerState, BuzolaProviderProps, LinkProps, OutletProps, RouteContextValue } from './react/index.js';
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AACnE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxE,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAC9G,YAAY,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAG7G,YAAY,EACX,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,cAAc,GACd,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAGtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAC9E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAGlG,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AACnE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxE,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAC9G,YAAY,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAG7G,YAAY,EACX,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,cAAc,GACd,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAGtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAC9E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAClG,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAGvD,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { createBrowserNavigationAdapter, createMemoryNavigationAdapter } from '.
8
8
  export { s } from './engine/schema.js';
9
9
  // ─── Route definition ───────────────────────────────────────────────────────
10
10
  export { BuzolaRedirect, createPage, redirect } from './define/create-page.js';
11
+ export { lazyRoute } from './define/lazy-route.js';
11
12
  // ─── React (re-export from subpath) ─────────────────────────────────────────
12
13
  export { BuzolaProvider, ErrorBoundary, Link, Outlet, RouteContext, RouterContext, useBlocker, useInvalidate, useNavigate, useNavigationState, useParams, useRoute, useRouter, useRouterState, useSearchParams, } from './react/index.js';
13
14
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAGnE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAyB9G,gFAAgF;AAChF,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAEtC,+EAA+E;AAC/E,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAG9E,+EAA+E;AAC/E,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAGnE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAyB9G,gFAAgF;AAChF,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAEtC,+EAA+E;AAC/E,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAE9E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAGlD,+EAA+E;AAC/E,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buzola/router",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,98 @@
1
+ import './setup-dom.js'
2
+ import { act, cleanup, render } from '@testing-library/react'
3
+ import { afterEach, describe, expect, it, mock } from 'bun:test'
4
+ import React from 'react'
5
+ import { lazyRoute } from '../define/lazy-route.js'
6
+ import { createMemoryNavigationAdapter } from '../engine/navigation-adapter.js'
7
+ import { buildRouteTree } from '../engine/route-tree.js'
8
+ import { Router } from '../engine/router.js'
9
+ import { BuzolaProvider, Outlet } from '../react/index.js'
10
+
11
+ afterEach(cleanup)
12
+
13
+ function About() {
14
+ return <div>About</div>
15
+ }
16
+
17
+ /** Resolves a task later, the way a browser resolves `import()` even for a module it has already loaded. */
18
+ function loadAfterATask(): Promise<{ default: typeof About }> {
19
+ return new Promise(resolve => setTimeout(() => resolve({ default: About }), 0))
20
+ }
21
+
22
+ /** Lets the load settle and React render what it unblocked. */
23
+ async function settle(route: { preload: () => Promise<unknown> }) {
24
+ await act(async () => {
25
+ await route.preload().catch(() => {})
26
+ await new Promise(resolve => setTimeout(resolve, 0))
27
+ })
28
+ }
29
+
30
+ async function renderRoute(component: React.ComponentType<Record<string, never>>) {
31
+ const fallbacks: string[] = []
32
+ function Fallback() {
33
+ React.useLayoutEffect(() => {
34
+ fallbacks.push('shown')
35
+ }, [])
36
+ return null
37
+ }
38
+ const routes = buildRouteTree([{ path: '/', component, isIndex: true }])
39
+ const router = new Router({ routes, adapter: createMemoryNavigationAdapter({ initialURL: 'http://localhost/' }) })
40
+ // An awaited act: React parks a `use()` suspension made inside a synchronous one and never retries it.
41
+ const { container } = await act(async () =>
42
+ render(
43
+ <BuzolaProvider router={router}>
44
+ <Outlet fallback={<Fallback />} errorFallback={error => <div>Failed: {error.message}</div>} />
45
+ </BuzolaProvider>,
46
+ )
47
+ )
48
+ return { container, fallbacks }
49
+ }
50
+
51
+ describe('lazyRoute', () => {
52
+ it('renders a preloaded route in its first commit, without the fallback', async () => {
53
+ const route = lazyRoute(loadAfterATask)
54
+ await route.preload()
55
+
56
+ const { container, fallbacks } = await renderRoute(route.component)
57
+
58
+ expect(container.textContent).toBe('About')
59
+ expect(fallbacks).toEqual([])
60
+ })
61
+
62
+ it('suspends until a route that was not preloaded loads', async () => {
63
+ const route = lazyRoute(loadAfterATask)
64
+
65
+ const { container, fallbacks } = await renderRoute(route.component)
66
+ expect(fallbacks).toEqual(['shown'])
67
+
68
+ await settle(route)
69
+ expect(container.textContent).toBe('About')
70
+ })
71
+
72
+ it('shares one request between preload and render', async () => {
73
+ const load = mock(loadAfterATask)
74
+ const route = lazyRoute(load)
75
+
76
+ route.preload()
77
+ const { container } = await renderRoute(route.component)
78
+ await settle(route)
79
+
80
+ expect(container.textContent).toBe('About')
81
+ expect(load).toHaveBeenCalledTimes(1)
82
+ })
83
+
84
+ it('a failed load reaches the error boundary without asking again, and the next preload asks again', async () => {
85
+ const load = mock(() => Promise.reject(new Error('chunk missing')))
86
+ const route = lazyRoute(load)
87
+
88
+ const { container } = await renderRoute(route.component)
89
+ await act(() => new Promise(resolve => setTimeout(resolve, 0)))
90
+ expect(container.textContent).toBe('Failed: chunk missing')
91
+
92
+ expect(load).toHaveBeenCalledTimes(1)
93
+
94
+ load.mockImplementation(loadAfterATask)
95
+ expect(await route.preload()).toBe(About)
96
+ expect(load).toHaveBeenCalledTimes(2)
97
+ })
98
+ })
@@ -0,0 +1,47 @@
1
+ import { type ComponentType, createElement, type ReactElement, use } from 'react'
2
+
3
+ type RouteModuleComponent = ComponentType<Record<string, never>>
4
+
5
+ export interface LazyRoute {
6
+ /** Renders the loaded component, synchronously once the module has loaded. */
7
+ component: RouteModuleComponent
8
+ /** Starts loading the module. Calls share one request; a failed one is retried by the next call. */
9
+ preload: () => Promise<RouteModuleComponent>
10
+ }
11
+
12
+ /**
13
+ * A route component loaded on demand, like `React.lazy`, but sharing its request with `preload`.
14
+ *
15
+ * `React.lazy` starts its own `import()` the first time it renders, even when the module is already
16
+ * loaded, and the browser resolves that a task later. React then commits the Suspense fallback and
17
+ * holds the page back ~300 ms before revealing it. Once loaded, this component renders without suspending.
18
+ */
19
+ export function lazyRoute(load: () => Promise<{ default: RouteModuleComponent }>): LazyRoute {
20
+ let loaded: RouteModuleComponent | null = null
21
+ let request: Promise<RouteModuleComponent> | null = null
22
+ let failed = false
23
+
24
+ const start = (): Promise<RouteModuleComponent> => {
25
+ failed = false
26
+ request = load().then(
27
+ module => {
28
+ loaded = module.default
29
+ return module.default
30
+ },
31
+ (error: unknown) => {
32
+ failed = true
33
+ throw error
34
+ },
35
+ )
36
+ return request
37
+ }
38
+
39
+ const preload = (): Promise<RouteModuleComponent> => (request === null || failed ? start() : request)
40
+
41
+ function LazyRouteComponent(): ReactElement {
42
+ // A failed request stays failed for rendering, as with `React.lazy`; only `preload` asks again.
43
+ return createElement(loaded ?? use(request ?? start()))
44
+ }
45
+
46
+ return { component: LazyRouteComponent, preload }
47
+ }
package/src/index.ts CHANGED
@@ -40,6 +40,8 @@ export { s } from './engine/schema.js'
40
40
  // ─── Route definition ───────────────────────────────────────────────────────
41
41
  export { BuzolaRedirect, createPage, redirect } from './define/create-page.js'
42
42
  export type { CatchContext, PageDefinition, PageProps, RedirectFn } from './define/create-page.js'
43
+ export { lazyRoute } from './define/lazy-route.js'
44
+ export type { LazyRoute } from './define/lazy-route.js'
43
45
 
44
46
  // ─── React (re-export from subpath) ─────────────────────────────────────────
45
47
  export {