@orkestrel/router 0.0.17 → 0.0.18
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/dist/src/browser/index.js +10 -11
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +42 -56
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +42 -56
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +3 -6
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +3 -6
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -18
|
@@ -2,7 +2,7 @@ import { canonicalizePath, createRouter, joinPaths } from "../core/index.js";
|
|
|
2
2
|
import { createAbort } from "@orkestrel/abort";
|
|
3
3
|
import { Emitter } from "@orkestrel/emitter";
|
|
4
4
|
import { ContractError, isFunction, isString, preview } from "@orkestrel/contract";
|
|
5
|
-
|
|
5
|
+
// src/browser/helpers.ts
|
|
6
6
|
/**
|
|
7
7
|
* Computes the canonical path key a `Navigator` registers a browser navigation
|
|
8
8
|
* route under.
|
|
@@ -115,8 +115,7 @@ function resolveLocationPath(location, history, base) {
|
|
|
115
115
|
function findAnchor(event) {
|
|
116
116
|
for (const node of event.composedPath()) if (node instanceof HTMLAnchorElement) return node;
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
//#region src/browser/Navigator.ts
|
|
118
|
+
// src/browser/Navigator.ts
|
|
120
119
|
/**
|
|
121
120
|
* Represents the headless History/hash navigation entity — composes one core
|
|
122
121
|
* `Router<Meta>`, resolving the current location on `start()` and
|
|
@@ -300,15 +299,17 @@ var Navigator = class {
|
|
|
300
299
|
this.#emitter.emit("navigate", to);
|
|
301
300
|
}
|
|
302
301
|
async #guarded(guard, to, from, handle) {
|
|
303
|
-
|
|
302
|
+
const verdict = await this.#verdict(guard, to, from, handle);
|
|
303
|
+
if (handle.signal.aborted || !verdict) return;
|
|
304
|
+
this.#commit(to);
|
|
305
|
+
}
|
|
306
|
+
async #verdict(guard, to, from, handle) {
|
|
304
307
|
try {
|
|
305
|
-
|
|
308
|
+
return await guard(to, from, handle.signal);
|
|
306
309
|
} catch (error) {
|
|
307
310
|
this.#surface(error);
|
|
308
|
-
return;
|
|
311
|
+
return false;
|
|
309
312
|
}
|
|
310
|
-
if (handle.signal.aborted || !verdict) return;
|
|
311
|
-
this.#commit(to);
|
|
312
313
|
}
|
|
313
314
|
#surface(error) {
|
|
314
315
|
const handler = this.#error;
|
|
@@ -333,8 +334,7 @@ var Navigator = class {
|
|
|
333
334
|
}, true, this.#base));
|
|
334
335
|
}
|
|
335
336
|
};
|
|
336
|
-
|
|
337
|
-
//#region src/browser/factories.ts
|
|
337
|
+
// src/browser/factories.ts
|
|
338
338
|
/**
|
|
339
339
|
* Creates a {@link NavigatorInterface} — the headless History/hash navigation
|
|
340
340
|
* entity composing one core `Router<Meta>`.
|
|
@@ -369,7 +369,6 @@ var Navigator = class {
|
|
|
369
369
|
function createNavigator(options) {
|
|
370
370
|
return new Navigator(options);
|
|
371
371
|
}
|
|
372
|
-
//#endregion
|
|
373
372
|
export { Navigator, computeNavigationKey, createNavigator, extractHashPath, findAnchor, resolveLocationPath };
|
|
374
373
|
|
|
375
374
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (self-describing helper naming —\n// module scope, no entity context). Every one is exported (the centralized-file\n// rule): the `Navigator` composes them, and each has its own unit test. NO `node:*`\n// — DOM-typed only (`Location`, `Event`, `HTMLAnchorElement`), valid under the\n// `src:browser` scoped isolation check.\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Computes the canonical path key a `Navigator` registers a browser navigation\n * route under.\n *\n * @remarks\n * Projects the route's path through the core engine's canonical trailing-slash\n * identity, so `/users` and `/users/` replace one another in the Navigator's\n * shared Router. The entry's `meta` payload is never read, so any payload type\n * is accepted.\n *\n * @param entry - The Router entry carrying the Navigator route\n * @returns The route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<unknown>): string {\n\treturn canonicalizePath(entry.path)\n}\n\n/**\n * Extracts the `/`-prefixed pathname from a `location.hash` value — strips the\n * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.\n *\n * @remarks\n * The grammar this package matches everywhere is `/`-prefixed, so a hash-mode\n * location's `'#/users/7?x'` becomes `'/users/7'`\n * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,\n * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls\n * back). Total — never throws.\n *\n * @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)\n * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash\n *\n * @example\n * ```ts\n * extractHashPath('#/users/7?x') // '/users/7'\n * extractHashPath('#/tokens') // '/tokens'\n * extractHashPath('') // '' — the Navigator falls back\n * extractHashPath('#other') // '' — not a `#/` route hash\n * ```\n */\nexport function extractHashPath(hash: string): string {\n\tif (!hash.startsWith('#/')) return ''\n\tconst withoutHash = hash.slice(1)\n\tconst queryIndex = withoutHash.indexOf('?')\n\treturn queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n}\n\n/**\n * Resolves the `/`-prefixed pathname to match for the current location, in\n * either navigation mode — the one seam `extractHashPath` (hash mode) and\n * history-mode base-stripping share.\n *\n * @remarks\n * Hash mode (`history: false`) reads `location.hash` through\n * {@link extractHashPath}. History mode (`history: true`) reads\n * `location.pathname` and strips a leading `base` prefix when one is\n * configured: `base` itself maps to the root `'/'`; a pathname that is not\n * under `base` is returned unchanged (a base mismatch is not this helper's\n * concern — the `Navigator`'s match then misses). Total — never throws.\n *\n * @param location - The `hash` + `pathname` pair to resolve from (accepts a\n * real `Location` or any object shaped the same, for pure unit testing)\n * @param history - If `true`, the pathname is read from `location.pathname`\n * with `base` stripped; if `false`, it is read from `location.hash`\n * @param base - The history-mode path prefix to strip (ignored in hash mode;\n * omit for no prefix)\n * @returns The `/`-prefixed pathname to match\n *\n * @example\n * ```ts\n * resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'\n * resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'\n * ```\n */\nexport function resolveLocationPath(\n\tlocation: Pick<Location, 'hash' | 'pathname'>,\n\thistory: boolean,\n\tbase?: string,\n): string {\n\tif (!history) return extractHashPath(location.hash)\n\tconst pathname = location.pathname\n\tif (base === undefined || base === '') return pathname\n\tconst normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base\n\tif (pathname === normalizedBase) return '/'\n\tif (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length)\n\treturn pathname\n}\n\n/**\n * Finds the nearest enclosing `<a>` element a DOM event originated from, by\n * walking its composed path — the pure lookup behind history-mode link\n * interception.\n *\n * @remarks\n * Uses `event.composedPath()` (not `event.target`) so a click on a styled\n * child INSIDE an anchor (an icon, a span) still resolves to the anchor.\n * Total — never throws; returns `undefined` when no anchor is found on the\n * path.\n *\n * @param event - The DOM event to search (typically a `click`)\n * @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`\n *\n * @example\n * ```ts\n * document.addEventListener('click', (event) => {\n * \tconst anchor = findAnchor(event)\n * \tif (anchor !== undefined) console.log(anchor.href)\n * })\n * ```\n */\nexport function findAnchor(event: Event): HTMLAnchorElement | undefined {\n\tfor (const node of event.composedPath()) {\n\t\tif (node instanceof HTMLAnchorElement) return node\n\t}\n\treturn undefined\n}\n","import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js'\nimport type { AbortInterface } from '@orkestrel/abort'\nimport type { EmitterErrorHandler, EmitterInterface } from '@orkestrel/emitter'\nimport type { RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * Represents the headless History/hash navigation entity — composes one core\n * `Router<Meta>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter}. No `render` /\n * `outlet` — the consumer owns rendering.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n *\n * @remarks\n * - **One shared engine.** Each `route.path` is registered on the SAME\n * `Router` machine the core `Dispatcher` composes, keyed for dedup by its\n * {@link canonicalizePath} (last write wins, replace-in-place) — literal-\n * over-param precedence, trailing-slash insensitivity, and\n * `:param`/`*wildcard` extraction all come from that one shared engine.\n * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match\n * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the\n * `fallback` through the SAME engine → a fallback that ALSO matches nothing\n * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a\n * newer navigation) and leaves `active` `undefined`, emitting nothing —\n * honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may\n * veto → on a verdict, `active` is set and `navigate` emitted.\n * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`\n * handle, aborting the PREVIOUS navigation's handle first; a guard verdict\n * that resolves after its navigation was superseded (`signal.aborted`) is\n * discarded, same as a `false`/rejected verdict. A guard throw routes to\n * the `error` handler and vetoes. `stop()`/`destroy()` also abort the\n * pending handle.\n * - **Hash vs history mode.** Hash mode (`history: false`, the default) binds\n * `hashchange`; history mode (`history: true`) binds `popstate` and, when\n * `intercept` is set, same-origin `<a>` click interception (a plain\n * left-click with no modifier keys, `target`, or `download` attribute).\n *\n * @example\n * ```ts\n * const navigator = new Navigator<{ readonly title: string }>({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * })\n * navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))\n * navigator.start() // resolves the current hash now, and on every hashchange\n * navigator.navigate('/tokens')\n * ```\n */\nexport class Navigator<Meta> implements NavigatorInterface<Meta> {\n\treadonly #router: RouterInterface<Meta>\n\treadonly #emitter: Emitter<NavigatorEventMap<Meta>>\n\treadonly #history: boolean\n\treadonly #base: string | undefined\n\treadonly #fallback: string | undefined\n\treadonly #guard: NavigatorOptions<Meta>['guard']\n\treadonly #error: EmitterErrorHandler | undefined\n\treadonly #intercept: boolean\n\treadonly #listener: () => void\n\treadonly #clickListener: (event: MouseEvent) => void\n\t#active: RouterMatch<Meta> | undefined\n\t#started = false\n\t#current: AbortInterface | undefined\n\n\tconstructor(options: NavigatorOptions<Meta>) {\n\t\tif (options.guard !== undefined && !isFunction(options.guard))\n\t\t\tthrow new ContractError('a navigator guard must be a function when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'guard'],\n\t\t\t\t\tlimit: 'function or undefined',\n\t\t\t\t\treceived: preview(options.guard),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new ContractError('a navigator fallback must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'fallback'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.fallback),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new ContractError('a navigator base must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'base'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.base),\n\t\t\t\t},\n\t\t\t})\n\t\tthis.#history = options.history ?? false\n\t\tthis.#base = options.base\n\t\tthis.#intercept = options.intercept ?? false\n\t\tthis.#guard = options.guard\n\t\tthis.#error = options.error\n\t\tthis.#emitter = new Emitter<NavigatorEventMap<Meta>>({\n\t\t\t...(options.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#router = createRouter<Meta>({\n\t\t\tentries: options.routes,\n\t\t\t...(options.sensitive === undefined ? {} : { sensitive: options.sensitive }),\n\t\t\tkey: computeNavigationKey,\n\t\t})\n\t\tthis.#fallback = options.fallback ?? options.routes[0]?.path\n\t\tthis.#listener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<Meta> {\n\t\treturn this.#router\n\t}\n\n\tget emitter(): EmitterInterface<NavigatorEventMap<Meta>> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): RouterMatch<Meta> | undefined {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\tif (this.#started) return\n\t\tthis.#started = true\n\t\tif (!this.#history) {\n\t\t\twindow.addEventListener('hashchange', this.#listener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#listener)\n\t\t\tif (this.#intercept) document.addEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#resolve()\n\t}\n\n\tstop(): void {\n\t\tif (!this.#started) return\n\t\tthis.#started = false\n\t\tif (!this.#history) {\n\t\t\twindow.removeEventListener('hashchange', this.#listener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#listener)\n\t\t\tif (this.#intercept) document.removeEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#current?.abort()\n\t}\n\n\tnavigate(path: string): void {\n\t\tif (!this.#history) {\n\t\t\tconst next = `#${path}`\n\t\t\tif (window.location.hash === next) this.#resolve()\n\t\t\telse window.location.hash = next\n\t\t\treturn\n\t\t}\n\t\tconst target = this.#base === undefined ? path : joinPaths(this.#base, path)\n\t\twindow.history.pushState(null, '', target)\n\t\tthis.#resolve()\n\t}\n\n\tmatch(path: string): RouterMatch<Meta> | undefined {\n\t\treturn this.#router.match(path)\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// === Private\n\n\t// Compute the pathname to match for the CURRENT location, resolve it (falling back to the\n\t// configured fallback through the SAME engine on a miss), and either navigate or — when\n\t// neither the location nor the fallback matches anything — leave `active` `undefined` with\n\t// no emit, honest to the one-shared-engine rule: no phantom match is fabricated.\n\t#resolve(): void {\n\t\tconst pathname = resolveLocationPath(\n\t\t\t{ hash: window.location.hash, pathname: window.location.pathname },\n\t\t\tthis.#history,\n\t\t\tthis.#base,\n\t\t)\n\t\tconst to = this.match(pathname) ?? this.#matchFallback()\n\t\tif (to === undefined) {\n\t\t\tthis.#current?.abort()\n\t\t\tthis.#active = undefined\n\t\t\treturn\n\t\t}\n\t\tthis.#navigate(to)\n\t}\n\n\t#matchFallback(): RouterMatch<Meta> | undefined {\n\t\tif (this.#fallback === undefined) return undefined\n\t\treturn this.match(this.#fallback)\n\t}\n\n\t// Supersede the previous pending navigation's abort handle, mint a fresh one for this\n\t// navigation, and either commit directly (no guard configured — the synchronous fast path) or\n\t// run the guard pipeline.\n\t#navigate(to: RouterMatch<Meta>): void {\n\t\tthis.#current?.abort()\n\t\tconst handle = createAbort()\n\t\tthis.#current = handle\n\t\tconst guard = this.#guard\n\t\tif (guard === undefined) {\n\t\t\tthis.#commit(to)\n\t\t\treturn\n\t\t}\n\t\tvoid this.#guarded(guard, to, this.#active, handle)\n\t}\n\n\t#commit(to: RouterMatch<Meta>): void {\n\t\tthis.#active = to\n\t\tthis.#emitter.emit('navigate', to)\n\t}\n\n\t// Await the guard's verdict; a throw routes to the `error` handler and vetoes, a discarded\n\t// verdict (superseded through `handle.signal.aborted`, or a plain `false`/rejected verdict) leaves\n\t// `active` unchanged with no emit, and a true verdict commits.\n\tasync #guarded(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<void> {\n\t\tlet verdict: boolean\n\t\ttry {\n\t\t\tverdict = await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn\n\t\t}\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\t// Route a guard throw to the `error` handler (the Emitter pattern's own channel, not a listener\n\t// throw so it cannot flow through the emitter's `emit`), swallowing a throwing handler itself\n\t// (anti-recursion, mirroring the emitter's own contract).\n\t#surface(error: unknown): void {\n\t\tconst handler = this.#error\n\t\tif (handler === undefined) return\n\t\ttry {\n\t\t\thandler(error, 'navigate')\n\t\t} catch {\n\t\t\t// The error handler itself threw — swallow it (anti-recursion).\n\t\t}\n\t}\n\n\t// Same-origin `<a>` click interception (history mode, opt-in through `intercept`): skip an\n\t// already-handled event, a non-primary button, any modifier key, a targeted or download link,\n\t// or a cross-origin destination — otherwise prevent the native navigation and `navigate` instead.\n\t#intercepted(event: MouseEvent): void {\n\t\tif (event.defaultPrevented || event.button !== 0) return\n\t\tif (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\tconst anchor = findAnchor(event)\n\t\tif (anchor === undefined) return\n\t\tif (anchor.target !== '' && anchor.target !== '_self') return\n\t\tif (anchor.hasAttribute('download')) return\n\t\tconst url = new URL(anchor.href, window.location.href)\n\t\tif (url.origin !== window.location.origin) return\n\t\tevent.preventDefault()\n\t\tthis.navigate(resolveLocationPath({ hash: url.hash, pathname: url.pathname }, true, this.#base))\n\t}\n}\n","import type { NavigatorInterface, NavigatorOptions } from './types.js'\nimport { Navigator } from './Navigator.js'\n\n/**\n * Creates a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<Meta>`.\n *\n * @remarks\n * Prefer this over `new Navigator(...)` at call sites that only need the\n * interface.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n * @param options - The `routes` to register, the `history` toggle (default\n * `false`, hash mode), an optional `base` (history mode), an optional\n * `fallback` path, an optional `guard` hook, opt-in link `intercept`\n * (history mode), the `sensitive` case toggle, and the Emitter pattern's\n * `on`/`error` wiring\n * @returns A live {@link NavigatorInterface} handle — call `start()` to begin\n * dispatching\n *\n * @example\n * ```ts\n * import { createNavigator } from '@src/browser'\n *\n * const navigator = createNavigator({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * \ton: { navigate: (match) => (document.title = match.meta.title) },\n * })\n * navigator.start()\n * ```\n */\nexport function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta> {\n\treturn new Navigator<Meta>(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/helpers.ts","../../../src/browser/Navigator.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The PURE browser-navigation primitives (self-describing helper naming —\n// module scope, no entity context). Every one is exported (the centralized-file\n// rule): the `Navigator` composes them, and each has its own unit test. NO `node:*`\n// — DOM-typed only (`Location`, `Event`, `HTMLAnchorElement`), valid under the\n// `src:browser` scoped isolation check.\n\nimport type { RouteEntry } from '@src/core'\nimport { canonicalizePath } from '@src/core'\n\n/**\n * Computes the canonical path key a `Navigator` registers a browser navigation\n * route under.\n *\n * @remarks\n * Projects the route's path through the core engine's canonical trailing-slash\n * identity, so `/users` and `/users/` replace one another in the Navigator's\n * shared Router. The entry's `meta` payload is never read, so any payload type\n * is accepted.\n *\n * @param entry - The Router entry carrying the Navigator route\n * @returns The route's canonical path\n *\n * @example\n * ```ts\n * computeNavigationKey({ path: '/users/', meta: {} }) // '/users'\n * ```\n */\nexport function computeNavigationKey(entry: RouteEntry<unknown>): string {\n\treturn canonicalizePath(entry.path)\n}\n\n/**\n * Extracts the `/`-prefixed pathname from a `location.hash` value — strips the\n * leading `#` (keeping the route's own leading `/`) and any `?query` suffix.\n *\n * @remarks\n * The grammar this package matches everywhere is `/`-prefixed, so a hash-mode\n * location's `'#/users/7?x'` becomes `'/users/7'`\n * — a hash pattern is expected to start `'#/'`; anything else (an empty hash,\n * or one that does not begin `'#/'`) yields `''` (the `Navigator` then falls\n * back). Total — never throws.\n *\n * @param hash - The raw `window.location.hash` value (for example `'#/users/7?x'`)\n * @returns The `/`-prefixed pathname to match, or `''` for an empty / non-`#/` hash\n *\n * @example\n * ```ts\n * extractHashPath('#/users/7?x') // '/users/7'\n * extractHashPath('#/tokens') // '/tokens'\n * extractHashPath('') // '' — the Navigator falls back\n * extractHashPath('#other') // '' — not a `#/` route hash\n * ```\n */\nexport function extractHashPath(hash: string): string {\n\tif (!hash.startsWith('#/')) return ''\n\tconst withoutHash = hash.slice(1)\n\tconst queryIndex = withoutHash.indexOf('?')\n\treturn queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n}\n\n/**\n * Resolves the `/`-prefixed pathname to match for the current location, in\n * either navigation mode — the one seam `extractHashPath` (hash mode) and\n * history-mode base-stripping share.\n *\n * @remarks\n * Hash mode (`history: false`) reads `location.hash` through\n * {@link extractHashPath}. History mode (`history: true`) reads\n * `location.pathname` and strips a leading `base` prefix when one is\n * configured: `base` itself maps to the root `'/'`; a pathname that is not\n * under `base` is returned unchanged (a base mismatch is not this helper's\n * concern — the `Navigator`'s match then misses). Total — never throws.\n *\n * @param location - The `hash` + `pathname` pair to resolve from (accepts a\n * real `Location` or any object shaped the same, for pure unit testing)\n * @param history - If `true`, the pathname is read from `location.pathname`\n * with `base` stripped; if `false`, it is read from `location.hash`\n * @param base - The history-mode path prefix to strip (ignored in hash mode;\n * omit for no prefix)\n * @returns The `/`-prefixed pathname to match\n *\n * @example\n * ```ts\n * resolveLocationPath({ hash: '#/users/7', pathname: '/' }, false) // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app/users/7' }, true, '/app') // '/users/7'\n * resolveLocationPath({ hash: '', pathname: '/app' }, true, '/app') // '/'\n * resolveLocationPath({ hash: '', pathname: '/other/users' }, true, '/app') // '/other/users'\n * ```\n */\nexport function resolveLocationPath(\n\tlocation: Pick<Location, 'hash' | 'pathname'>,\n\thistory: boolean,\n\tbase?: string,\n): string {\n\tif (!history) return extractHashPath(location.hash)\n\tconst pathname = location.pathname\n\tif (base === undefined || base === '') return pathname\n\tconst normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base\n\tif (pathname === normalizedBase) return '/'\n\tif (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length)\n\treturn pathname\n}\n\n/**\n * Finds the nearest enclosing `<a>` element a DOM event originated from, by\n * walking its composed path — the pure lookup behind history-mode link\n * interception.\n *\n * @remarks\n * Uses `event.composedPath()` (not `event.target`) so a click on a styled\n * child INSIDE an anchor (an icon, a span) still resolves to the anchor.\n * Total — never throws; returns `undefined` when no anchor is found on the\n * path.\n *\n * @param event - The DOM event to search (typically a `click`)\n * @returns The nearest enclosing `HTMLAnchorElement`, or `undefined`\n *\n * @example\n * ```ts\n * document.addEventListener('click', (event) => {\n * \tconst anchor = findAnchor(event)\n * \tif (anchor !== undefined) console.log(anchor.href)\n * })\n * ```\n */\nexport function findAnchor(event: Event): HTMLAnchorElement | undefined {\n\tfor (const node of event.composedPath()) {\n\t\tif (node instanceof HTMLAnchorElement) return node\n\t}\n\treturn undefined\n}\n","import type { NavigatorEventMap, NavigatorInterface, NavigatorOptions } from './types.js'\nimport type { AbortInterface } from '@orkestrel/abort'\nimport type { EmitterErrorHandler, EmitterInterface } from '@orkestrel/emitter'\nimport type { RouterInterface, RouterMatch } from '@src/core'\nimport { createAbort } from '@orkestrel/abort'\nimport { Emitter } from '@orkestrel/emitter'\nimport { ContractError, isFunction, isString, preview } from '@orkestrel/contract'\nimport { createRouter, joinPaths } from '@src/core'\nimport { computeNavigationKey, findAnchor, resolveLocationPath } from './helpers.js'\n\n/**\n * Represents the headless History/hash navigation entity — composes one core\n * `Router<Meta>`, resolving the current location on `start()` and\n * every subsequent navigation event, tracking `active`, and emitting\n * `navigate` through the core {@link Emitter}. No `render` /\n * `outlet` — the consumer owns rendering.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n *\n * @remarks\n * - **One shared engine.** Each `route.path` is registered on the SAME\n * `Router` machine the core `Dispatcher` composes, keyed for dedup by its\n * {@link canonicalizePath} (last write wins, replace-in-place) — literal-\n * over-param precedence, trailing-slash insensitivity, and\n * `:param`/`*wildcard` extraction all come from that one shared engine.\n * - **Resolve pipeline.** Compute the `/`-prefixed pathname to match\n * ({@link resolveLocationPath}) → {@link match} it → on a miss, match the\n * `fallback` through the SAME engine → a fallback that ALSO matches nothing\n * aborts any pending guarded navigation (a miss SUPERSEDES it, same as a\n * newer navigation) and leaves `active` `undefined`, emitting nothing —\n * honest to the one-shared-engine rule: no phantom match is fabricated → the optional `guard` may\n * veto → on a verdict, `active` is set and `navigate` emitted.\n * - **Supersede-safe guard.** Every navigation mints an `@orkestrel/abort`\n * handle, aborting the PREVIOUS navigation's handle first; a guard verdict\n * that resolves after its navigation was superseded (`signal.aborted`) is\n * discarded, same as a `false`/rejected verdict. A guard throw routes to\n * the `error` handler and vetoes. `stop()`/`destroy()` also abort the\n * pending handle.\n * - **Hash vs history mode.** Hash mode (`history: false`, the default) binds\n * `hashchange`; history mode (`history: true`) binds `popstate` and, when\n * `intercept` is set, same-origin `<a>` click interception (a plain\n * left-click with no modifier keys, `target`, or `download` attribute).\n *\n * @example\n * ```ts\n * const navigator = new Navigator<{ readonly title: string }>({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * })\n * navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))\n * navigator.start() // resolves the current hash now, and on every hashchange\n * navigator.navigate('/tokens')\n * ```\n */\nexport class Navigator<Meta> implements NavigatorInterface<Meta> {\n\treadonly #router: RouterInterface<Meta>\n\treadonly #emitter: Emitter<NavigatorEventMap<Meta>>\n\treadonly #history: boolean\n\treadonly #base: string | undefined\n\treadonly #fallback: string | undefined\n\treadonly #guard: NavigatorOptions<Meta>['guard']\n\treadonly #error: EmitterErrorHandler | undefined\n\treadonly #intercept: boolean\n\treadonly #listener: () => void\n\treadonly #clickListener: (event: MouseEvent) => void\n\t#active: RouterMatch<Meta> | undefined\n\t#started = false\n\t#current: AbortInterface | undefined\n\n\tconstructor(options: NavigatorOptions<Meta>) {\n\t\tif (options.guard !== undefined && !isFunction(options.guard))\n\t\t\tthrow new ContractError('a navigator guard must be a function when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'guard'],\n\t\t\t\t\tlimit: 'function or undefined',\n\t\t\t\t\treceived: preview(options.guard),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.fallback !== undefined && !isString(options.fallback))\n\t\t\tthrow new ContractError('a navigator fallback must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'fallback'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.fallback),\n\t\t\t\t},\n\t\t\t})\n\t\tif (options.base !== undefined && !isString(options.base))\n\t\t\tthrow new ContractError('a navigator base must be a string when defined', {\n\t\t\t\tcode: 'literal',\n\t\t\t\tcontext: {\n\t\t\t\t\tpath: ['options', 'base'],\n\t\t\t\t\tlimit: 'string or undefined',\n\t\t\t\t\treceived: preview(options.base),\n\t\t\t\t},\n\t\t\t})\n\t\tthis.#history = options.history ?? false\n\t\tthis.#base = options.base\n\t\tthis.#intercept = options.intercept ?? false\n\t\tthis.#guard = options.guard\n\t\tthis.#error = options.error\n\t\tthis.#emitter = new Emitter<NavigatorEventMap<Meta>>({\n\t\t\t...(options.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#router = createRouter<Meta>({\n\t\t\tentries: options.routes,\n\t\t\t...(options.sensitive === undefined ? {} : { sensitive: options.sensitive }),\n\t\t\tkey: computeNavigationKey,\n\t\t})\n\t\tthis.#fallback = options.fallback ?? options.routes[0]?.path\n\t\tthis.#listener = this.#resolve.bind(this)\n\t\tthis.#clickListener = this.#intercepted.bind(this)\n\t}\n\n\tget router(): RouterInterface<Meta> {\n\t\treturn this.#router\n\t}\n\n\tget emitter(): EmitterInterface<NavigatorEventMap<Meta>> {\n\t\treturn this.#emitter\n\t}\n\n\tget active(): RouterMatch<Meta> | undefined {\n\t\treturn this.#active\n\t}\n\n\tstart(): void {\n\t\tif (this.#started) return\n\t\tthis.#started = true\n\t\tif (!this.#history) {\n\t\t\twindow.addEventListener('hashchange', this.#listener)\n\t\t} else {\n\t\t\twindow.addEventListener('popstate', this.#listener)\n\t\t\tif (this.#intercept) document.addEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#resolve()\n\t}\n\n\tstop(): void {\n\t\tif (!this.#started) return\n\t\tthis.#started = false\n\t\tif (!this.#history) {\n\t\t\twindow.removeEventListener('hashchange', this.#listener)\n\t\t} else {\n\t\t\twindow.removeEventListener('popstate', this.#listener)\n\t\t\tif (this.#intercept) document.removeEventListener('click', this.#clickListener)\n\t\t}\n\t\tthis.#current?.abort()\n\t}\n\n\tnavigate(path: string): void {\n\t\tif (!this.#history) {\n\t\t\tconst next = `#${path}`\n\t\t\tif (window.location.hash === next) this.#resolve()\n\t\t\telse window.location.hash = next\n\t\t\treturn\n\t\t}\n\t\tconst target = this.#base === undefined ? path : joinPaths(this.#base, path)\n\t\twindow.history.pushState(null, '', target)\n\t\tthis.#resolve()\n\t}\n\n\tmatch(path: string): RouterMatch<Meta> | undefined {\n\t\treturn this.#router.match(path)\n\t}\n\n\tdestroy(): void {\n\t\tthis.stop()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// === Private\n\n\t// Compute the pathname to match for the CURRENT location, resolve it (falling back to the\n\t// configured fallback through the SAME engine on a miss), and either navigate or — when\n\t// neither the location nor the fallback matches anything — leave `active` `undefined` with\n\t// no emit, honest to the one-shared-engine rule: no phantom match is fabricated.\n\t#resolve(): void {\n\t\tconst pathname = resolveLocationPath(\n\t\t\t{ hash: window.location.hash, pathname: window.location.pathname },\n\t\t\tthis.#history,\n\t\t\tthis.#base,\n\t\t)\n\t\tconst to = this.match(pathname) ?? this.#matchFallback()\n\t\tif (to === undefined) {\n\t\t\tthis.#current?.abort()\n\t\t\tthis.#active = undefined\n\t\t\treturn\n\t\t}\n\t\tthis.#navigate(to)\n\t}\n\n\t#matchFallback(): RouterMatch<Meta> | undefined {\n\t\tif (this.#fallback === undefined) return undefined\n\t\treturn this.match(this.#fallback)\n\t}\n\n\t// Supersede the previous pending navigation's abort handle, mint a fresh one for this\n\t// navigation, and either commit directly (no guard configured — the synchronous fast path) or\n\t// run the guard pipeline.\n\t#navigate(to: RouterMatch<Meta>): void {\n\t\tthis.#current?.abort()\n\t\tconst handle = createAbort()\n\t\tthis.#current = handle\n\t\tconst guard = this.#guard\n\t\tif (guard === undefined) {\n\t\t\tthis.#commit(to)\n\t\t\treturn\n\t\t}\n\t\tvoid this.#guarded(guard, to, this.#active, handle)\n\t}\n\n\t#commit(to: RouterMatch<Meta>): void {\n\t\tthis.#active = to\n\t\tthis.#emitter.emit('navigate', to)\n\t}\n\n\t// Await the guard's verdict; a throw routes to the `error` handler and vetoes, a discarded\n\t// verdict (superseded through `handle.signal.aborted`, or a plain `false`/rejected verdict) leaves\n\t// `active` unchanged with no emit, and a true verdict commits.\n\tasync #guarded(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<void> {\n\t\tconst verdict = await this.#verdict(guard, to, from, handle)\n\t\tif (handle.signal.aborted || !verdict) return\n\t\tthis.#commit(to)\n\t}\n\n\tasync #verdict(\n\t\tguard: NonNullable<NavigatorOptions<Meta>['guard']>,\n\t\tto: RouterMatch<Meta>,\n\t\tfrom: RouterMatch<Meta> | undefined,\n\t\thandle: AbortInterface,\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\treturn await guard(to, from, handle.signal)\n\t\t} catch (error) {\n\t\t\tthis.#surface(error)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Route a guard throw to the `error` handler (the Emitter pattern's own channel, not a listener\n\t// throw so it cannot flow through the emitter's `emit`), swallowing a throwing handler itself\n\t// (anti-recursion, mirroring the emitter's own contract).\n\t#surface(error: unknown): void {\n\t\tconst handler = this.#error\n\t\tif (handler === undefined) return\n\t\ttry {\n\t\t\thandler(error, 'navigate')\n\t\t} catch {\n\t\t\t// The error handler itself threw — swallow it (anti-recursion).\n\t\t}\n\t}\n\n\t// Same-origin `<a>` click interception (history mode, opt-in through `intercept`): skip an\n\t// already-handled event, a non-primary button, any modifier key, a targeted or download link,\n\t// or a cross-origin destination — otherwise prevent the native navigation and `navigate` instead.\n\t#intercepted(event: MouseEvent): void {\n\t\tif (event.defaultPrevented || event.button !== 0) return\n\t\tif (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return\n\t\tconst anchor = findAnchor(event)\n\t\tif (anchor === undefined) return\n\t\tif (anchor.target !== '' && anchor.target !== '_self') return\n\t\tif (anchor.hasAttribute('download')) return\n\t\tconst url = new URL(anchor.href, window.location.href)\n\t\tif (url.origin !== window.location.origin) return\n\t\tevent.preventDefault()\n\t\tthis.navigate(resolveLocationPath({ hash: url.hash, pathname: url.pathname }, true, this.#base))\n\t}\n}\n","import type { NavigatorInterface, NavigatorOptions } from './types.js'\nimport { Navigator } from './Navigator.js'\n\n/**\n * Creates a {@link NavigatorInterface} — the headless History/hash navigation\n * entity composing one core `Router<Meta>`.\n *\n * @remarks\n * Prefer this over `new Navigator(...)` at call sites that only need the\n * interface.\n *\n * @typeParam Meta - The opaque per-route payload a match carries back\n * @param options - The `routes` to register, the `history` toggle (default\n * `false`, hash mode), an optional `base` (history mode), an optional\n * `fallback` path, an optional `guard` hook, opt-in link `intercept`\n * (history mode), the `sensitive` case toggle, and the Emitter pattern's\n * `on`/`error` wiring\n * @returns A live {@link NavigatorInterface} handle — call `start()` to begin\n * dispatching\n *\n * @example\n * ```ts\n * import { createNavigator } from '@src/browser'\n *\n * const navigator = createNavigator({\n * \troutes: [\n * \t\t{ path: '/users/:id', meta: { title: 'User' } },\n * \t\t{ path: '/tokens', meta: { title: 'Tokens' } },\n * \t],\n * \ton: { navigate: (match) => (document.title = match.meta.title) },\n * })\n * navigator.start()\n * ```\n */\nexport function createNavigator<Meta>(options: NavigatorOptions<Meta>): NavigatorInterface<Meta> {\n\treturn new Navigator<Meta>(options)\n}\n"],"mappings":"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"}
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
|
-
|
|
4
|
+
// src/core/constants.ts
|
|
5
5
|
/**
|
|
6
6
|
* Lists the HTTP methods a {@link import('./types.js').DispatcherInterface} registers
|
|
7
7
|
* routes under, in canonical order — a frozen literal tuple, and the single source the
|
|
@@ -92,8 +92,7 @@ var TIER_PARAM = 1;
|
|
|
92
92
|
* ```
|
|
93
93
|
*/
|
|
94
94
|
var TIER_WILDCARD = 0;
|
|
95
|
-
|
|
96
|
-
//#region src/core/helpers.ts
|
|
95
|
+
// src/core/helpers.ts
|
|
97
96
|
/**
|
|
98
97
|
* Escapes every regex metacharacter in a literal string so it can be embedded
|
|
99
98
|
* inside a larger `RegExp` source without being interpreted as syntax.
|
|
@@ -210,10 +209,9 @@ function computeDispatchKey(entry) {
|
|
|
210
209
|
* ```
|
|
211
210
|
*/
|
|
212
211
|
function compilePath(path, sensitive = true) {
|
|
213
|
-
const params = [];
|
|
214
212
|
const normalized = canonicalizePath(path);
|
|
215
213
|
const segments = normalized.split("/");
|
|
216
|
-
const
|
|
214
|
+
const compiledSegments = segments.map((segment, index) => {
|
|
217
215
|
const isFinal = index === segments.length - 1;
|
|
218
216
|
if (!isFinal && /^\*[A-Za-z_]\w*/.test(segment)) throw new _orkestrel_contract.ContractError("a wildcard segment must be the final segment of a path pattern", {
|
|
219
217
|
code: "placement",
|
|
@@ -224,17 +222,21 @@ function compilePath(path, sensitive = true) {
|
|
|
224
222
|
}
|
|
225
223
|
});
|
|
226
224
|
const tier = classifySegment(segment, isFinal);
|
|
227
|
-
if (tier === 0) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
225
|
+
if (tier === 0) return {
|
|
226
|
+
pattern: "(.+)",
|
|
227
|
+
param: segment.slice(1)
|
|
228
|
+
};
|
|
231
229
|
if (tier === 1) {
|
|
232
230
|
const name = /^:([A-Za-z_]\w*)/.exec(segment)?.[1] ?? "";
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
return {
|
|
232
|
+
pattern: `([^/]+)${escapeRegExp(segment.slice(1 + name.length))}`,
|
|
233
|
+
param: name
|
|
234
|
+
};
|
|
235
235
|
}
|
|
236
|
-
return escapeRegExp(segment);
|
|
237
|
-
})
|
|
236
|
+
return { pattern: escapeRegExp(segment) };
|
|
237
|
+
});
|
|
238
|
+
const pattern = compiledSegments.map((segment) => segment.pattern).join("/");
|
|
239
|
+
const params = compiledSegments.flatMap((segment) => segment.param === void 0 ? [] : [segment.param]);
|
|
238
240
|
const suffix = normalized === "/" || normalized === "" ? "" : "/?";
|
|
239
241
|
const flags = sensitive ? "" : "i";
|
|
240
242
|
return {
|
|
@@ -295,13 +297,13 @@ function decodeParam(value) {
|
|
|
295
297
|
function matchPath(compiled, pathname) {
|
|
296
298
|
const result = compiled.regex.exec(pathname);
|
|
297
299
|
if (result === null) return void 0;
|
|
298
|
-
const params = {
|
|
299
|
-
for (let index = 0; index < compiled.params.length; index += 1) {
|
|
300
|
-
const name = compiled.params[index];
|
|
300
|
+
const params = compiled.params.entries().flatMap(([index, name]) => {
|
|
301
301
|
const value = result[index + 1];
|
|
302
|
-
if (name
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
if (name === void 0 || value === void 0) return [];
|
|
303
|
+
const decoded = decodeParam(value);
|
|
304
|
+
return name === "__proto__" ? [] : [[name, decoded]];
|
|
305
|
+
});
|
|
306
|
+
return Object.freeze(Object.fromEntries(params));
|
|
305
307
|
}
|
|
306
308
|
/**
|
|
307
309
|
* Classifies one path segment into its specificity tier — the same syntax
|
|
@@ -401,8 +403,8 @@ function computeSpecificity(path) {
|
|
|
401
403
|
function compareSpecificity(a, b) {
|
|
402
404
|
const left = computeSpecificity(a);
|
|
403
405
|
const right = computeSpecificity(b);
|
|
404
|
-
const
|
|
405
|
-
for (
|
|
406
|
+
const longest = left.length >= right.length ? left : right;
|
|
407
|
+
for (const [index] of longest.entries()) {
|
|
406
408
|
const tierA = left[index] ?? -1;
|
|
407
409
|
const tierB = right[index] ?? -1;
|
|
408
410
|
if (tierA !== tierB) return tierB - tierA;
|
|
@@ -480,8 +482,7 @@ function joinPaths(prefix, path) {
|
|
|
480
482
|
function defineRoute(input) {
|
|
481
483
|
return input;
|
|
482
484
|
}
|
|
483
|
-
|
|
484
|
-
//#region src/core/parsers.ts
|
|
485
|
+
// src/core/parsers.ts
|
|
485
486
|
/**
|
|
486
487
|
* Narrows a raw `request.method` string into a typed {@link Method} — total,
|
|
487
488
|
* never throws.
|
|
@@ -508,8 +509,7 @@ function defineRoute(input) {
|
|
|
508
509
|
function parseMethod(value) {
|
|
509
510
|
return METHOD_LIST.find((method) => method === value);
|
|
510
511
|
}
|
|
511
|
-
|
|
512
|
-
//#region src/core/Group.ts
|
|
512
|
+
// src/core/Group.ts
|
|
513
513
|
/**
|
|
514
514
|
* Represents a prefix-scoped registration handle over a {@link import('./Router.js').Router} —
|
|
515
515
|
* pure string composition, no independent state or storage.
|
|
@@ -549,8 +549,7 @@ var Group = class Group {
|
|
|
549
549
|
return new Group(this.#parent, joinPaths(this.prefix, prefix));
|
|
550
550
|
}
|
|
551
551
|
};
|
|
552
|
-
|
|
553
|
-
//#region src/core/Router.ts
|
|
552
|
+
// src/core/Router.ts
|
|
554
553
|
/**
|
|
555
554
|
* Represents the path-matching + registry engine — registers `{ path, meta, name? }`
|
|
556
555
|
* entries (compiling each path once) and resolves a concrete pathname to the most
|
|
@@ -599,37 +598,28 @@ var Router = class {
|
|
|
599
598
|
for (const entry of inputs) this.#register(entry);
|
|
600
599
|
}
|
|
601
600
|
match(pathname, answers) {
|
|
602
|
-
|
|
603
|
-
for (let index = 0; index < this.#entries.length; index += 1) {
|
|
604
|
-
const entry = this.#entries[index];
|
|
601
|
+
const best = this.#entries.entries().reduce((selected, [index, entry]) => {
|
|
605
602
|
const compiled = this.#compiled[index];
|
|
606
|
-
if (entry === void 0 || compiled === void 0)
|
|
607
|
-
if (answers !== void 0 && !answers(entry.meta))
|
|
603
|
+
if (entry === void 0 || compiled === void 0) return selected;
|
|
604
|
+
if (answers !== void 0 && !answers(entry.meta)) return selected;
|
|
608
605
|
const params = matchPath(compiled, pathname);
|
|
609
|
-
if (params === void 0)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
params
|
|
613
|
-
};
|
|
614
|
-
}
|
|
606
|
+
if (params === void 0) return selected;
|
|
607
|
+
return selected === void 0 || compareSpecificity(entry.path, selected[0].path) < 0 ? [entry, params] : selected;
|
|
608
|
+
}, void 0);
|
|
615
609
|
if (best === void 0) return void 0;
|
|
616
610
|
return {
|
|
617
|
-
path: best.
|
|
618
|
-
params: best
|
|
619
|
-
meta: best.
|
|
620
|
-
...best.
|
|
611
|
+
path: best[0].path,
|
|
612
|
+
params: best[1],
|
|
613
|
+
meta: best[0].meta,
|
|
614
|
+
...best[0].name === void 0 ? {} : { name: best[0].name }
|
|
621
615
|
};
|
|
622
616
|
}
|
|
623
617
|
entries(pathname) {
|
|
624
618
|
if (pathname === void 0) return [...this.#entries];
|
|
625
|
-
|
|
626
|
-
for (let index = 0; index < this.#entries.length; index += 1) {
|
|
627
|
-
const entry = this.#entries[index];
|
|
619
|
+
return this.#entries.entries().flatMap(([index, entry]) => {
|
|
628
620
|
const compiled = this.#compiled[index];
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
return out;
|
|
621
|
+
return entry !== void 0 && compiled !== void 0 && matchPath(compiled, pathname) !== void 0 ? [entry] : [];
|
|
622
|
+
}).toArray();
|
|
633
623
|
}
|
|
634
624
|
group(prefix) {
|
|
635
625
|
return new Group(this, prefix);
|
|
@@ -674,8 +664,7 @@ var Router = class {
|
|
|
674
664
|
this.#compiled.push(compiled);
|
|
675
665
|
}
|
|
676
666
|
};
|
|
677
|
-
|
|
678
|
-
//#region src/core/DispatchGroup.ts
|
|
667
|
+
// src/core/DispatchGroup.ts
|
|
679
668
|
/**
|
|
680
669
|
* Represents a prefix-scoped registration handle over a
|
|
681
670
|
* {@link import('./Dispatcher.js').Dispatcher} — the method-dimensioned counterpart of
|
|
@@ -717,8 +706,7 @@ var DispatchGroup = class DispatchGroup {
|
|
|
717
706
|
return new DispatchGroup(this.#parent, joinPaths(this.prefix, prefix));
|
|
718
707
|
}
|
|
719
708
|
};
|
|
720
|
-
|
|
721
|
-
//#region src/core/Dispatcher.ts
|
|
709
|
+
// src/core/Dispatcher.ts
|
|
722
710
|
/**
|
|
723
711
|
* Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP
|
|
724
712
|
* method dispatch and web-standard `Request`/`Response` handling over one internal
|
|
@@ -913,8 +901,7 @@ var Dispatcher = class {
|
|
|
913
901
|
});
|
|
914
902
|
}
|
|
915
903
|
};
|
|
916
|
-
|
|
917
|
-
//#region src/core/factories.ts
|
|
904
|
+
// src/core/factories.ts
|
|
918
905
|
/**
|
|
919
906
|
* Creates a {@link RouterInterface} — the pure path-matching + registry engine
|
|
920
907
|
* shared by the browser `Navigator` and the core `Dispatcher`.
|
|
@@ -982,7 +969,6 @@ function createRouter(options) {
|
|
|
982
969
|
function createDispatcher(options) {
|
|
983
970
|
return new Dispatcher(options);
|
|
984
971
|
}
|
|
985
|
-
//#endregion
|
|
986
972
|
exports.DispatchGroup = DispatchGroup;
|
|
987
973
|
exports.Dispatcher = Dispatcher;
|
|
988
974
|
exports.Group = Group;
|