navgo 3.0.3 → 3.0.4

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 (4) hide show
  1. package/index.d.ts +23 -23
  2. package/index.js +10 -10
  3. package/package.json +27 -18
  4. package/readme.md +76 -76
package/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface ValidatorHelpers {
14
14
  oneOf(values: Iterable<string>): (value: string | null | undefined) => boolean
15
15
  }
16
16
 
17
- /** Optional per-route hooks recognized by Navaid. */
17
+ /** Optional per-route hooks recognized by Navgo. */
18
18
  export interface Hooks {
19
19
  /** Validate params with custom per-param validators. Return `false` to skip a match. */
20
20
  param_validators?: Record<string, (value: string | null | undefined) => boolean>
@@ -55,27 +55,11 @@ export interface MatchResult<T = unknown> {
55
55
  params: Params
56
56
  }
57
57
 
58
- export interface Router<T = unknown> {
59
- /** Format `url` relative to the configured base. */
60
- format(url: string): string | false
61
- /** SvelteKit-like navigation that runs loaders before updating the URL. */
62
- goto(url: string, opts?: { replace?: boolean }): Promise<void>
63
- /** Shallow push — updates URL/state without triggering handlers. */
64
- pushState(url?: string | URL, state?: any): void
65
- /** Shallow replace — updates URL/state without triggering handlers. */
66
- replaceState(url?: string | URL, state?: any): void
67
- /** Manually preload loaders for a URL (deduped). */
68
- preload(url: string): Promise<unknown | void>
69
- /** Try to match `url`; returns route tuple and params or `null`. Supports async `validate`. */
70
- match(url: string): Promise<MatchResult<T> | null>
71
- /** Attach history + click listeners and immediately process current location. */
72
- init(): Promise<void>
73
- /** Remove listeners installed by `init()`. */
74
- destroy(): void
75
- }
58
+ // For convenience in docs/types, alias the class instance type
59
+ export type Router<T = unknown> = Navgo<T>
76
60
 
77
- /** Router metadata stored under `history.state.__navaid`. */
78
- export interface NavaidHistoryMeta {
61
+ /** Router metadata stored under `history.state.__navgo`. */
62
+ export interface NavgoHistoryMeta {
79
63
  /** Monotonic index of the current history entry for scroll restoration. */
80
64
  idx: number
81
65
  /** Present when the entry was created via shallow `pushState`/`replaceState`. */
@@ -102,9 +86,25 @@ export interface Options {
102
86
  url_changed?(payload: any): void
103
87
  }
104
88
 
105
- /** Navaid default export: class-based router. */
106
- export default class Navgo<T = unknown> implements Router<T> {
89
+ /** Navgo default export: class-based router. */
90
+ export default class Navgo<T = unknown> {
107
91
  constructor(routes?: Array<RouteTuple<T>>, opts?: Options)
92
+ /** Format `url` relative to the configured base. */
93
+ format(url: string): string | false
94
+ /** SvelteKit-like navigation that runs loaders before updating the URL. */
95
+ goto(url: string, opts?: { replace?: boolean }): Promise<void>
96
+ /** Shallow push — updates URL/state without triggering handlers. */
97
+ pushState(url?: string | URL, state?: any): void
98
+ /** Shallow replace — updates URL/state without triggering handlers. */
99
+ replaceState(url?: string | URL, state?: any): void
100
+ /** Manually preload loaders for a URL (deduped). */
101
+ preload(url: string): Promise<unknown | void>
102
+ /** Try to match `url`; returns route tuple and params or `null`. Supports async `validate`. */
103
+ match(url: string): Promise<MatchResult<T> | null>
104
+ /** Attach history + click listeners and immediately process current location. */
105
+ init(): Promise<void>
106
+ /** Remove listeners installed by `init()`. */
107
+ destroy(): void
108
108
  /** Built-in validator helpers (namespaced). */
109
109
  static validators: ValidatorHelpers
110
110
  }
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parse } from 'regexparam'
2
2
 
3
- const ℹ = (...args) => console.debug(...args)
3
+ const ℹ = (...args) => {}
4
4
 
5
5
  export default class Navgo {
6
6
  #opts = {
@@ -67,7 +67,7 @@ export default class Navgo {
67
67
  // ignore popstate while a hash-originating nav is in flight
68
68
  if (this.#hash_navigating) return
69
69
 
70
- const st = ev?.state?.__navaid
70
+ const st = ev?.state?.__navgo
71
71
  ℹ('[🧭 event:popstate]', st)
72
72
  // Hash-only or state-only change: pathname+search unchanged -> skip loaders
73
73
  const cur = this.#current.url
@@ -97,7 +97,7 @@ export default class Navgo {
97
97
  this.#hash_navigating = false
98
98
  const prev = history.state && typeof history.state == 'object' ? history.state : {}
99
99
  const next_idx = this.#route_idx + 1
100
- const next_state = { ...prev, __navaid: { ...prev.__navaid, idx: next_idx } }
100
+ const next_state = { ...prev, __navgo: { ...prev.__navgo, idx: next_idx } }
101
101
  history.replaceState(next_state, '', location.href)
102
102
  this.#route_idx = next_idx
103
103
  ℹ('[🧭 event:hashchange]', { idx: next_idx, href: location.href })
@@ -125,7 +125,7 @@ export default class Navgo {
125
125
  // persist scroll for refresh / session restore
126
126
  try {
127
127
  sessionStorage.setItem(
128
- `__navaid_scroll:${location.href}`,
128
+ `__navgo_scroll:${location.href}`,
129
129
  JSON.stringify({ x: scrollX, y: scrollY }),
130
130
  )
131
131
  } catch {}
@@ -259,7 +259,7 @@ export default class Navgo {
259
259
  if (nav.cancelled) {
260
260
  // use history.go to cancel the nav, and jump back to where we are
261
261
  if (is_popstate) {
262
- const new_idx = ev_param?.state?.__navaid?.idx
262
+ const new_idx = ev_param?.state?.__navgo?.idx
263
263
  if (new_idx != null) {
264
264
  const delta = new_idx - this.#route_idx
265
265
  if (delta) {
@@ -315,7 +315,7 @@ export default class Navgo {
315
315
  history.state && typeof history.state == 'object' ? history.state : {}
316
316
  const next_state = {
317
317
  ...prev_state,
318
- __navaid: { ...prev_state.__navaid, idx: next_idx, type: nav_type },
318
+ __navgo: { ...prev_state.__navgo, idx: next_idx, type: nav_type },
319
319
  }
320
320
  history[(opts.replace ? 'replace' : 'push') + 'State'](next_state, null, url.href)
321
321
  ℹ('[🧭 history]', opts.replace ? 'replaceState' : 'pushState', {
@@ -522,10 +522,10 @@ export default class Navgo {
522
522
  }
523
523
 
524
524
  // ensure current history state carries our index
525
- const cur_idx = history.state?.__navaid?.idx
525
+ const cur_idx = history.state?.__navgo?.idx
526
526
  if (cur_idx == null) {
527
527
  const prev = history.state && typeof history.state == 'object' ? history.state : {}
528
- const next_state = { ...prev, __navaid: { ...prev.__navaid, idx: this.#route_idx } }
528
+ const next_state = { ...prev, __navgo: { ...prev.__navgo, idx: this.#route_idx } }
529
529
  history.replaceState(next_state, '', location.href)
530
530
  ℹ('[🧭 history]', 'init idx', { idx: this.#route_idx })
531
531
  } else {
@@ -567,7 +567,7 @@ export default class Navgo {
567
567
  const is_initial = ctx && 'from' in ctx ? ctx.from == null : !t
568
568
  if (is_initial) {
569
569
  try {
570
- const k = `__navaid_scroll:${location.href}`
570
+ const k = `__navgo_scroll:${location.href}`
571
571
  const { x, y } = JSON.parse(sessionStorage.getItem(k))
572
572
  sessionStorage.removeItem(k)
573
573
  scrollTo(x, y)
@@ -578,7 +578,7 @@ export default class Navgo {
578
578
  // 1) On back/forward, restore saved position if available
579
579
  if (t === 'popstate') {
580
580
  const ev_state = ctx?.state ?? ctx?.event?.state
581
- const idx = ev_state?.__navaid?.idx
581
+ const idx = ev_state?.__navgo?.idx
582
582
  const target_idx = typeof idx === 'number' ? idx : this.#route_idx - 1
583
583
  this.#route_idx = target_idx
584
584
  const pos = this.#scroll.get(target_idx)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navgo",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/mustafa0x/navgo.git"
@@ -15,11 +15,10 @@
15
15
  "name": "mustafa j"
16
16
  },
17
17
  "scripts": {
18
- "pretest": "pnpm run build",
19
- "test": "vitest run",
20
- "test:e2e": "playwright test",
21
- "test:all": "pnpm run build && vitest run && playwright test",
22
- "types": "pnpm --package=typescript@5.6.3 dlx tsc -p test/types"
18
+ "build": "perl -0777 -i -pe 's/console.debug\\(...args\\)/{}/g' index.js",
19
+ "prepublishOnly": "pnpm run build",
20
+ "test": "vitest run index.test.js",
21
+ "types": "tsc -p test/types"
23
22
  },
24
23
  "files": [
25
24
  "*.d.ts",
@@ -34,26 +33,27 @@
34
33
  "regexparam": "^3.0.0"
35
34
  },
36
35
  "devDependencies": {
37
- "@eslint/js": "^9.36.0",
38
- "@playwright/test": "^1.55.1",
36
+ "@eslint/js": "^9.37.0",
37
+ "@playwright/test": "^1.56.0",
39
38
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
40
- "@tailwindcss/vite": "^4.1.13",
39
+ "@tailwindcss/vite": "^4.1.14",
41
40
  "@tsconfig/svelte": "^5.0.5",
42
- "@types/node": "^24.6.0",
43
- "eslint": "^9.36.0",
41
+ "@types/node": "^24.7.2",
42
+ "eslint": "^9.37.0",
44
43
  "eslint-plugin-svelte": "^3.12.4",
45
44
  "globals": "^16.4.0",
46
- "jiti": "^2.6.0",
45
+ "jiti": "^2.6.1",
47
46
  "lightningcss": "^1.30.2",
48
47
  "prettier": "^3.6.2",
49
48
  "prettier-plugin-svelte": "^3.4.0",
50
49
  "prettier-plugin-tailwindcss": "^0.6.14",
51
- "svelte": "^5.39.7",
52
- "tailwindcss": "^4.1.13",
50
+ "svelte": "^5.39.11",
51
+ "tailwindcss": "^4.1.14",
53
52
  "terser": "5.44.0",
54
- "typescript-eslint": "^8.45.0",
55
- "vite": "^7.1.7",
56
- "vitest": "^2.1.3"
53
+ "typescript": "5.9.3",
54
+ "typescript-eslint": "^8.46.0",
55
+ "vite": "^7.1.9",
56
+ "vitest": "^3.2.4"
57
57
  },
58
58
  "pnpm": {
59
59
  "onlyBuiltDependencies": [
@@ -69,6 +69,15 @@
69
69
  "quoteProps": "as-needed",
70
70
  "bracketSpacing": true,
71
71
  "arrowParens": "avoid",
72
- "useTabs": true
72
+ "useTabs": true,
73
+ "overrides": [
74
+ {
75
+ "files": "*.md",
76
+ "options": {
77
+ "tabWidth": 2,
78
+ "useTabs": false
79
+ }
80
+ }
81
+ ]
73
82
  }
74
83
  }
package/readme.md CHANGED
@@ -11,51 +11,51 @@ import Navgo from 'navgo'
11
11
 
12
12
  // Define routes up front (strings or RegExp)
13
13
  const routes = [
14
- ['/', {}],
15
- ['/users/:username', {}],
16
- ['/books/*', {}],
17
- [/articles\/(?<year>[0-9]{4})/, {}],
18
- [/privacy|privacy-policy/, {}],
19
- [
20
- '/admin',
21
- {
22
- // constrain params with built-ins or your own
23
- param_validators: {
24
- /* id: Navaid.validators.int({ min: 1 }) */
25
- },
26
- // load data before URL changes; result goes to after_navigate(...)
27
- loaders: params => fetch('/api/admin').then(r => r.json()),
28
- // per-route guard; cancel synchronously to block nav
29
- beforeRouteLeave(nav) {
30
- if ((nav.type === 'link' || nav.type === 'nav') && !confirm('Enter admin?')) {
31
- nav.cancel()
32
- }
33
- },
34
- },
35
- ],
14
+ ['/', {}],
15
+ ['/users/:username', {}],
16
+ ['/books/*', {}],
17
+ [/articles\/(?<year>[0-9]{4})/, {}],
18
+ [/privacy|privacy-policy/, {}],
19
+ [
20
+ '/admin',
21
+ {
22
+ // constrain params with built-ins or your own
23
+ param_validators: {
24
+ /* id: Navgo.validators.int({ min: 1 }) */
25
+ },
26
+ // load data before URL changes; result goes to after_navigate(...)
27
+ loaders: params => fetch('/api/admin').then(r => r.json()),
28
+ // per-route guard; cancel synchronously to block nav
29
+ beforeRouteLeave(nav) {
30
+ if ((nav.type === 'link' || nav.type === 'nav') && !confirm('Enter admin?')) {
31
+ nav.cancel()
32
+ }
33
+ },
34
+ },
35
+ ],
36
36
  ]
37
37
 
38
38
  // Create router with options + callbacks
39
39
  const router = new Navgo(routes, {
40
- base: '/',
41
- before_navigate(nav) {
42
- // app-level hook before loaders/URL update; may cancel
43
- console.log('before_navigate', nav.type, '→', nav.to?.url.pathname)
44
- },
45
- after_navigate(nav) {
46
- // called after routing completes; nav.to.data holds loader result
47
- if (nav.to?.data?.__error?.status === 404) {
48
- console.log('404 for', nav.to.url.pathname)
49
- return
50
- }
51
-
52
- console.log('after_navigate', nav.to?.url.pathname, nav.to?.data)
53
- },
54
- url_changed(cur) {
55
- // fires on shallow/hash/popstate-shallow/404 and full navigations
56
- // `cur` is the router snapshot: { url: URL, route, params }
57
- console.log('url_changed', cur.url.href)
58
- },
40
+ base: '/',
41
+ before_navigate(nav) {
42
+ // app-level hook before loaders/URL update; may cancel
43
+ console.log('before_navigate', nav.type, '→', nav.to?.url.pathname)
44
+ },
45
+ after_navigate(nav) {
46
+ // called after routing completes; nav.to.data holds loader result
47
+ if (nav.to?.data?.__error?.status === 404) {
48
+ console.log('404 for', nav.to.url.pathname)
49
+ return
50
+ }
51
+
52
+ console.log('after_navigate', nav.to?.url.pathname, nav.to?.data)
53
+ },
54
+ url_changed(cur) {
55
+ // fires on shallow/hash/popstate-shallow/404 and full navigations
56
+ // `cur` is the router snapshot: { url: URL, route, params }
57
+ console.log('url_changed', cur.url.href)
58
+ },
59
59
  })
60
60
 
61
61
  // Long-lived router: history + <a> bindings
@@ -92,32 +92,32 @@ Notes:
92
92
  #### `options`
93
93
 
94
94
  - `base`: `string` (default `'/'`)
95
- - App base pathname. With or without leading/trailing slashes is accepted.
95
+ - App base pathname. With or without leading/trailing slashes is accepted.
96
96
  - `before_navigate`: `(nav: Navigation) => void`
97
- - App-level hook called once per navigation attempt after the per-route guard and before loaders/URL update. May call `nav.cancel()` synchronously to prevent navigation.
97
+ - App-level hook called once per navigation attempt after the per-route guard and before loaders/URL update. May call `nav.cancel()` synchronously to prevent navigation.
98
98
  - `after_navigate`: `(nav: Navigation) => void`
99
- - App-level hook called after routing completes (URL updated, data loaded). `nav.to.data` holds any loader data.
99
+ - App-level hook called after routing completes (URL updated, data loaded). `nav.to.data` holds any loader data.
100
100
  - `url_changed`: `(snapshot: any) => void`
101
- - Fires on every URL change — shallow `pushState`/`replaceState`, hash changes, `popstate` shallow entries, 404s, and full navigations.
102
- - Receives the router's current snapshot: an object like `{ url: URL, route: RouteTuple|null, params: Params }`.
103
- - The snapshot type is intentionally `any` and may evolve without a breaking change.
101
+ - Fires on every URL change — shallow `pushState`/`replaceState`, hash changes, `popstate` shallow entries, 404s, and full navigations.
102
+ - Receives the router's current snapshot: an object like `{ url: URL, route: RouteTuple|null, params: Params }`.
103
+ - The snapshot type is intentionally `any` and may evolve without a breaking change.
104
104
  - `preload_delay`: `number` (default `20`)
105
- - Delay in ms before hover preloading triggers.
105
+ - Delay in ms before hover preloading triggers.
106
106
  - `preload_on_hover`: `boolean` (default `true`)
107
- - When `false`, disables hover/touch preloading.
107
+ - When `false`, disables hover/touch preloading.
108
108
 
109
- Important: Navaid only processes routes that match your `base` path.
109
+ Important: Navgo only processes routes that match your `base` path.
110
110
 
111
111
  ### Route Hooks
112
112
 
113
113
  - param_validators?: `Record<string, (value: string|null|undefined) => boolean>`
114
- - Validate params (e.g., `id: Navaid.validators.int({ min: 1 })`). Any `false` result skips the route.
114
+ - Validate params (e.g., `id: Navgo.validators.int({ min: 1 })`). Any `false` result skips the route.
115
115
  - loaders?(params): `unknown | Promise | Array<unknown|Promise>`
116
- - Run before URL changes on `link`/`nav`. Results are cached per formatted path and forwarded to `after_navigate`.
116
+ - Run before URL changes on `link`/`nav`. Results are cached per formatted path and forwarded to `after_navigate`.
117
117
  - validate?(params): `boolean | Promise<boolean>`
118
- - Predicate called during matching. If it returns or resolves to `false`, the route is skipped.
118
+ - Predicate called during matching. If it returns or resolves to `false`, the route is skipped.
119
119
  - beforeRouteLeave?(nav): `(nav: Navigation) => void`
120
- - Guard called once per navigation attempt on the current route (leave). Call `nav.cancel()` synchronously to prevent navigation. For `popstate`, cancellation auto-reverts the history jump.
120
+ - Guard called once per navigation attempt on the current route (leave). Call `nav.cancel()` synchronously to prevent navigation. For `popstate`, cancellation auto-reverts the history jump.
121
121
 
122
122
  The `Navigation` object contains:
123
123
 
@@ -137,29 +137,29 @@ The `Navigation` object contains:
137
137
 
138
138
  - Router calls `before_navigate` on the current route (leave).
139
139
  - Call `nav.cancel()` synchronously to cancel.
140
- - For `link`/`nav`, it stops before URL change.
141
- - For `popstate`, cancellation causes an automatic `history.go(...)` to revert to the previous index.
142
- - For `leave`, cancellation triggers the native “Leave site?” dialog (behavior is browser-controlled).
140
+ - For `link`/`nav`, it stops before URL change.
141
+ - For `popstate`, cancellation causes an automatic `history.go(...)` to revert to the previous index.
142
+ - For `leave`, cancellation triggers the native “Leave site?” dialog (behavior is browser-controlled).
143
143
 
144
144
  Example:
145
145
 
146
146
  ```js
147
147
  const routes = [
148
- [
149
- '/admin',
150
- {
151
- param_validators: {
152
- /* ... */
153
- },
154
- loaders: params => fetch('/api/admin/stats').then(r => r.json()),
155
- beforeRouteLeave(nav) {
156
- if (nav.type === 'link' || nav.type === 'nav') {
157
- if (!confirm('Enter admin area?')) nav.cancel()
158
- }
159
- },
160
- },
161
- ],
162
- ['/', {}],
148
+ [
149
+ '/admin',
150
+ {
151
+ param_validators: {
152
+ /* ... */
153
+ },
154
+ loaders: params => fetch('/api/admin/stats').then(r => r.json()),
155
+ beforeRouteLeave(nav) {
156
+ if (nav.type === 'link' || nav.type === 'nav') {
157
+ if (!confirm('Enter admin area?')) nav.cancel()
158
+ }
159
+ },
160
+ },
161
+ ],
162
+ ['/', {}],
163
163
  ]
164
164
 
165
165
  const router = new Navgo(routes, { base: '/app' })
@@ -204,7 +204,7 @@ The desired path to navigate. If it begins with `/` and does not match the confi
204
204
  Type: `Object`
205
205
 
206
206
  - replace: `Boolean` (default `false`)
207
- - When `true`, uses `history.replaceState`; otherwise `history.pushState`.
207
+ - When `true`, uses `history.replaceState`; otherwise `history.pushState`.
208
208
 
209
209
  ### init()
210
210
 
@@ -312,7 +312,7 @@ This lets you reflect UI state in the URL while deferring route transitions unti
312
312
 
313
313
  ### History Index & popstate Cancellation
314
314
 
315
- To enable `popstate` cancellation, Navaid stores a monotonic `idx` in `history.state.__navaid.idx`. On `popstate`, a cancelled navigation computes the delta between the target and current `idx` and calls `history.go(-delta)` to return to the prior entry.
315
+ To enable `popstate` cancellation, Navgo stores a monotonic `idx` in `history.state.__navgo.idx`. On `popstate`, a cancelled navigation computes the delta between the target and current `idx` and calls `history.go(-delta)` to return to the prior entry.
316
316
 
317
317
  ### Scroll Restoration
318
318
 
@@ -320,8 +320,8 @@ Navgo manages scroll manually (sets `history.scrollRestoration = 'manual'`) and
320
320
 
321
321
  - Saves the current scroll position for the active history index.
322
322
  - On `link`/`nav` (after route commit):
323
- - If the URL has a `#hash`, scroll to the matching element `id` or `[name="..."]`.
324
- - Otherwise, scroll to the top `(0, 0)`.
323
+ - If the URL has a `#hash`, scroll to the matching element `id` or `[name="..."]`.
324
+ - Otherwise, scroll to the top `(0, 0)`.
325
325
  - On `popstate`: restore the saved position for the target history index; if not found but there is a `#hash`, scroll to the anchor instead.
326
326
  - Shallow `pushState`/`replaceState` never adjust scroll (routing is skipped).
327
327