@pyreon/router 0.11.5 → 0.11.7

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.
@@ -35,7 +35,7 @@ interface RouteMeta {
35
35
  /** If true, guards can redirect to login */
36
36
  requiresAuth?: boolean;
37
37
  /** Scroll behavior for this route */
38
- scrollBehavior?: "top" | "restore" | "none";
38
+ scrollBehavior?: 'top' | 'restore' | 'none';
39
39
  }
40
40
  interface ResolvedRoute<P extends Record<string, string | undefined> = Record<string, string>, Q extends Record<string, string> = Record<string, string>> {
41
41
  path: string;
@@ -126,11 +126,11 @@ interface RouteRecord<TPath extends string = string> {
126
126
  /** Component rendered when this route's loader throws an error */
127
127
  errorComponent?: ComponentFn$1;
128
128
  }
129
- type ScrollBehaviorFn = (to: ResolvedRoute, from: ResolvedRoute, savedPosition: number | null) => "top" | "restore" | "none" | number;
129
+ type ScrollBehaviorFn = (to: ResolvedRoute, from: ResolvedRoute, savedPosition: number | null) => 'top' | 'restore' | 'none' | number;
130
130
  interface RouterOptions {
131
131
  routes: RouteRecord[];
132
132
  /** "hash" (default) uses location.hash; "history" uses pushState */
133
- mode?: "hash" | "history";
133
+ mode?: 'hash' | 'history';
134
134
  /**
135
135
  * Base path for the application. Used when deploying to a sub-path
136
136
  * (e.g. `"/app"` for `https://example.com/app/`).
@@ -142,7 +142,7 @@ interface RouterOptions {
142
142
  * Global scroll behavior. Per-route meta.scrollBehavior takes precedence.
143
143
  * Default: "top"
144
144
  */
145
- scrollBehavior?: ScrollBehaviorFn | "top" | "restore" | "none";
145
+ scrollBehavior?: ScrollBehaviorFn | 'top' | 'restore' | 'none';
146
146
  /**
147
147
  * Initial URL for SSR. On the server, window.location is unavailable;
148
148
  * pass the request URL here so the router resolves the correct route.
@@ -170,7 +170,7 @@ interface RouterOptions {
170
170
  * - `"add"` — ensures paths always end with `/`
171
171
  * - `"ignore"` — no normalization
172
172
  */
173
- trailingSlash?: "strip" | "add" | "ignore";
173
+ trailingSlash?: 'strip' | 'add' | 'ignore';
174
174
  }
175
175
  interface Router {
176
176
  /** Navigate to a path */
@@ -213,7 +213,7 @@ interface Router {
213
213
  }
214
214
  interface RouterInstance extends Router {
215
215
  routes: RouteRecord[];
216
- mode: "hash" | "history";
216
+ mode: 'hash' | 'history';
217
217
  /** Normalized base path (e.g. "/app"), empty string if none */
218
218
  _base: string;
219
219
  _currentPath: Signal<string>;
@@ -222,8 +222,8 @@ interface RouterInstance extends Router {
222
222
  _loadingSignal: Signal<number>;
223
223
  _resolve(rawPath: string): ResolvedRoute;
224
224
  _scrollPositions: Map<string, number>;
225
- _scrollBehavior: RouterOptions["scrollBehavior"];
226
- _onError: RouterOptions["onError"];
225
+ _scrollBehavior: RouterOptions['scrollBehavior'];
226
+ _onError: RouterOptions['onError'];
227
227
  _maxCacheSize: number;
228
228
  /**
229
229
  * Current RouterView nesting depth. Incremented by each RouterView as it
@@ -295,7 +295,7 @@ interface RouterLinkProps extends Props {
295
295
  * - "viewport" — prefetch when the link scrolls into the viewport
296
296
  * - "none" — no prefetching
297
297
  */
298
- prefetch?: "hover" | "viewport" | "none";
298
+ prefetch?: 'hover' | 'viewport' | 'none';
299
299
  children?: VNodeChild | null;
300
300
  }
301
301
  declare const RouterLink: ComponentFn<RouterLinkProps>;
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@pyreon/router",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
4
4
  "description": "Official router for Pyreon",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/router#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/core/router"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/router#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
15
  "files": [
16
16
  "lib",
17
17
  "src",
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
- "sideEffects": false,
22
21
  "type": "module",
22
+ "sideEffects": false,
23
23
  "main": "./lib/index.js",
24
24
  "module": "./lib/index.js",
25
25
  "types": "./lib/types/index.d.ts",
@@ -30,24 +30,24 @@
30
30
  "types": "./lib/types/index.d.ts"
31
31
  }
32
32
  },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
33
36
  "scripts": {
34
37
  "build": "vl_rolldown_build",
35
38
  "dev": "vl_rolldown_build-watch",
36
39
  "test": "vitest run",
37
40
  "typecheck": "tsc --noEmit",
38
- "lint": "biome check .",
41
+ "lint": "oxlint .",
39
42
  "prepublishOnly": "bun run build"
40
43
  },
41
44
  "dependencies": {
42
- "@pyreon/core": "^0.11.5",
43
- "@pyreon/reactivity": "^0.11.5",
44
- "@pyreon/runtime-dom": "^0.11.5"
45
+ "@pyreon/core": "^0.11.7",
46
+ "@pyreon/reactivity": "^0.11.7",
47
+ "@pyreon/runtime-dom": "^0.11.7"
45
48
  },
46
49
  "devDependencies": {
47
50
  "@happy-dom/global-registrator": "^20.8.3",
48
51
  "happy-dom": "^20.8.3"
49
- },
50
- "publishConfig": {
51
- "access": "public"
52
52
  }
53
53
  }
@@ -1,8 +1,8 @@
1
- import type { ComponentFn, Props, VNodeChild } from "@pyreon/core"
2
- import { createRef, h, onUnmount, provide, useContext } from "@pyreon/core"
3
- import { LoaderDataContext, prefetchLoaderData } from "./loader"
4
- import { isLazy, RouterContext, setActiveRouter } from "./router"
5
- import type { LazyComponent, ResolvedRoute, RouteRecord, Router, RouterInstance } from "./types"
1
+ import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
2
+ import { createRef, h, onUnmount, provide, useContext } from '@pyreon/core'
3
+ import { LoaderDataContext, prefetchLoaderData } from './loader'
4
+ import { isLazy, RouterContext, setActiveRouter } from './router'
5
+ import type { LazyComponent, ResolvedRoute, RouteRecord, Router, RouterInstance } from './types'
6
6
 
7
7
  // Track prefetched paths per router to avoid duplicate fetches
8
8
  const _prefetched = new WeakMap<RouterInstance, Set<string>>()
@@ -100,7 +100,7 @@ export const RouterView: ComponentFn<RouterViewProps> = (props) => {
100
100
  return renderLazyRoute(router, record, raw)
101
101
  }
102
102
 
103
- return h("div", { "data-pyreon-router-view": true }, child as unknown as VNodeChild)
103
+ return h('div', { 'data-pyreon-router-view': true }, child as unknown as VNodeChild)
104
104
  }
