navgo 6.0.2 → 6.0.3

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/changelog.md CHANGED
@@ -3,6 +3,12 @@
3
3
  ## v6
4
4
 
5
5
  - add route groups for nested layouts/shared loaders; expose ordered `matches` (layouts → route) on `nav.to.matches` and `router.route`
6
+ - add optional route group `id`s plus keyed `layouts` lookups on navigation targets, `match()`, and `router.route` for direct access to shared layout/group data
7
+ - migration:
8
+ - before: `const app_data = nav.to?.matches?.find(m => m.type === 'layout')?.data`
9
+ - after: `const app_data = nav.to?.layouts?.app?.data`
10
+ - docs/typed surfaces now describe `match()` as returning `{ route, params, matches, layouts }` and `window.navgo.route` as including `layouts`
11
+ - covered on direct `match()`, completed navigations, `popstate`, and preload-reused navigations
6
12
  - run group loaders (and group `before_route_leave`) for matched child routes; preload caches the full loader chain and `goto` reuses it
7
13
  - use `param_rules` for per-param validation + coercion (superseding `param_validators`, which has been removed)
8
14
  - example:
package/index.d.ts CHANGED
@@ -83,6 +83,8 @@ export interface LoaderContext {
83
83
  export interface Match<T = unknown> {
84
84
  /** Matched layout/group wrapper or the final route tuple. */
85
85
  type: 'layout' | 'route'
86
+ /** Present when `type === 'layout'` and the route group declared an `id`. */
87
+ id?: string
86
88
  /** Present when `type === 'layout'`. */
87
89
  layout?: any
88
90
  /** Present when `type === 'route'`. */
@@ -91,7 +93,17 @@ export interface Match<T = unknown> {
91
93
  data?: unknown
92
94
  }
93
95
 
96
+ export interface LayoutMatch<T = unknown> extends Match<T> {
97
+ type: 'layout'
98
+ id: string
99
+ }
100
+
101
+ /** Keyed lookup into matched layout/group wrappers. Values are the same objects as in `matches`. */
102
+ export type LayoutsMap<T = unknown> = Record<string, LayoutMatch<T>>
103
+
94
104
  export interface RouteGroup<T = unknown> {
105
+ /** Optional stable key for direct access via `layouts[id]`. Must be unique across groups. */
106
+ id?: string
95
107
  /** Optional layout component/module (router does not render; it just forwards this). */
96
108
  layout?: any
97
109
  /** Load data for this layout group. Return a LoadPlan (object) or a Promise for arbitrary data. */
@@ -109,6 +121,7 @@ export type RouteEntry<T = unknown> = RouteTuple<T> | RouteGroup<T>
109
121
 
110
122
  export interface PreloadBundle<T = unknown> {
111
123
  matches: Match<T>[]
124
+ layouts?: LayoutsMap<T>
112
125
  data?: unknown
113
126
  }
114
127
 
@@ -133,6 +146,8 @@ export interface NavigationTarget<T = unknown> {
133
146
  route: RouteTuple<T> | null
134
147
  /** Ordered matches for nested layouts and the final route (outer → inner). */
135
148
  matches?: Match<T>[]
149
+ /** Keyed lookup into matched layout/group wrappers by `RouteGroup.id`. */
150
+ layouts?: LayoutsMap<T>
136
151
  /** Optional data from route loader when available. */
137
152
  data?: unknown
138
153
  }
@@ -156,6 +171,7 @@ export interface MatchResult<T = unknown> {
156
171
  route: RouteTuple<T>
157
172
  params: Params
158
173
  matches: Match<T>[]
174
+ layouts: LayoutsMap<T>
159
175
  }
160
176
 
161
177
  // For convenience in docs/types, alias the class instance type
@@ -217,12 +233,13 @@ export default class Navgo<T = unknown> {
217
233
  init(): Promise<void>
218
234
  /** Remove listeners installed by `init()`. */
219
235
  destroy(): void
220
- /** Writable store with current { url, route, params, matches, search_params }. */
236
+ /** Writable store with current { url, route, params, matches, layouts, search_params }. */
221
237
  readonly route: import('svelte/store').Writable<{
222
238
  url: URL
223
239
  route: RouteTuple<T> | null
224
240
  params: Params
225
241
  matches: Match<T>[]
242
+ layouts: LayoutsMap<T>
226
243
  search_params: Record<string, unknown>
227
244
  }>
228
245
  /** Last completed navigation object. */
package/index.js CHANGED
@@ -49,8 +49,15 @@ export default class Navgo {
49
49
  #base_rgx = /^\/+/
50
50
  /** @type {Map<string, { promise?: Promise<PreloadBundle>, data?: PreloadBundle }>} */
51
51
  #preloads = new Map()
52
- /** @type {{ url: URL|null, route: RouteTuple|null, params: Params, matches: Match[], search_params: Record<string, unknown> }} */
53
- #current = { url: null, route: null, params: {}, matches: [], search_params: {} }
52
+ /** @type {{ url: URL|null, route: RouteTuple|null, params: Params, matches: Match[], layouts: Record<string, Match>, search_params: Record<string, unknown> }} */
53
+ #current = {
54
+ url: null,
55
+ route: null,
56
+ params: {},
57
+ matches: [],
58
+ layouts: Object.create(null),
59
+ search_params: {},
60
+ }
54
61
  /** @type {number} */
55
62
  #route_idx = 0
56
63
  /** @type {boolean} */
@@ -81,6 +88,7 @@ export default class Navgo {
81
88
  route: null,
82
89
  params: {},
83
90
  matches: [],
91
+ layouts: Object.create(null),
84
92
  search_params: {},
85
93
  })
86
94
  /** @type {Navigation|null} */
@@ -632,10 +640,17 @@ export default class Navgo {
632
640
  }
633
641
  }
634
642
 
643
+ #build_layouts(matches) {
644
+ const out = Object.create(null)
645
+ for (const m of matches || []) if (m.type === 'layout' && m.id) out[m.id] = m
646
+ return out
647
+ }
648
+
635
649
  #build_matches(route, stack) {
636
650
  const out = []
637
651
  for (const g of stack || []) {
638
652
  const obj = { type: 'layout', layout: g.layout }
653
+ if (g.id) obj.id = g.id
639
654
  Object.defineProperty(obj, '__entry', { value: g })
640
655
  out.push(obj)
641
656
  }
@@ -671,6 +686,7 @@ export default class Navgo {
671
686
  for (let i = 0; i < matches.length; i++) matches[i].data = datas[i]
672
687
  return {
673
688
  matches,
689
+ layouts: this.#build_layouts(matches),
674
690
  data: datas[datas.length - 1],
675
691
  search: { schema, opts, defaults, search_params },
676
692
  }
@@ -689,6 +705,7 @@ export default class Navgo {
689
705
  params: this.#current.params || {},
690
706
  route: this.#current.route,
691
707
  matches: this.#current.matches || [],
708
+ layouts: this.#current.layouts || Object.create(null),
692
709
  }
693
710
  : null
694
711
  return {
@@ -746,7 +763,7 @@ export default class Navgo {
746
763
 
747
764
  let nav = this.#make_nav({
748
765
  type: nav_type,
749
- to: { url, params: {}, route: null, matches: [] },
766
+ to: { url, params: {}, route: null, matches: [], layouts: Object.create(null) },
750
767
  event: ev_param,
751
768
  })
752
769
  ℹ('[🧭 goto]', 'start', {
@@ -780,8 +797,9 @@ export default class Navgo {
780
797
  params: hit.params || {},
781
798
  route: hit.route || null,
782
799
  matches: hit.matches || [],
800
+ layouts: hit.layouts || this.#build_layouts(hit.matches || []),
783
801
  }
784
- : { url, params: {}, route: null, matches: [] }
802
+ : { url, params: {}, route: null, matches: [], layouts: Object.create(null) }
785
803
  if (match_error) nav.to.data = { __error: match_error }
786
804
 
787
805
  // before_navigate (skip initial)
@@ -856,6 +874,10 @@ export default class Navgo {
856
874
  route: match_error ? null : hit?.route || null,
857
875
  params: match_error ? {} : hit?.params || {},
858
876
  matches,
877
+ layouts:
878
+ hit && !match_error
879
+ ? bundle?.layouts || this.#build_layouts(matches)
880
+ : Object.create(null),
859
881
  search_params: {},
860
882
  }
861
883
 
@@ -870,6 +892,7 @@ export default class Navgo {
870
892
  params: prev.params || {},
871
893
  route: prev.route,
872
894
  matches: prev.matches || [],
895
+ layouts: prev.layouts || Object.create(null),
873
896
  }
874
897
  : null,
875
898
  to: {
@@ -877,6 +900,7 @@ export default class Navgo {
877
900
  params: match_error ? {} : hit?.params || {},
878
901
  route: match_error ? null : hit?.route || null,
879
902
  matches,
903
+ layouts: this.#current.layouts || Object.create(null),
880
904
  data,
881
905
  },
882
906
  event: ev_param,
@@ -1147,10 +1171,12 @@ export default class Navgo {
1147
1171
  }
1148
1172
 
1149
1173
  ℹ('[🧭 match]', 'hit', { pattern: obj.data?.[0], params })
1174
+ const matches = this.#build_matches(obj.data, obj.stack)
1150
1175
  return {
1151
1176
  route: obj.data || null,
1152
1177
  params,
1153
- matches: this.#build_matches(obj.data, obj.stack),
1178
+ matches,
1179
+ layouts: this.#build_layouts(matches),
1154
1180
  }
1155
1181
  }
1156
1182
  ℹ('[🧭 match]', 'miss', { url: url_raw })
@@ -1166,6 +1192,8 @@ export default class Navgo {
1166
1192
  this.#base_rgx =
1167
1193
  this.#base == '/' ? /^\/+/ : new RegExp('^\\' + this.#base + '(?=\\/|$)\\/?', 'i')
1168
1194
 
1195
+ const group_ids = new Map()
1196
+
1169
1197
  function compile_routes(entries, stack = []) {
1170
1198
  const out = []
1171
1199
  for (const e of entries || []) {
@@ -1181,6 +1209,14 @@ export default class Navgo {
1181
1209
  continue
1182
1210
  }
1183
1211
  if (e && typeof e === 'object' && Array.isArray(e.routes)) {
1212
+ if (e.id != null) {
1213
+ if (typeof e.id !== 'string' || !e.id) {
1214
+ throw new Error('Route group id must be a non-empty string')
1215
+ }
1216
+ if (group_ids.has(e.id))
1217
+ throw new Error(`Duplicate route group id "${e.id}"`)
1218
+ group_ids.set(e.id, e)
1219
+ }
1184
1220
  out.push(...compile_routes(e.routes, stack.concat(e)))
1185
1221
  continue
1186
1222
  }
package/llms.txt CHANGED
@@ -6,7 +6,7 @@
6
6
  - `goto()` and `preload()` are safe to call without `await` from events (no unhandled rejections); errors surface via `nav.to.data.__error` (or preload bundle `data.__error`)
7
7
  - `router.init()` attaches the router instance to `window.navgo` by default
8
8
  - stores:
9
- - `window.navgo.route`: `{ url, route, params, matches, search_params }`
9
+ - `window.navgo.route`: `{ url, route, params, matches, layouts, search_params }`
10
10
  - `window.navgo.is_navigating`: boolean
11
11
 
12
12
  ## Setup (App Wiring)
@@ -52,6 +52,7 @@
52
52
  - Handle 404 / loader errors:
53
53
  - `const err = nav.to?.data?.__error; if (err?.status === 404) show_404 = true`
54
54
  - Shared layout data:
55
- - read `nav.to.matches` outer → inner; layouts are `m.type === 'layout'`, route leaf is `m.type === 'route'`
55
+ - use `nav.to.layouts?.id?.data` or `$route.layouts?.id?.data` for direct access to matched group data
56
+ - `nav.to.matches` remains the ordered outer → inner structure; layouts are `m.type === 'layout'`, route leaf is `m.type === 'route'`
56
57
  - Stable scroll panes:
57
58
  - set `id="pane"` or `data-scroll-id="pane"` on scroll containers to get popstate restoration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navgo",
3
- "version": "6.0.2",
3
+ "version": "6.0.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/mustafa0x/navgo.git"
package/readme.md CHANGED
@@ -114,6 +114,7 @@ Each route group is an object:
114
114
 
115
115
  ```js
116
116
  {
117
+ id?: string,
117
118
  layout?: any,
118
119
  loader?: (ctx) => LoadPlan | Promise<unknown>,
119
120
  before_route_leave?: (nav) => void,
@@ -121,6 +122,7 @@ Each route group is an object:
121
122
  }
122
123
  ```
123
124
 
125
+ - `id` enables direct keyed access via `nav.to.layouts[id]` / `$route.layouts[id]`. IDs must be unique across route groups.
124
126
  - `layout` is forwarded into `nav.to.matches` (the router does not render anything).
125
127
  - `loader` runs for every matched child route in the group.
126
128
  - `before_route_leave` runs when leaving a matched route within the group.
@@ -171,7 +173,7 @@ Important: Navgo only processes routes that match your `base` path.
171
173
 
172
174
  ### Instance stores
173
175
 
174
- - `router.route` -- `Writable<{ url: URL; route: RouteTuple|null; params: Params; matches: Match[]; search_params: Record<string, unknown> }>`
176
+ - `router.route` -- `Writable<{ url: URL; route: RouteTuple|null; params: Params; matches: Match[]; layouts: Record<string, Match>; search_params: Record<string, unknown> }>`
175
177
  - Readonly property that holds the current snapshot.
176
178
  - Subscribe to react to changes; Navgo updates it on every URL change.
177
179
  - `router.is_navigating` -- `Writable<boolean>`
@@ -315,8 +317,8 @@ The `Navigation` object contains:
315
317
  ```ts
316
318
  {
317
319
  type: 'link' | 'goto' | 'popstate' | 'leave',
318
- from: { url, params, route, matches } | null,
319
- to: { url, params, route, matches, data } | null,
320
+ from: { url, params, route, matches, layouts } | null,
321
+ to: { url, params, route, matches, layouts, data } | null,
320
322
  will_unload: boolean,
321
323
  cancelled: boolean,
322
324
  event?: Event,
@@ -324,7 +326,7 @@ The `Navigation` object contains:
324
326
  }
325
327
  ```
326
328
 
327
- `nav.to.matches` is ordered **outer → inner** and contains both layouts and the final route:
329
+ `nav.to.matches` is ordered **outer → inner** and remains the canonical structure:
328
330
 
329
331
  ```js
330
332
  for (const m of nav.to?.matches || []) {
@@ -333,6 +335,13 @@ for (const m of nav.to?.matches || []) {
333
335
  }
334
336
  ```
335
337
 
338
+ If a matched route group declares an `id`, Navgo also exposes a keyed lookup that points at the same match object:
339
+
340
+ ```js
341
+ const session = nav.to?.layouts?.app?.data
342
+ const admin_layout = $route.layouts?.admin
343
+ ```
344
+
336
345
  #### Order & cancellation:
337
346
 
338
347
  - Router calls `before_route_leave` on the current route (leave).
@@ -556,7 +565,7 @@ scroll flow
556
565
  ### Method-by-Method Semantics
557
566
 
558
567
  - `format(uri)` -- normalizes a path relative to `base`. Returns `false` when `uri` is outside of `base`.
559
- - `match(uri)` -- returns a Promise of `{ route, params } | null` using string/RegExp patterns and `param_rules` (Valibot schemas). Awaits an async `validate(params)` if provided.
568
+ - `match(uri)` -- returns a Promise of `{ route, params, matches, layouts } | null` using string/RegExp patterns and `param_rules` (Valibot schemas). Awaits an async `validate(params)` if provided.
560
569
  - `goto(uri, { replace? })` -- fires route-level `before_route_leave('goto')`, calls global `before_navigate`, saves scroll, runs loader, pushes/replaces, and completes via `after_navigate`.
561
570
  - `init()` -- wires global listeners (`popstate`, `pushstate`, `replacestate`, click) and optional hover/tap preloading; immediately processes the current location.
562
571
  - `destroy()` -- removes listeners added by `init()`.