105
105
 
106
106
  // ─── RouterLink ───────────────────────────────────────────────────────────────
@@ -121,13 +121,13 @@ export interface RouterLinkProps extends Props {
121
121
  * - "viewport" — prefetch when the link scrolls into the viewport
122
122
  * - "none" — no prefetching
123
123
  */
124
- prefetch?: "hover" | "viewport" | "none"
124
+ prefetch?: 'hover' | 'viewport' | 'none'
125
125
  children?: VNodeChild | null
126
126
  }
127
127
 
128
128
  export const RouterLink: ComponentFn<RouterLinkProps> = (props) => {
129
129
  const router = useContext(RouterContext)
130
- const prefetchMode = props.prefetch ?? "hover"
130
+ const prefetchMode = props.prefetch ?? 'hover'
131
131
 
132
132
  const handleClick = (e: MouseEvent) => {
133
133
  e.preventDefault()
@@ -140,29 +140,29 @@ export const RouterLink: ComponentFn<RouterLinkProps> = (props) => {
140
140
  }
141
141
 
142
142
  const handleMouseEnter = () => {
143
- if (prefetchMode !== "hover" || !router) return
143
+ if (prefetchMode !== 'hover' || !router) return
144
144
  prefetchRoute(router as RouterInstance, props.to)
145
145
  }
146
146
 
147
147
  const inst = router as RouterInstance | null
148
- const href = inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`
148
+ const href = inst?.mode === 'history' ? `${inst._base}${props.to}` : `#${props.to}`
149
149
 
150
150
  const activeClass = (): string => {
151
- if (!router) return ""
151
+ if (!router) return ''
152
152
  const current = router.currentRoute().path
153
153
  const target = props.to
154
154
  const isExact = current === target
155
155
  const isActive = isExact || (!props.exact && isSegmentPrefix(current, target))
156
156
 
157
157
  const classes: string[] = []
158
- if (isActive) classes.push(props.activeClass ?? "router-link-active")
159
- if (isExact) classes.push(props.exactActiveClass ?? "router-link-exact-active")
160
- return classes.join(" ").trim()
158
+ if (isActive) classes.push(props.activeClass ?? 'router-link-active')
159
+ if (isExact) classes.push(props.exactActiveClass ?? 'router-link-exact-active')
160
+ return classes.join(' ').trim()
161
161
  }
162
162
 
163
163
  // Viewport prefetching — observe link visibility with IntersectionObserver
164
164
  const ref = createRef<Element>()
165
- if (prefetchMode === "viewport" && router && typeof IntersectionObserver !== "undefined") {
165
+ if (prefetchMode === 'viewport' && router && typeof IntersectionObserver !== 'undefined') {
166
166
  const observer = new IntersectionObserver((entries) => {
167
167
  for (const entry of entries) {
168
168
  if (entry.isIntersecting) {
@@ -180,7 +180,7 @@ export const RouterLink: ComponentFn<RouterLinkProps> = (props) => {
180
180
  }
181
181
 
182
182
  return h(
183
- "a",
183
+ 'a',
184
184
  { ref, href, class: activeClass, onClick: handleClick, onMouseEnter: handleMouseEnter },
185
185
  props.children ?? props.to,
186
186
  )
@@ -214,7 +214,7 @@ function renderLazyRoute(
214
214
  raw
215
215
  .loader()
216
216
  .then((mod) => {
217
- const resolved = typeof mod === "function" ? mod : mod.default
217
+ const resolved = typeof mod === 'function' ? mod : mod.default
218
218
  cacheSet(router, record, resolved)
219
219
  router._loadingSignal.update((n) => n + 1)
220
220
  })
@@ -224,7 +224,7 @@ function renderLazyRoute(
224
224
  tryLoad(attempt + 1),
225
225
  )
226
226
  }
227
- if (typeof window !== "undefined" && isStaleChunk(err)) {
227
+ if (typeof window !== 'undefined' && isStaleChunk(err)) {
228
228
  window.location.reload()
229
229
  return
230
230
  }
@@ -247,7 +247,7 @@ function renderWithLoader(
247
247
  router: RouterInstance,
248
248
  record: RouteRecord,
249
249
  Comp: ComponentFn,
250
- route: Pick<ResolvedRoute, "params" | "query" | "meta">,
250
+ route: Pick<ResolvedRoute, 'params' | 'query' | 'meta'>,
251
251
  ): VNodeChild {
252
252
  const routeProps = { params: route.params, query: route.query, meta: route.meta }
253
253
  if (!record.loader) {
@@ -285,9 +285,9 @@ function cacheSet(router: RouterInstance, record: RouteRecord, comp: ComponentFn
285
285
  * `/admin` is a prefix of `/admin/users` but NOT of `/admin-panel`.
286
286
  */
287
287
  function isSegmentPrefix(current: string, target: string): boolean {
288
- if (target === "/") return false
289
- const cs = current.split("/").filter(Boolean)
290
- const ts = target.split("/").filter(Boolean)
288
+ if (target === '/') return false
289
+ const cs = current.split('/').filter(Boolean)
290
+ const ts = target.split('/').filter(Boolean)
291
291
  if (ts.length > cs.length) return false
292
292
  return ts.every((seg, i) => seg === cs[i])
293
293
  }
@@ -298,7 +298,7 @@ function isSegmentPrefix(current: string, target: string): boolean {
298
298
  * so the user gets the new bundle instead of a broken loading state.
299
299
  */
300
300
  function isStaleChunk(err: unknown): boolean {
301
- if (err instanceof TypeError && String(err.message).includes("Failed to fetch")) return true
301
+ if (err instanceof TypeError && String(err.message).includes('Failed to fetch')) return true
302
302
  if (err instanceof SyntaxError) return true
303
303
  return false
304
304
  }
package/src/index.ts CHANGED
@@ -41,10 +41,10 @@
41
41
  * router.push({ name: "user", params: { id: "42" } })
42
42
  */
43
43
 
44
- export type { RouterLinkProps, RouterProviderProps, RouterViewProps } from "./components"
44
+ export type { RouterLinkProps, RouterProviderProps, RouterViewProps } from './components'
45
45
  // Components
46
- export { RouterLink, RouterProvider, RouterView } from "./components"
47
- export { hydrateLoaderData, prefetchLoaderData, serializeLoaderData, useLoaderData } from "./loader"
46
+ export { RouterLink, RouterProvider, RouterView } from './components'
47
+ export { hydrateLoaderData, prefetchLoaderData, serializeLoaderData, useLoaderData } from './loader'
48
48
  // Match utilities (useful for SSR route pre-fetching)
49
49
  export {
50
50
  buildPath,
@@ -53,7 +53,7 @@ export {
53
53
  parseQueryMulti,
54
54
  resolveRoute,
55
55
  stringifyQuery,
56
- } from "./match"
56
+ } from './match'
57
57
  // Router factory + hooks
58
58
  export {
59
59
  createRouter,
@@ -65,7 +65,7 @@ export {
65
65
  useRoute,
66
66
  useRouter,
67
67
  useSearchParams,
68
- } from "./router"
68
+ } from './router'
69
69
  // Types
70
70
  // Data loaders
71
71
  export type {
@@ -85,6 +85,6 @@ export type {
85
85
  Router,
86
86
  RouterOptions,
87
87
  ScrollBehaviorFn,
88
- } from "./types"
88
+ } from './types'
89
89
  // Lazy helper
90
- export { lazy } from "./types"
90
+ export { lazy } from './types'
package/src/loader.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Context } from "@pyreon/core"
2
- import { createContext, useContext } from "@pyreon/core"
3
- import type { RouterInstance } from "./types"
1
+ import type { Context } from '@pyreon/core'
2
+ import { createContext, useContext } from '@pyreon/core'
3
+ import type { RouterInstance } from './types'
4
4
 
5
5
  /**
6
6
  * Context frame that holds the loader data for the currently rendered route record.
@@ -87,7 +87,7 @@ export function hydrateLoaderData(
87
87
  router: RouterInstance,
88
88
  serialized: Record<string, unknown>,
89
89
  ): void {
90
- if (!serialized || typeof serialized !== "object") return
90
+ if (!serialized || typeof serialized !== 'object') return
91
91
  const route = router._resolve(router.currentRoute().path)
92
92
  for (const record of route.matched) {
93
93
  if (Object.hasOwn(serialized, record.path)) {
package/src/match.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ResolvedRoute, RouteMeta, RouteRecord } from "./types"
1
+ import type { ResolvedRoute, RouteMeta, RouteRecord } from './types'
2
2
 
3
3
  // ─── Query string ─────────────────────────────────────────────────────────────
4
4
 
@@ -9,11 +9,11 @@ import type { ResolvedRoute, RouteMeta, RouteRecord } from "./types"
9
9
  export function parseQuery(qs: string): Record<string, string> {
10
10
  if (!qs) return {}
11
11
  const result: Record<string, string> = {}
12
- for (const part of qs.split("&")) {
13
- const eqIdx = part.indexOf("=")
12
+ for (const part of qs.split('&')) {
13
+ const eqIdx = part.indexOf('=')
14
14
  if (eqIdx < 0) {
15
15
  const key = decodeURIComponent(part)
16
- if (key) result[key] = ""
16
+ if (key) result[key] = ''
17
17
  } else {
18
18
  const key = decodeURIComponent(part.slice(0, eqIdx))
19
19
  const val = decodeURIComponent(part.slice(eqIdx + 1))
@@ -33,13 +33,13 @@ export function parseQuery(qs: string): Record<string, string> {
33
33
  export function parseQueryMulti(qs: string): Record<string, string | string[]> {
34
34
  if (!qs) return {}
35
35
  const result: Record<string, string | string[]> = {}
36
- for (const part of qs.split("&")) {
37
- const eqIdx = part.indexOf("=")
36
+ for (const part of qs.split('&')) {
37
+ const eqIdx = part.indexOf('=')
38
38
  let key: string
39
39
  let val: string
40
40
  if (eqIdx < 0) {
41
41
  key = decodeURIComponent(part)
42
- val = ""
42
+ val = ''
43
43
  } else {
44
44
  key = decodeURIComponent(part.slice(0, eqIdx))
45
45
  val = decodeURIComponent(part.slice(eqIdx + 1))
@@ -62,7 +62,7 @@ export function stringifyQuery(query: Record<string, string>): string {
62
62
  for (const [k, v] of Object.entries(query)) {
63
63
  parts.push(v ? `${encodeURIComponent(k)}=${encodeURIComponent(v)}` : encodeURIComponent(k))
64
64
  }
65
- return parts.length ? `?${parts.join("&")}` : ""
65
+ return parts.length ? `?${parts.join('&')}` : ''
66
66
  }
67
67
 
68
68
  // ─── Compiled route structures ───────────────────────────────────────────────
@@ -134,21 +134,21 @@ interface FlattenedRoute {
134
134
  const _compiledCache = new WeakMap<RouteRecord[], CompiledRoute[]>()
135
135
 
136
136
  function compileSegment(raw: string): CompiledSegment {
137
- if (raw.endsWith("*") && raw.startsWith(":")) {
137
+ if (raw.endsWith('*') && raw.startsWith(':')) {
138
138
  return { raw, isParam: true, isSplat: true, isOptional: false, paramName: raw.slice(1, -1) }
139
139
  }
140
- if (raw.endsWith("?") && raw.startsWith(":")) {
140
+ if (raw.endsWith('?') && raw.startsWith(':')) {
141
141
  return { raw, isParam: true, isSplat: false, isOptional: true, paramName: raw.slice(1, -1) }
142
142
  }
143
- if (raw.startsWith(":")) {
143
+ if (raw.startsWith(':')) {
144
144
  return { raw, isParam: true, isSplat: false, isOptional: false, paramName: raw.slice(1) }
145
145
  }
146
- return { raw, isParam: false, isSplat: false, isOptional: false, paramName: "" }
146
+ return { raw, isParam: false, isSplat: false, isOptional: false, paramName: '' }
147
147
  }
148
148
 
149
149
  function compileRoute(route: RouteRecord): CompiledRoute {
150
150
  const pattern = route.path
151
- const isWildcard = pattern === "(.*)" || pattern === "*"
151
+ const isWildcard = pattern === '(.*)' || pattern === '*'
152
152
 
153
153
  if (isWildcard) {
154
154
  return {
@@ -163,9 +163,9 @@ function compileRoute(route: RouteRecord): CompiledRoute {
163
163
  }
164
164
  }
165
165
 
166
- const segments = pattern.split("/").filter(Boolean).map(compileSegment)
166
+ const segments = pattern.split('/').filter(Boolean).map(compileSegment)
167
167
  const isStatic = segments.every((s) => !s.isParam)
168
- const staticPath = isStatic ? `/${segments.map((s) => s.raw).join("/")}` : null
168
+ const staticPath = isStatic ? `/${segments.map((s) => s.raw).join('/')}` : null
169
169
  const first = segments.length > 0 ? segments[0] : undefined
170
170
  const firstSegment = first && !first.isParam ? first.raw : null
171
171
 
@@ -239,7 +239,7 @@ function makeFlatEntry(
239
239
  segmentCount: segments.length,
240
240
  matchedChain: chain,
241
241
  isStatic,
242
- staticPath: isStatic ? `/${segments.map((s) => s.raw).join("/")}` : null,
242
+ staticPath: isStatic ? `/${segments.map((s) => s.raw).join('/')}` : null,
243
243
  meta,
244
244
  firstSegment: getFirstSegment(segments),
245
245
  hasSplat: segments.some((s) => s.isSplat),
@@ -369,7 +369,7 @@ function buildRouteIndex(routes: RouteRecord[], compiled: CompiledRoute[]): Rout
369
369
  /** Split path into segments without allocating a filtered array */
370
370
  function splitPath(path: string): string[] {
371
371
  // Fast path for common cases
372
- if (path === "/") return []
372
+ if (path === '/') return []
373
373
  // Remove leading slash, split, no filter needed if path is clean
374
374
  const start = path.charCodeAt(0) === 47 /* / */ ? 1 : 0
375
375
  const end = path.length
@@ -390,7 +390,7 @@ function splitPath(path: string): string[] {
390
390
 
391
391
  /** Decode only if the segment contains a `%` character */
392
392
  function decodeSafe(s: string): string {
393
- return s.indexOf("%") >= 0 ? decodeURIComponent(s) : s
393
+ return s.indexOf('%') >= 0 ? decodeURIComponent(s) : s
394
394
  }
395
395
 
396
396
  // ─── Path matching (compiled) ────────────────────────────────────────────────
@@ -411,28 +411,28 @@ function matchPatternSegment(
411
411
  params: Record<string, string>,
412
412
  pathParts: string[],
413
413
  i: number,
414
- ): "splat" | "continue" | "fail" {
415
- if (pp.endsWith("*") && pp.startsWith(":")) {
416
- params[pp.slice(1, -1)] = pathParts.slice(i).map(decodeURIComponent).join("/")
417
- return "splat"
414
+ ): 'splat' | 'continue' | 'fail' {
415
+ if (pp.endsWith('*') && pp.startsWith(':')) {
416
+ params[pp.slice(1, -1)] = pathParts.slice(i).map(decodeURIComponent).join('/')
417
+ return 'splat'
418
418
  }
419
- if (pp.endsWith("?") && pp.startsWith(":")) {
419
+ if (pp.endsWith('?') && pp.startsWith(':')) {
420
420
  if (pt !== undefined) params[pp.slice(1, -1)] = decodeURIComponent(pt)
421
- return "continue"
421
+ return 'continue'
422
422
  }
423
- if (pt === undefined) return "fail"
424
- if (pp.startsWith(":")) {
423
+ if (pt === undefined) return 'fail'
424
+ if (pp.startsWith(':')) {
425
425
  params[pp.slice(1)] = decodeURIComponent(pt)
426
- return "continue"
426
+ return 'continue'
427
427
  }
428
- return pp === pt ? "continue" : "fail"
428
+ return pp === pt ? 'continue' : 'fail'
429
429
  }
430
430
 
431
431
  export function matchPath(pattern: string, path: string): Record<string, string> | null {
432
- if (pattern === "(.*)" || pattern === "*") return {}
432
+ if (pattern === '(.*)' || pattern === '*') return {}
433
433
 
434
- const patternParts = pattern.split("/").filter(Boolean)
435
- const pathParts = path.split("/").filter(Boolean)
434
+ const patternParts = pattern.split('/').filter(Boolean)
435
+ const pathParts = path.split('/').filter(Boolean)
436
436
 
437
437
  const params: Record<string, string> = {}
438
438
  for (let i = 0; i < patternParts.length; i++) {
@@ -443,8 +443,8 @@ export function matchPath(pattern: string, path: string): Record<string, string>
443
443
  pathParts,
444
444
  i,
445
445
  )
446
- if (result === "splat") return params
447
- if (result === "fail") return null
446
+ if (result === 'splat') return params
447
+ if (result === 'fail') return null
448
448
  }
449
449
 
450
450
  if (pathParts.length > patternParts.length) return null
@@ -460,7 +460,7 @@ function captureSplat(pathParts: string[], from: number, pathLen: number): strin
460
460
  const p = pathParts[j]
461
461
  if (p !== undefined) remaining.push(decodeSafe(p))
462
462
  }
463
- return remaining.join("/")
463
+ return remaining.join('/')
464
464
  }
465
465
 
466
466
  // ─── Flattened route matching ─────────────────────────────────────────────────
@@ -534,13 +534,13 @@ interface MatchResult {
534
534
  * Uses flattened index for O(1) static lookup and first-segment dispatch.
535
535
  */
536
536
  export function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRoute {
537
- const qIdx = rawPath.indexOf("?")
537
+ const qIdx = rawPath.indexOf('?')
538
538
  const pathAndHash = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath
539
- const queryPart = qIdx >= 0 ? rawPath.slice(qIdx + 1) : ""
539
+ const queryPart = qIdx >= 0 ? rawPath.slice(qIdx + 1) : ''
540
540
 
541
- const hIdx = pathAndHash.indexOf("#")
541
+ const hIdx = pathAndHash.indexOf('#')
542
542
  const cleanPath = hIdx >= 0 ? pathAndHash.slice(0, hIdx) : pathAndHash
543
- const hash = hIdx >= 0 ? pathAndHash.slice(hIdx + 1) : ""
543
+ const hash = hIdx >= 0 ? pathAndHash.slice(hIdx + 1) : ''
544
544
 
545
545
  const query = parseQuery(queryPart)
546
546
 
@@ -627,13 +627,13 @@ export function buildPath(pattern: string, params: Record<string, string>): stri
627
627
  const built = pattern.replace(/\/:([^/]+)\?/g, (_match, key) => {
628
628
  const val = params[key]
629
629
  // Optional param — omit the entire segment if no value provided
630
- if (!val) return ""
630
+ if (!val) return ''
631
631
  return `/${encodeURIComponent(val)}`
632
632
  })
633
633
  return built.replace(/:([^/]+)\*?/g, (match, key) => {
634
- const val = params[key] ?? ""
634
+ const val = params[key] ?? ''
635
635
  // Splat params contain slashes — don't encode them
636
- if (match.endsWith("*")) return val.split("/").map(encodeURIComponent).join("/")
636
+ if (match.endsWith('*')) return val.split('/').map(encodeURIComponent).join('/')
637
637
  return encodeURIComponent(val)
638
638
  })
639
639
  }