@mindees/router 0.22.2 → 0.22.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.
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +22 -0
- package/dist/data.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +6 -2
- package/dist/router.js.map +1 -1
- package/package.json +3 -3
package/dist/data.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data.d.ts","names":[],"sources":["../src/data.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"data.d.ts","names":[],"sources":["../src/data.ts"],"mappings":";;;UAkBiB,aAAA;EAIP;EAFR,MAAA,EAAQ,MAAA;EAIE;EAFV,MAAA,EAAQ,MAAA;EAIA;EAFR,QAAA,EAAU,cAAA;EAES;EAAnB,MAAA,EAAQ,WAAA;AAAA;;KAIE,QAAA,IAAY,GAAA,EAAK,aAAA,eAA4B,OAAO;;;;;AAAA;AAQhE;KAAY,YAAA,IAAgB,IAAA;EAAQ,MAAA,EAAQ,MAAM;AAAA;;KAGtC,YAAA;;UAGK,UAAA;EANoD;EAQnE,MAAA,EAAQ,YAAA;EALc;EAOtB,IAAA,GAAO,CAAC;EAPc;EAStB,KAAA;AAAA"}
|
package/dist/data.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import { RouterError } from "./errors.js";
|
|
1
2
|
//#region src/data.ts
|
|
3
|
+
/**
|
|
4
|
+
* Loaders + data — a stale-while-revalidate (SWR) cache for route loaders.
|
|
5
|
+
*
|
|
6
|
+
* A route may declare a `loader`. The manager runs loaders for the matched
|
|
7
|
+
* chain, caches results (keyed by route identity + params + `loaderDeps`),
|
|
8
|
+
* serves stale data while revalidating, aborts superseded loads via
|
|
9
|
+
* `AbortSignal`, and exposes results **reactively** so a component's data binding
|
|
10
|
+
* updates when its load resolves — without re-mounting the component. See
|
|
11
|
+
* ADR-0005.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
2
15
|
const IDLE = Object.freeze({ status: "idle" });
|
|
3
16
|
/** Create a loader manager. */
|
|
4
17
|
function createLoaderManager(options) {
|
|
@@ -47,6 +60,15 @@ function createLoaderManager(options) {
|
|
|
47
60
|
const route = match.route;
|
|
48
61
|
const loader = route.loader;
|
|
49
62
|
if (!loader) return;
|
|
63
|
+
if (route.searchSchema && match.issues && match.issues.length > 0) {
|
|
64
|
+
setEntry(route, innerKey(route, match), {
|
|
65
|
+
status: "error",
|
|
66
|
+
error: new RouterError("VALIDATE_SEARCH", "Search params failed validation", match.issues),
|
|
67
|
+
loadedAt: now()
|
|
68
|
+
});
|
|
69
|
+
options.onChange();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
50
72
|
const key = innerKey(route, match);
|
|
51
73
|
const gkey = `${idOf(route)}:${key}`;
|
|
52
74
|
const existing = getEntry(route, key);
|
package/dist/data.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data.js","names":[],"sources":["../src/data.ts"],"sourcesContent":["/**\n * Loaders + data — a stale-while-revalidate (SWR) cache for route loaders.\n *\n * A route may declare a `loader`. The manager runs loaders for the matched\n * chain, caches results (keyed by route identity + params + `loaderDeps`),\n * serves stale data while revalidating, aborts superseded loads via\n * `AbortSignal`, and exposes results **reactively** so a component's data binding\n * updates when its load resolves — without re-mounting the component. See\n * ADR-0005.\n *\n * @module\n */\n\nimport type { RouterLocation } from './history'\nimport type { RouteMatch, RouteRecord } from './router'\n\n/** Context passed to a route `loader`. */\nexport interface LoaderContext {\n /** The matched path params. */\n params: Record<string, string>\n /** The (validated) search params. */\n search: Record<string, unknown>\n /** The current location. */\n location: RouterLocation\n /** Aborted when this load is superseded (navigated away, or re-keyed). */\n signal: AbortSignal\n}\n\n/** A route data loader. May be sync or async. */\nexport type LoaderFn = (ctx: LoaderContext) => unknown | Promise<unknown>\n\n/**\n * Declares which inputs key a route's loader cache (e.g. specific search params).\n * The returned value keys the SWR cache via `JSON.stringify`, so it must be\n * JSON-serializable (and ideally have stable property order). A non-serializable\n * value degrades to a params-only cache key rather than throwing.\n */\nexport type LoaderDepsFn = (args: { search: Record<string, unknown> }) => unknown\n\n/** Loader lifecycle status. */\nexport type LoaderStatus = 'idle' | 'pending' | 'success' | 'error'\n\n/** The reactive state of a route's loader. */\nexport interface LoaderData<T = unknown> {\n /** `idle` when the route has no loader or hasn't started. */\n status: LoaderStatus\n /** The loaded value (also present while `pending` if a stale value exists). */\n data?: T\n /** The error, when `status === 'error'`. */\n error?: unknown\n}\n\nconst IDLE: LoaderData = Object.freeze({ status: 'idle' })\n\ninterface CacheEntry {\n status: LoaderStatus\n data?: unknown\n error?: unknown\n loadedAt: number\n controller?: AbortController\n}\n\n/** Options for {@link createLoaderManager}. */\nexport interface LoaderManagerOptions {\n /** The current location (for the loader context). */\n location: () => RouterLocation\n /** Called whenever cached data changes (bump the reactive data version). */\n onChange: () => void\n /** Subscribe the current reactive scope to data changes (read the version signal). */\n track: () => void\n /** Wall-clock now (injectable for tests). Defaults to `Date.now`. */\n now?: () => number\n}\n\n/** Manages route loaders + their SWR cache. */\nexport interface LoaderManager {\n /** Ensure loads for the matched chain and abort loads for routes no longer matched. */\n sync(matches: readonly RouteMatch[]): void\n /** Ensure loads for `matches` WITHOUT aborting others (used by `preload`). */\n preload(matches: readonly RouteMatch[]): void\n /** Reactively read a match's loader state. */\n read(match: RouteMatch): LoaderData\n /** Mark the given chain's entries stale and reload them. */\n invalidate(matches: readonly RouteMatch[]): void\n /** Abort all in-flight loads. */\n dispose(): void\n}\n\n/** Create a loader manager. */\nexport function createLoaderManager(options: LoaderManagerOptions): LoaderManager {\n const now = options.now ?? Date.now\n // Per-route SWR cache. The outer WeakMap lets a route's entries be reclaimed\n // when its RouteRecord is dropped; the inner Map is bounded (see setEntry) so a\n // high-cardinality dynamic route (e.g. `/posts/:id` visited thousands of times)\n // can't grow it without limit. `let` so dispose() can drop it wholesale.\n let cache = new WeakMap<RouteRecord, Map<string, CacheEntry>>()\n const inFlight = new Map<string, AbortController>()\n const ids = new WeakMap<RouteRecord, number>()\n let nextId = 0\n let disposed = false\n\n // Max distinct (params, loaderDeps) entries retained per route before the oldest\n // non-pending entries are evicted (LRU by last write). Bounds memory growth.\n const MAX_ENTRIES_PER_ROUTE = 64\n\n const idOf = (route: RouteRecord): number => {\n let id = ids.get(route)\n if (id === undefined) {\n id = nextId++\n ids.set(route, id)\n }\n return id\n }\n\n const innerKey = (route: RouteRecord, match: RouteMatch): string => {\n try {\n const deps = route.loaderDeps ? route.loaderDeps({ search: match.search }) : null\n return JSON.stringify({ p: match.params, d: deps })\n } catch {\n // A throwing or non-serializable loaderDeps (BigInt/circular): degrade to a\n // params-only key rather than throwing out of navigation. `params` is\n // always a Record<string, string>, so this is total. (loaderDeps should be\n // pure and JSON-serializable — see LoaderDepsFn.) innerKey is also called\n // from currentGlobalKeys/read/invalidate, so it must never throw.\n return `${JSON.stringify({ p: match.params })}::unserializable-deps`\n }\n }\n\n const getEntry = (route: RouteRecord, key: string): CacheEntry | undefined =>\n cache.get(route)?.get(key)\n\n const setEntry = (route: RouteRecord, key: string, entry: CacheEntry): void => {\n let m = cache.get(route)\n if (!m) {\n m = new Map()\n cache.set(route, m)\n }\n // Re-insert so Map iteration order is least-recently-written first.\n m.delete(key)\n m.set(key, entry)\n // Bound the cache: evict the oldest entries that aren't currently loading.\n if (m.size > MAX_ENTRIES_PER_ROUTE) {\n for (const [k, e] of m) {\n if (m.size <= MAX_ENTRIES_PER_ROUTE) break\n if (e.status !== 'pending') m.delete(k)\n }\n }\n }\n\n const ensure = (match: RouteMatch): void => {\n if (disposed) return\n const route = match.route\n const loader = route.loader\n if (!loader) return\n\n const key = innerKey(route, match)\n const gkey = `${idOf(route)}:${key}`\n const existing = getEntry(route, key)\n const staleTime = route.staleTime ?? 0\n\n if (existing?.status === 'success' && now() - existing.loadedAt < staleTime) return\n if (inFlight.has(gkey)) return // a load for this exact key is already running\n\n const controller = new AbortController()\n inFlight.set(gkey, controller)\n // Keep any prior data visible while revalidating (stale-while-revalidate).\n const pendingEntry: CacheEntry = {\n status: 'pending',\n loadedAt: existing?.loadedAt ?? 0,\n controller,\n }\n if (existing?.data !== undefined) pendingEntry.data = existing.data\n setEntry(route, key, pendingEntry)\n options.onChange()\n\n const ctx: LoaderContext = {\n params: match.params,\n search: match.search,\n location: options.location(),\n signal: controller.signal,\n }\n\n // Settle the load. Even an aborted load (e.g. a preload superseded by a\n // navigation) still warms the cache for the route's next visit — but only if\n // our pending entry hasn't already been replaced by a newer load, and we\n // notify observers only when we weren't aborted. This also guarantees an\n // aborted preload never leaves a permanently `pending` cache entry.\n const settle = (settled: CacheEntry): void => {\n if (inFlight.get(gkey) === controller) inFlight.delete(gkey)\n if (getEntry(route, key) !== pendingEntry) return // a newer load superseded us\n setEntry(route, key, settled)\n if (!controller.signal.aborted) options.onChange()\n }\n\n Promise.resolve()\n .then(() => loader(ctx))\n .then(\n (data) => settle({ status: 'success', data, loadedAt: now() }),\n (error: unknown) => {\n const errored: CacheEntry = { status: 'error', error, loadedAt: now() }\n if (existing?.data !== undefined) errored.data = existing.data\n settle(errored)\n },\n )\n }\n\n const currentGlobalKeys = (matches: readonly RouteMatch[]): Set<string> => {\n const keys = new Set<string>()\n for (const m of matches) {\n if (m.route.loader) keys.add(`${idOf(m.route)}:${innerKey(m.route, m)}`)\n }\n return keys\n }\n\n // Run ensure() for one match, isolating failures so a single route can never\n // abort the whole pass (and so nothing escapes navigation).\n const ensureSafe = (match: RouteMatch): void => {\n try {\n ensure(match)\n } catch {\n // A route's key/loader-start failure must not break sibling/child loads.\n }\n }\n\n return {\n sync(matches) {\n for (const m of matches) ensureSafe(m)\n // Abort in-flight loads for routes no longer in the matched chain.\n const keep = currentGlobalKeys(matches)\n for (const [gkey, controller] of inFlight) {\n if (!keep.has(gkey)) {\n controller.abort()\n inFlight.delete(gkey)\n }\n }\n },\n preload(matches) {\n for (const m of matches) ensureSafe(m)\n },\n read(match) {\n options.track()\n const route = match.route\n if (!route.loader) return IDLE\n const entry = getEntry(route, innerKey(route, match))\n if (!entry) return IDLE\n const out: LoaderData = { status: entry.status }\n if (entry.data !== undefined) out.data = entry.data\n if (entry.error !== undefined) out.error = entry.error\n return out\n },\n invalidate(matches) {\n for (const m of matches) {\n if (!m.route.loader) continue\n const key = innerKey(m.route, m)\n const entry = getEntry(m.route, key)\n if (entry) entry.loadedAt = 0 // force stale\n // If a load is already in flight for this key, abort it so the sync below\n // starts a FRESH load — otherwise ensure() bails on the in-flight guard\n // and the pre-invalidate (possibly stale) result is served, with the\n // staleness mark silently overwritten when it resolves.\n const gkey = `${idOf(m.route)}:${key}`\n const controller = inFlight.get(gkey)\n if (controller) {\n controller.abort()\n inFlight.delete(gkey)\n }\n }\n this.sync(matches)\n },\n dispose() {\n disposed = true\n for (const controller of inFlight.values()) controller.abort()\n inFlight.clear()\n // Drop all cached loader data so a disposed (but still-referenced) manager\n // doesn't retain payloads; a fresh WeakMap lets the inner Maps be GC'd.\n cache = new WeakMap()\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"data.js","names":[],"sources":["../src/data.ts"],"sourcesContent":["/**\n * Loaders + data — a stale-while-revalidate (SWR) cache for route loaders.\n *\n * A route may declare a `loader`. The manager runs loaders for the matched\n * chain, caches results (keyed by route identity + params + `loaderDeps`),\n * serves stale data while revalidating, aborts superseded loads via\n * `AbortSignal`, and exposes results **reactively** so a component's data binding\n * updates when its load resolves — without re-mounting the component. See\n * ADR-0005.\n *\n * @module\n */\n\nimport { RouterError } from './errors'\nimport type { RouterLocation } from './history'\nimport type { RouteMatch, RouteRecord } from './router'\n\n/** Context passed to a route `loader`. */\nexport interface LoaderContext {\n /** The matched path params. */\n params: Record<string, string>\n /** The (validated) search params. */\n search: Record<string, unknown>\n /** The current location. */\n location: RouterLocation\n /** Aborted when this load is superseded (navigated away, or re-keyed). */\n signal: AbortSignal\n}\n\n/** A route data loader. May be sync or async. */\nexport type LoaderFn = (ctx: LoaderContext) => unknown | Promise<unknown>\n\n/**\n * Declares which inputs key a route's loader cache (e.g. specific search params).\n * The returned value keys the SWR cache via `JSON.stringify`, so it must be\n * JSON-serializable (and ideally have stable property order). A non-serializable\n * value degrades to a params-only cache key rather than throwing.\n */\nexport type LoaderDepsFn = (args: { search: Record<string, unknown> }) => unknown\n\n/** Loader lifecycle status. */\nexport type LoaderStatus = 'idle' | 'pending' | 'success' | 'error'\n\n/** The reactive state of a route's loader. */\nexport interface LoaderData<T = unknown> {\n /** `idle` when the route has no loader or hasn't started. */\n status: LoaderStatus\n /** The loaded value (also present while `pending` if a stale value exists). */\n data?: T\n /** The error, when `status === 'error'`. */\n error?: unknown\n}\n\nconst IDLE: LoaderData = Object.freeze({ status: 'idle' })\n\ninterface CacheEntry {\n status: LoaderStatus\n data?: unknown\n error?: unknown\n loadedAt: number\n controller?: AbortController\n}\n\n/** Options for {@link createLoaderManager}. */\nexport interface LoaderManagerOptions {\n /** The current location (for the loader context). */\n location: () => RouterLocation\n /** Called whenever cached data changes (bump the reactive data version). */\n onChange: () => void\n /** Subscribe the current reactive scope to data changes (read the version signal). */\n track: () => void\n /** Wall-clock now (injectable for tests). Defaults to `Date.now`. */\n now?: () => number\n}\n\n/** Manages route loaders + their SWR cache. */\nexport interface LoaderManager {\n /** Ensure loads for the matched chain and abort loads for routes no longer matched. */\n sync(matches: readonly RouteMatch[]): void\n /** Ensure loads for `matches` WITHOUT aborting others (used by `preload`). */\n preload(matches: readonly RouteMatch[]): void\n /** Reactively read a match's loader state. */\n read(match: RouteMatch): LoaderData\n /** Mark the given chain's entries stale and reload them. */\n invalidate(matches: readonly RouteMatch[]): void\n /** Abort all in-flight loads. */\n dispose(): void\n}\n\n/** Create a loader manager. */\nexport function createLoaderManager(options: LoaderManagerOptions): LoaderManager {\n const now = options.now ?? Date.now\n // Per-route SWR cache. The outer WeakMap lets a route's entries be reclaimed\n // when its RouteRecord is dropped; the inner Map is bounded (see setEntry) so a\n // high-cardinality dynamic route (e.g. `/posts/:id` visited thousands of times)\n // can't grow it without limit. `let` so dispose() can drop it wholesale.\n let cache = new WeakMap<RouteRecord, Map<string, CacheEntry>>()\n const inFlight = new Map<string, AbortController>()\n const ids = new WeakMap<RouteRecord, number>()\n let nextId = 0\n let disposed = false\n\n // Max distinct (params, loaderDeps) entries retained per route before the oldest\n // non-pending entries are evicted (LRU by last write). Bounds memory growth.\n const MAX_ENTRIES_PER_ROUTE = 64\n\n const idOf = (route: RouteRecord): number => {\n let id = ids.get(route)\n if (id === undefined) {\n id = nextId++\n ids.set(route, id)\n }\n return id\n }\n\n const innerKey = (route: RouteRecord, match: RouteMatch): string => {\n try {\n const deps = route.loaderDeps ? route.loaderDeps({ search: match.search }) : null\n return JSON.stringify({ p: match.params, d: deps })\n } catch {\n // A throwing or non-serializable loaderDeps (BigInt/circular): degrade to a\n // params-only key rather than throwing out of navigation. `params` is\n // always a Record<string, string>, so this is total. (loaderDeps should be\n // pure and JSON-serializable — see LoaderDepsFn.) innerKey is also called\n // from currentGlobalKeys/read/invalidate, so it must never throw.\n return `${JSON.stringify({ p: match.params })}::unserializable-deps`\n }\n }\n\n const getEntry = (route: RouteRecord, key: string): CacheEntry | undefined =>\n cache.get(route)?.get(key)\n\n const setEntry = (route: RouteRecord, key: string, entry: CacheEntry): void => {\n let m = cache.get(route)\n if (!m) {\n m = new Map()\n cache.set(route, m)\n }\n // Re-insert so Map iteration order is least-recently-written first.\n m.delete(key)\n m.set(key, entry)\n // Bound the cache: evict the oldest entries that aren't currently loading.\n if (m.size > MAX_ENTRIES_PER_ROUTE) {\n for (const [k, e] of m) {\n if (m.size <= MAX_ENTRIES_PER_ROUTE) break\n if (e.status !== 'pending') m.delete(k)\n }\n }\n }\n\n const ensure = (match: RouteMatch): void => {\n if (disposed) return\n const route = match.route\n const loader = route.loader\n if (!loader) return\n\n // If THIS route declared a `searchSchema` and the URL's search FAILED to validate, never run its\n // loader on the raw, unvalidated (attacker-controlled) search — `LoaderContext.search` is the\n // \"validated\" value by contract. Surface the validation error deterministically instead. (`issues`\n // ride on every chain match, so gate on `route.searchSchema` to skip only schema-declaring routes.)\n if (route.searchSchema && match.issues && match.issues.length > 0) {\n setEntry(route, innerKey(route, match), {\n status: 'error',\n error: new RouterError('VALIDATE_SEARCH', 'Search params failed validation', match.issues),\n loadedAt: now(),\n })\n options.onChange()\n return\n }\n\n const key = innerKey(route, match)\n const gkey = `${idOf(route)}:${key}`\n const existing = getEntry(route, key)\n const staleTime = route.staleTime ?? 0\n\n if (existing?.status === 'success' && now() - existing.loadedAt < staleTime) return\n if (inFlight.has(gkey)) return // a load for this exact key is already running\n\n const controller = new AbortController()\n inFlight.set(gkey, controller)\n // Keep any prior data visible while revalidating (stale-while-revalidate).\n const pendingEntry: CacheEntry = {\n status: 'pending',\n loadedAt: existing?.loadedAt ?? 0,\n controller,\n }\n if (existing?.data !== undefined) pendingEntry.data = existing.data\n setEntry(route, key, pendingEntry)\n options.onChange()\n\n const ctx: LoaderContext = {\n params: match.params,\n search: match.search,\n location: options.location(),\n signal: controller.signal,\n }\n\n // Settle the load. Even an aborted load (e.g. a preload superseded by a\n // navigation) still warms the cache for the route's next visit — but only if\n // our pending entry hasn't already been replaced by a newer load, and we\n // notify observers only when we weren't aborted. This also guarantees an\n // aborted preload never leaves a permanently `pending` cache entry.\n const settle = (settled: CacheEntry): void => {\n if (inFlight.get(gkey) === controller) inFlight.delete(gkey)\n if (getEntry(route, key) !== pendingEntry) return // a newer load superseded us\n setEntry(route, key, settled)\n if (!controller.signal.aborted) options.onChange()\n }\n\n Promise.resolve()\n .then(() => loader(ctx))\n .then(\n (data) => settle({ status: 'success', data, loadedAt: now() }),\n (error: unknown) => {\n const errored: CacheEntry = { status: 'error', error, loadedAt: now() }\n if (existing?.data !== undefined) errored.data = existing.data\n settle(errored)\n },\n )\n }\n\n const currentGlobalKeys = (matches: readonly RouteMatch[]): Set<string> => {\n const keys = new Set<string>()\n for (const m of matches) {\n if (m.route.loader) keys.add(`${idOf(m.route)}:${innerKey(m.route, m)}`)\n }\n return keys\n }\n\n // Run ensure() for one match, isolating failures so a single route can never\n // abort the whole pass (and so nothing escapes navigation).\n const ensureSafe = (match: RouteMatch): void => {\n try {\n ensure(match)\n } catch {\n // A route's key/loader-start failure must not break sibling/child loads.\n }\n }\n\n return {\n sync(matches) {\n for (const m of matches) ensureSafe(m)\n // Abort in-flight loads for routes no longer in the matched chain.\n const keep = currentGlobalKeys(matches)\n for (const [gkey, controller] of inFlight) {\n if (!keep.has(gkey)) {\n controller.abort()\n inFlight.delete(gkey)\n }\n }\n },\n preload(matches) {\n for (const m of matches) ensureSafe(m)\n },\n read(match) {\n options.track()\n const route = match.route\n if (!route.loader) return IDLE\n const entry = getEntry(route, innerKey(route, match))\n if (!entry) return IDLE\n const out: LoaderData = { status: entry.status }\n if (entry.data !== undefined) out.data = entry.data\n if (entry.error !== undefined) out.error = entry.error\n return out\n },\n invalidate(matches) {\n for (const m of matches) {\n if (!m.route.loader) continue\n const key = innerKey(m.route, m)\n const entry = getEntry(m.route, key)\n if (entry) entry.loadedAt = 0 // force stale\n // If a load is already in flight for this key, abort it so the sync below\n // starts a FRESH load — otherwise ensure() bails on the in-flight guard\n // and the pre-invalidate (possibly stale) result is served, with the\n // staleness mark silently overwritten when it resolves.\n const gkey = `${idOf(m.route)}:${key}`\n const controller = inFlight.get(gkey)\n if (controller) {\n controller.abort()\n inFlight.delete(gkey)\n }\n }\n this.sync(matches)\n },\n dispose() {\n disposed = true\n for (const controller of inFlight.values()) controller.abort()\n inFlight.clear()\n // Drop all cached loader data so a disposed (but still-referenced) manager\n // doesn't retain payloads; a fresh WeakMap lets the inner Maps be GC'd.\n cache = new WeakMap()\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAqDA,MAAM,OAAmB,OAAO,OAAO,EAAE,QAAQ,OAAO,CAAC;;AAqCzD,SAAgB,oBAAoB,SAA8C;CAChF,MAAM,MAAM,QAAQ,OAAO,KAAK;CAKhC,IAAI,wBAAQ,IAAI,QAA8C;CAC9D,MAAM,2BAAW,IAAI,IAA6B;CAClD,MAAM,sBAAM,IAAI,QAA6B;CAC7C,IAAI,SAAS;CACb,IAAI,WAAW;CAIf,MAAM,wBAAwB;CAE9B,MAAM,QAAQ,UAA+B;EAC3C,IAAI,KAAK,IAAI,IAAI,KAAK;EACtB,IAAI,OAAO,KAAA,GAAW;GACpB,KAAK;GACL,IAAI,IAAI,OAAO,EAAE;EACnB;EACA,OAAO;CACT;CAEA,MAAM,YAAY,OAAoB,UAA8B;EAClE,IAAI;GACF,MAAM,OAAO,MAAM,aAAa,MAAM,WAAW,EAAE,QAAQ,MAAM,OAAO,CAAC,IAAI;GAC7E,OAAO,KAAK,UAAU;IAAE,GAAG,MAAM;IAAQ,GAAG;GAAK,CAAC;EACpD,QAAQ;GAMN,OAAO,GAAG,KAAK,UAAU,EAAE,GAAG,MAAM,OAAO,CAAC,EAAE;EAChD;CACF;CAEA,MAAM,YAAY,OAAoB,QACpC,MAAM,IAAI,KAAK,GAAG,IAAI,GAAG;CAE3B,MAAM,YAAY,OAAoB,KAAa,UAA4B;EAC7E,IAAI,IAAI,MAAM,IAAI,KAAK;EACvB,IAAI,CAAC,GAAG;GACN,oBAAI,IAAI,IAAI;GACZ,MAAM,IAAI,OAAO,CAAC;EACpB;EAEA,EAAE,OAAO,GAAG;EACZ,EAAE,IAAI,KAAK,KAAK;EAEhB,IAAI,EAAE,OAAO,uBACX,KAAK,MAAM,CAAC,GAAG,MAAM,GAAG;GACtB,IAAI,EAAE,QAAQ,uBAAuB;GACrC,IAAI,EAAE,WAAW,WAAW,EAAE,OAAO,CAAC;EACxC;CAEJ;CAEA,MAAM,UAAU,UAA4B;EAC1C,IAAI,UAAU;EACd,MAAM,QAAQ,MAAM;EACpB,MAAM,SAAS,MAAM;EACrB,IAAI,CAAC,QAAQ;EAMb,IAAI,MAAM,gBAAgB,MAAM,UAAU,MAAM,OAAO,SAAS,GAAG;GACjE,SAAS,OAAO,SAAS,OAAO,KAAK,GAAG;IACtC,QAAQ;IACR,OAAO,IAAI,YAAY,mBAAmB,mCAAmC,MAAM,MAAM;IACzF,UAAU,IAAI;GAChB,CAAC;GACD,QAAQ,SAAS;GACjB;EACF;EAEA,MAAM,MAAM,SAAS,OAAO,KAAK;EACjC,MAAM,OAAO,GAAG,KAAK,KAAK,EAAE,GAAG;EAC/B,MAAM,WAAW,SAAS,OAAO,GAAG;EACpC,MAAM,YAAY,MAAM,aAAa;EAErC,IAAI,UAAU,WAAW,aAAa,IAAI,IAAI,SAAS,WAAW,WAAW;EAC7E,IAAI,SAAS,IAAI,IAAI,GAAG;EAExB,MAAM,aAAa,IAAI,gBAAgB;EACvC,SAAS,IAAI,MAAM,UAAU;EAE7B,MAAM,eAA2B;GAC/B,QAAQ;GACR,UAAU,UAAU,YAAY;GAChC;EACF;EACA,IAAI,UAAU,SAAS,KAAA,GAAW,aAAa,OAAO,SAAS;EAC/D,SAAS,OAAO,KAAK,YAAY;EACjC,QAAQ,SAAS;EAEjB,MAAM,MAAqB;GACzB,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,UAAU,QAAQ,SAAS;GAC3B,QAAQ,WAAW;EACrB;EAOA,MAAM,UAAU,YAA8B;GAC5C,IAAI,SAAS,IAAI,IAAI,MAAM,YAAY,SAAS,OAAO,IAAI;GAC3D,IAAI,SAAS,OAAO,GAAG,MAAM,cAAc;GAC3C,SAAS,OAAO,KAAK,OAAO;GAC5B,IAAI,CAAC,WAAW,OAAO,SAAS,QAAQ,SAAS;EACnD;EAEA,QAAQ,QAAQ,EACb,WAAW,OAAO,GAAG,CAAC,EACtB,MACE,SAAS,OAAO;GAAE,QAAQ;GAAW;GAAM,UAAU,IAAI;EAAE,CAAC,IAC5D,UAAmB;GAClB,MAAM,UAAsB;IAAE,QAAQ;IAAS;IAAO,UAAU,IAAI;GAAE;GACtE,IAAI,UAAU,SAAS,KAAA,GAAW,QAAQ,OAAO,SAAS;GAC1D,OAAO,OAAO;EAChB,CACF;CACJ;CAEA,MAAM,qBAAqB,YAAgD;EACzE,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,KAAK,SACd,IAAI,EAAE,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,OAAO,CAAC,GAAG;EAEzE,OAAO;CACT;CAIA,MAAM,cAAc,UAA4B;EAC9C,IAAI;GACF,OAAO,KAAK;EACd,QAAQ,CAER;CACF;CAEA,OAAO;EACL,KAAK,SAAS;GACZ,KAAK,MAAM,KAAK,SAAS,WAAW,CAAC;GAErC,MAAM,OAAO,kBAAkB,OAAO;GACtC,KAAK,MAAM,CAAC,MAAM,eAAe,UAC/B,IAAI,CAAC,KAAK,IAAI,IAAI,GAAG;IACnB,WAAW,MAAM;IACjB,SAAS,OAAO,IAAI;GACtB;EAEJ;EACA,QAAQ,SAAS;GACf,KAAK,MAAM,KAAK,SAAS,WAAW,CAAC;EACvC;EACA,KAAK,OAAO;GACV,QAAQ,MAAM;GACd,MAAM,QAAQ,MAAM;GACpB,IAAI,CAAC,MAAM,QAAQ,OAAO;GAC1B,MAAM,QAAQ,SAAS,OAAO,SAAS,OAAO,KAAK,CAAC;GACpD,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,MAAkB,EAAE,QAAQ,MAAM,OAAO;GAC/C,IAAI,MAAM,SAAS,KAAA,GAAW,IAAI,OAAO,MAAM;GAC/C,IAAI,MAAM,UAAU,KAAA,GAAW,IAAI,QAAQ,MAAM;GACjD,OAAO;EACT;EACA,WAAW,SAAS;GAClB,KAAK,MAAM,KAAK,SAAS;IACvB,IAAI,CAAC,EAAE,MAAM,QAAQ;IACrB,MAAM,MAAM,SAAS,EAAE,OAAO,CAAC;IAC/B,MAAM,QAAQ,SAAS,EAAE,OAAO,GAAG;IACnC,IAAI,OAAO,MAAM,WAAW;IAK5B,MAAM,OAAO,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG;IACjC,MAAM,aAAa,SAAS,IAAI,IAAI;IACpC,IAAI,YAAY;KACd,WAAW,MAAM;KACjB,SAAS,OAAO,IAAI;IACtB;GACF;GACA,KAAK,KAAK,OAAO;EACnB;EACA,UAAU;GACR,WAAW;GACX,KAAK,MAAM,cAAc,SAAS,OAAO,GAAG,WAAW,MAAM;GAC7D,SAAS,MAAM;GAGf,wBAAQ,IAAI,QAAQ;EACtB;CACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
|
|
|
14
14
|
/** The npm package name. */
|
|
15
15
|
declare const name = "@mindees/router";
|
|
16
16
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
17
|
-
declare const VERSION = "0.22.
|
|
17
|
+
declare const VERSION = "0.22.4";
|
|
18
18
|
/**
|
|
19
19
|
* Current maturity. Router I (typed params, Standard-Schema search, history, the
|
|
20
20
|
* signals-native router, selector-isolated state, typed + relative navigation)
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
|
11
11
|
/** The npm package name. */
|
|
12
12
|
const name = "@mindees/router";
|
|
13
13
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
-
const VERSION = "0.22.
|
|
14
|
+
const VERSION = "0.22.4";
|
|
15
15
|
/**
|
|
16
16
|
* Current maturity. Router I (typed params, Standard-Schema search, history, the
|
|
17
17
|
* signals-native router, selector-isolated state, typed + relative navigation)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/router` — **Quantum**, the typed router for MindeesNative.\n *\n * Router I (Phase 6): codegen-free typed path params ({@link PathParams}),\n * Standard-Schema validated search params, a signals-native router with typed +\n * relative navigation and selector-isolated state, and an injectable history\n * (memory + browser). See ADR-0003.\n *\n * Router II (Phase 7): render integration — {@link createRouterView} (nested,\n * fine-grained, layout-preserving) and typed {@link createLink} — plus SWR data\n * loaders (with `AbortSignal`, {@link Router.invalidate}/{@link Router.preload}),\n * navigation guards ({@link BeforeNavigate} cancel/redirect + idempotent\n * navigation), and web view transitions. See ADR-0004 and ADR-0005.\n *\n * Still a later phase (not exported): the global typed route registry and\n * file-based route scanning + bundler plugin. See `STATUS.md`.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Render integration: nested view + typed links (Router II). */\nexport {\n createLink,\n createRouterView,\n type LinkComponent,\n type LinkOptions,\n type LinkProps,\n type PrefetchMode,\n type RouterViewOptions,\n} from './components'\n/** Loaders + data (SWR). */\nexport type {\n LoaderContext,\n LoaderData,\n LoaderDepsFn,\n LoaderFn,\n LoaderStatus,\n} from './data'\n/** Errors. */\nexport { RouterError, type RouterErrorCode } from './errors'\n/** File-based routing: a module map → a router (Expo-style conventions). */\nexport { createFileRouter, type RouteModule, routesFromModules } from './file-routes'\n/** History capability. */\nexport {\n createBrowserHistory,\n createHref,\n createMemoryHistory,\n type HistoryListener,\n type MemoryHistoryOptions,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\n/** Ergonomic hooks + a bound Link that resolve the active router. */\nexport { Link, useParams, usePathname, useRouter, useSearch } from './hooks'\n/** Route patterns + codegen-free typed params. */\nexport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\n/** Router. */\nexport {\n type BeforeNavigate,\n type CreateRouterOptions,\n createRouter,\n type NavigateOptions,\n type NavTarget,\n type RouteComponentProps,\n type RouteMatch,\n type RouteRecord,\n type Router,\n type RouterState,\n resolvePath,\n} from './router'\n/** Search (query) params. */\nexport {\n parseQuery,\n type QueryValue,\n safeValidateSearch,\n stringifyQuery,\n type ValidationResult,\n validateSearch,\n} from './search'\n/** Standard Schema — the validator-agnostic interface (vendored, types only). */\nexport type { StandardSchemaV1 } from './standard-schema'\n\n/** The npm package name. */\nexport const name = '@mindees/router'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/router` — **Quantum**, the typed router for MindeesNative.\n *\n * Router I (Phase 6): codegen-free typed path params ({@link PathParams}),\n * Standard-Schema validated search params, a signals-native router with typed +\n * relative navigation and selector-isolated state, and an injectable history\n * (memory + browser). See ADR-0003.\n *\n * Router II (Phase 7): render integration — {@link createRouterView} (nested,\n * fine-grained, layout-preserving) and typed {@link createLink} — plus SWR data\n * loaders (with `AbortSignal`, {@link Router.invalidate}/{@link Router.preload}),\n * navigation guards ({@link BeforeNavigate} cancel/redirect + idempotent\n * navigation), and web view transitions. See ADR-0004 and ADR-0005.\n *\n * Still a later phase (not exported): the global typed route registry and\n * file-based route scanning + bundler plugin. See `STATUS.md`.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Render integration: nested view + typed links (Router II). */\nexport {\n createLink,\n createRouterView,\n type LinkComponent,\n type LinkOptions,\n type LinkProps,\n type PrefetchMode,\n type RouterViewOptions,\n} from './components'\n/** Loaders + data (SWR). */\nexport type {\n LoaderContext,\n LoaderData,\n LoaderDepsFn,\n LoaderFn,\n LoaderStatus,\n} from './data'\n/** Errors. */\nexport { RouterError, type RouterErrorCode } from './errors'\n/** File-based routing: a module map → a router (Expo-style conventions). */\nexport { createFileRouter, type RouteModule, routesFromModules } from './file-routes'\n/** History capability. */\nexport {\n createBrowserHistory,\n createHref,\n createMemoryHistory,\n type HistoryListener,\n type MemoryHistoryOptions,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\n/** Ergonomic hooks + a bound Link that resolve the active router. */\nexport { Link, useParams, usePathname, useRouter, useSearch } from './hooks'\n/** Route patterns + codegen-free typed params. */\nexport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\n/** Router. */\nexport {\n type BeforeNavigate,\n type CreateRouterOptions,\n createRouter,\n type NavigateOptions,\n type NavTarget,\n type RouteComponentProps,\n type RouteMatch,\n type RouteRecord,\n type Router,\n type RouterState,\n resolvePath,\n} from './router'\n/** Search (query) params. */\nexport {\n parseQuery,\n type QueryValue,\n safeValidateSearch,\n stringifyQuery,\n type ValidationResult,\n validateSearch,\n} from './search'\n/** Standard Schema — the validator-agnostic interface (vendored, types only). */\nexport type { StandardSchemaV1 } from './standard-schema'\n\n/** The npm package name. */\nexport const name = '@mindees/router'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.4'\n\n/**\n * Current maturity. Router I (typed params, Standard-Schema search, history, the\n * signals-native router, selector-isolated state, typed + relative navigation)\n * and Router II (nested rendering, typed links, SWR loaders, navigation guards,\n * view transitions) are implemented and tested. The global typed route registry\n * and file-based route scanning are a later phase — see `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/** Static identity + maturity metadata for this package. */\nexport const info: PackageInfo = { name, version: VERSION, maturity }\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;AA8FA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;;AASvB,MAAa,WAAqB;;AAGlC,MAAa,OAAoB;CAAE;CAAM,SAAS;CAAS;AAAS"}
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;UA4DiB,mBAAA;EAUf;EARA,MAAA,EAAQ,MAAA;EAQa;EANrB,MAAA,QAAc,MAAA;EAUC;EARf,MAAA,QAAc,MAAA;;EAEd,IAAA,QAAY,UAAA;EAcA;EAZZ,QAAA,EAAU,WAAA;AAAA;;UAIK,WAAA;EA2BK;;;;;EArBpB,IAAA;EAEA;EAAA,SAAA,GAAY,SAAA,CAAU,mBAAA;EAAA;;;;;;;;;;EAWtB,YAAA,GAAe,gBAAA,UAA0B,MAAA;EAUzC;EARA,MAAA,GAAS,QAAA;EAQO;EANhB,UAAA,GAAa,YAAA;EAMS;EAJtB,SAAA;EAQyB;EANzB,QAAA,YAAoB,WAAA;EAQb;EANP,IAAA,GAAO,QAAA,CAAS,MAAA;AAAA;;UAID,UAAA;EAYN;EAVT,KAAA,EAAO,WAAA;EAUe;EARtB,QAAA;EAFO;EAIP,MAAA,EAAQ,MAAA;EAAR;EAEA,MAAA,EAAQ,MAAA;EAAR;EAEA,SAAA,EAAW,MAAA;EAAX;EAEA,MAAA,GAAS,aAAA,CAAc,gBAAA,CAAiB,KAAA;AAAA;;UAIzB,WAAA;EAJyB;EAMxC,QAAA,EAAU,cAAA;EANmC;AAI/C;;;EAOE,OAAA,WAAkB,UAAA;EAAA;EAElB,KAAA,EAAO,UAAA;EAIC;EAFR,QAAA;EAIc;EAFd,MAAA,EAAQ,MAAA;EAXR;EAaA,MAAA,EAAQ,MAAA;AAAA;;UAQO,eAAA;EAdR;EAgBP,OAAA;EAZA;EAcA,KAAA;EAZA;;;AAAc;EAiBd,cAAA;AAAA;;;;;KAOU,cAAA,IAAkB,EAAA,UAAY,IAAY;;KAGjD,SAAA;EAHO,wDAKV,MAAA,GAAS,MAAA,SAAe,UAAA;EAExB,IAAA;AAAA,KACG,aAAA,CAAc,CAAA;EAAoB,MAAA,EAAQ,UAAA,CAAW,CAAA;AAAA;EAAS,MAAA,GAAS,MAAA;AAAA;;;;;;;;;;;;;;;KAgBhE,SAAA;EAAgC,EAAA,EAAI,CAAA;AAAA,IAAM,SAAA,CAAU,CAAA;;UAe/C,mBAAA;EA/B2D;EAiC1E,MAAA,WAAiB,WAAA;EAjC+D;EAmChF,OAAA,GAAU,aAAA;EAnBS;EAqBnB,cAAA,GAAiB,cAAA;EArB6B;EAuB9C,eAAA;AAAA;;UAIe,MAAA;EA3BK;EA6BpB,KAAA,IAAS,WAAA;EA7BqC;EA+B9C,QAAA,IAAY,cAAA;EA/BkD;EAiC9D,OAAA,aAAoB,UAAA;EAjC2C;EAmC/D,KAAA,IAAS,UAAA;EApByB;EAsBlC,MAAA,IAAU,MAAA;EApBO;EAsBjB,MAAA,IAAU,MAAA;EAlBO;;;;;;;EA0BjB,MAAA,IAAU,QAAA,GAAW,KAAA,EAAO,WAAA,KAAgB,CAAA,EAAG,MAAA,IAAU,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,qBAAsB,CAAA;EA1BvE;EA4BjB,QAAA,mBAA2B,MAAA,WAAiB,SAAA,CAAU,CAAA,GAAI,OAAA,GAAU,eAAA;EA1BrD;EA4Bf,UAAA,CAAW,KAAA,EAAO,UAAA,GAAa,UAAA;EAxBhB;EA0Bf,UAAA;EA1BqB;;;;;;EAiCrB,OAAA,CAAQ,EAAA;EAboB;;;;EAkB5B,SAAA,CAAU,MAAA,WAAiB,WAAA;EAhB2B;;;;;;EAuBtD,MAAA,aAAmB,WAAA;EAED;EAAA,SAAT,OAAA,EAAS,aAAA;EAAa;EAE/B,OAAA;AAAA;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;UA4DiB,mBAAA;EAUf;EARA,MAAA,EAAQ,MAAA;EAQa;EANrB,MAAA,QAAc,MAAA;EAUC;EARf,MAAA,QAAc,MAAA;;EAEd,IAAA,QAAY,UAAA;EAcA;EAZZ,QAAA,EAAU,WAAA;AAAA;;UAIK,WAAA;EA2BK;;;;;EArBpB,IAAA;EAEA;EAAA,SAAA,GAAY,SAAA,CAAU,mBAAA;EAAA;;;;;;;;;;EAWtB,YAAA,GAAe,gBAAA,UAA0B,MAAA;EAUzC;EARA,MAAA,GAAS,QAAA;EAQO;EANhB,UAAA,GAAa,YAAA;EAMS;EAJtB,SAAA;EAQyB;EANzB,QAAA,YAAoB,WAAA;EAQb;EANP,IAAA,GAAO,QAAA,CAAS,MAAA;AAAA;;UAID,UAAA;EAYN;EAVT,KAAA,EAAO,WAAA;EAUe;EARtB,QAAA;EAFO;EAIP,MAAA,EAAQ,MAAA;EAAR;EAEA,MAAA,EAAQ,MAAA;EAAR;EAEA,SAAA,EAAW,MAAA;EAAX;EAEA,MAAA,GAAS,aAAA,CAAc,gBAAA,CAAiB,KAAA;AAAA;;UAIzB,WAAA;EAJyB;EAMxC,QAAA,EAAU,cAAA;EANmC;AAI/C;;;EAOE,OAAA,WAAkB,UAAA;EAAA;EAElB,KAAA,EAAO,UAAA;EAIC;EAFR,QAAA;EAIc;EAFd,MAAA,EAAQ,MAAA;EAXR;EAaA,MAAA,EAAQ,MAAA;AAAA;;UAQO,eAAA;EAdR;EAgBP,OAAA;EAZA;EAcA,KAAA;EAZA;;;AAAc;EAiBd,cAAA;AAAA;;;;;KAOU,cAAA,IAAkB,EAAA,UAAY,IAAY;;KAGjD,SAAA;EAHO,wDAKV,MAAA,GAAS,MAAA,SAAe,UAAA;EAExB,IAAA;AAAA,KACG,aAAA,CAAc,CAAA;EAAoB,MAAA,EAAQ,UAAA,CAAW,CAAA;AAAA;EAAS,MAAA,GAAS,MAAA;AAAA;;;;;;;;;;;;;;;KAgBhE,SAAA;EAAgC,EAAA,EAAI,CAAA;AAAA,IAAM,SAAA,CAAU,CAAA;;UAe/C,mBAAA;EA/B2D;EAiC1E,MAAA,WAAiB,WAAA;EAjC+D;EAmChF,OAAA,GAAU,aAAA;EAnBS;EAqBnB,cAAA,GAAiB,cAAA;EArB6B;EAuB9C,eAAA;AAAA;;UAIe,MAAA;EA3BK;EA6BpB,KAAA,IAAS,WAAA;EA7BqC;EA+B9C,QAAA,IAAY,cAAA;EA/BkD;EAiC9D,OAAA,aAAoB,UAAA;EAjC2C;EAmC/D,KAAA,IAAS,UAAA;EApByB;EAsBlC,MAAA,IAAU,MAAA;EApBO;EAsBjB,MAAA,IAAU,MAAA;EAlBO;;;;;;;EA0BjB,MAAA,IAAU,QAAA,GAAW,KAAA,EAAO,WAAA,KAAgB,CAAA,EAAG,MAAA,IAAU,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,qBAAsB,CAAA;EA1BvE;EA4BjB,QAAA,mBAA2B,MAAA,WAAiB,SAAA,CAAU,CAAA,GAAI,OAAA,GAAU,eAAA;EA1BrD;EA4Bf,UAAA,CAAW,KAAA,EAAO,UAAA,GAAa,UAAA;EAxBhB;EA0Bf,UAAA;EA1BqB;;;;;;EAiCrB,OAAA,CAAQ,EAAA;EAboB;;;;EAkB5B,SAAA,CAAU,MAAA,WAAiB,WAAA;EAhB2B;;;;;;EAuBtD,MAAA,aAAmB,WAAA;EAED;EAAA,SAAT,OAAA,EAAS,aAAA;EAAa;EAE/B,OAAA;AAAA;;;;;;;;;;;iBAoJc,WAAA,CAAY,EAAA,UAAY,IAAY;;;;;iBA+CpC,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,MAAM"}
|
package/dist/router.js
CHANGED
|
@@ -67,7 +67,7 @@ function shallowEqualRecord(a, b) {
|
|
|
67
67
|
* route must not poison all router state.
|
|
68
68
|
*/
|
|
69
69
|
function compileRoutes(routes) {
|
|
70
|
-
|
|
70
|
+
const valid = flattenRouteTree(routes, "", []).filter((fr) => {
|
|
71
71
|
try {
|
|
72
72
|
parsePattern(fr.fullPath);
|
|
73
73
|
return true;
|
|
@@ -75,7 +75,11 @@ function compileRoutes(routes) {
|
|
|
75
75
|
warnDev(`ignoring invalid route pattern ${fr.fullPath}: ${error.message}`);
|
|
76
76
|
return false;
|
|
77
77
|
}
|
|
78
|
-
})
|
|
78
|
+
});
|
|
79
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const fr of valid) if (seen.has(fr.fullPath)) warnDev(`duplicate route path "${fr.fullPath || "/"}" — only the first match is reachable`);
|
|
81
|
+
else seen.add(fr.fullPath);
|
|
82
|
+
return valid.sort((a, b) => compareSpecificity(a.fullPath, b.fullPath));
|
|
79
83
|
}
|
|
80
84
|
/**
|
|
81
85
|
* Match a location against the compiled route table, returning the matched chain
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["/**\n * The Quantum router — signals-native routing state with typed, validated\n * navigation and re-render isolation.\n *\n * Router state (location, params, search, matched route) is modeled as the\n * fine-grained signal graph from `@mindees/core` (Phase 1 `signal`/`computed`,\n * Phase 2 selector isolation). Consumers read a slice via {@link Router.select}\n * and re-run **only** when that slice changes — no whole-tree re-render on\n * navigation, no global-vs-local hook trap (cf. Expo Router). See ADR-0003.\n *\n * @module\n */\n\nimport {\n type Component,\n computed,\n createRoot,\n effect,\n type Memo,\n type MindeesNode,\n type Signal,\n signal,\n} from '@mindees/core'\nimport { clearActiveRouter, setActiveRouter } from './active'\nimport {\n createLoaderManager,\n type LoaderData,\n type LoaderDepsFn,\n type LoaderFn,\n type LoaderManager,\n} from './data'\nimport {\n createHref,\n createMemoryHistory,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\nimport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\nimport { parseQuery, type QueryValue, safeValidateSearch, stringifyQuery } from './search'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n// ---------------------------------------------------------------------------\n// Route table types\n// ---------------------------------------------------------------------------\n\n/**\n * Props a route's component receives from {@link createRouterView}. `params` and\n * `search` are **reactive accessors** (read them in a reactive scope so a\n * same-route param change updates in place, no re-mount); `children` is the\n * matched **child route** — render it wherever the nested route should appear\n * (the \"outlet\"). See ADR-0004.\n */\nexport interface RouteComponentProps {\n /** The router instance (for `navigate`/`select`). */\n router: Router\n /** Reactive accessor for the current (merged) path params. */\n params: () => Record<string, string>\n /** Reactive accessor for the current search params. */\n search: () => Record<string, unknown>\n /** Reactive accessor for this route's loader state (`idle` when it has no loader). */\n data: () => LoaderData\n /** The matched child route (the outlet); render it to show nested routes. */\n children: MindeesNode\n}\n\n/** A route: a path pattern, an optional component, optional search schema, and optional children. */\nexport interface RouteRecord {\n /**\n * The path pattern. At the top level this is absolute (`/posts/:postId`); a\n * nested route's path is **relative to its parent** (`settings`), and a child\n * with `''` or `'/'` is the parent's **index** route.\n */\n path: string\n /** The component to render for this route. A component-less route passes its child through. */\n component?: Component<RouteComponentProps>\n /**\n * A Standard Schema validating this route's search params. Its **output must\n * be object-shaped** (search params are a record), so a non-object schema like\n * `z.string()` is rejected at compile time and validated results need no cast.\n *\n * Note: only the **matched leaf** route's `searchSchema` is applied — a schema\n * on a parent/layout route is currently not used (search is global to the URL;\n * the leaf governs it). Per-route end-to-end `InferOutput` typing through\n * `Router.search()` arrives with the typed route registry (a later phase).\n */\n searchSchema?: StandardSchemaV1<unknown, Record<string, unknown>>\n /** Data loader for this route (sync or async); result is exposed via `data()`. */\n loader?: LoaderFn\n /** Declares which inputs key this route's loader cache (e.g. specific search params). */\n loaderDeps?: LoaderDepsFn\n /** Stale-while-revalidate window in ms; within it a successful load is reused. */\n staleTime?: number\n /** Nested child routes (their `path` is relative to this route). */\n children?: readonly RouteRecord[]\n /** Arbitrary route metadata. */\n meta?: Readonly<Record<string, unknown>>\n}\n\n/** The result of matching a location against the route table. */\nexport interface RouteMatch {\n /** The matched route record. */\n route: RouteRecord\n /** The matched pathname. */\n pathname: string\n /** Path params extracted from the pattern. */\n params: Record<string, string>\n /** Search params — validated output when a schema is present, else the raw parse. */\n search: Record<string, unknown>\n /** The raw parsed query, before schema validation. */\n searchRaw: Record<string, string | string[]>\n /** Search-validation issues, present only when validation failed. */\n issues?: ReadonlyArray<StandardSchemaV1.Issue>\n}\n\n/** The router's reactive state — a snapshot read through fine-grained signals. */\nexport interface RouterState {\n /** The current location. */\n location: RouterLocation\n /**\n * The matched route **chain**, root → leaf (one entry per nesting level), or\n * empty when nothing matched. Drives nested rendering ({@link createRouterView}).\n */\n matches: readonly RouteMatch[]\n /** The matched **leaf** route, or `null` if nothing matched. */\n match: RouteMatch | null\n /** Convenience: the current pathname. */\n pathname: string\n /** Convenience: the current path params (`{}` when unmatched). */\n params: Record<string, string>\n /** Convenience: the current search params (`{}` when unmatched). */\n search: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Navigation types — typed targets\n// ---------------------------------------------------------------------------\n\n/** Options that apply to any navigation. */\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. */\n replace?: boolean\n /** Navigate even if the target equals the current location (skips the idempotent no-op). */\n force?: boolean\n /**\n * Wrap the update in `document.startViewTransition` when available (web only).\n * Overrides the router-level `viewTransitions` default. No-op outside a DOM.\n */\n viewTransition?: boolean\n}\n\n/**\n * A navigation guard. Run before each navigation with the target and current\n * hrefs. Return `false` to cancel, a string to redirect, or nothing to proceed.\n */\nexport type BeforeNavigate = (to: string, from: string) => boolean | string | undefined\n\n/** The params/search/hash carried by a structured target, with params required iff the pattern has them. */\ntype NavExtras<P extends string> = {\n /** Search params to serialize into the query string. */\n search?: Record<string, QueryValue>\n /** A hash fragment (with or without a leading `#`). */\n hash?: string\n} & (HasPathParams<P> extends true ? { params: PathParams<P> } : { params?: Record<string, never> })\n\n/**\n * A fully-typed structured navigation target. `to` is a path pattern; `params`\n * is **required** when the pattern has dynamic segments and forbidden otherwise\n * — inferred from `to` with zero codegen.\n *\n * The param requirement is enforced when `to` is a **string literal**. If `to`\n * is a widened `string` (e.g. read from a variable typed `string`), its segments\n * can't be inferred, so `params` is not type-checked — a missing required param\n * then throws {@link RouterError} (`MISSING_PARAM`) at runtime.\n *\n * @example\n * router.navigate({ to: '/posts/:postId', params: { postId: '42' } })\n * router.navigate({ to: '/about' }) // no params allowed\n */\nexport type NavTarget<P extends string> = { to: P } & NavExtras<P>\n\n/** The broad runtime shape `navigate` accepts (the typed surface is {@link NavTarget}). */\ninterface NavTargetInput {\n to: string\n params?: Record<string, string | number>\n search?: Record<string, QueryValue>\n hash?: string\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createRouter}. */\nexport interface CreateRouterOptions {\n /** The route table. Order is irrelevant — routes are matched most-specific first. */\n routes: readonly RouteRecord[]\n /** The history adapter. Defaults to an in-memory history at `/`. */\n history?: RouterHistory\n /** A guard run before each navigation (cancel with `false`, redirect with a string). */\n beforeNavigate?: BeforeNavigate\n /** Wrap navigations in `document.startViewTransition` by default (web only). */\n viewTransitions?: boolean\n}\n\n/** A live router instance. */\nexport interface Router {\n /** The full reactive state snapshot. */\n state(): RouterState\n /** The current location. */\n location(): RouterLocation\n /** The matched route chain (root → leaf), or empty when unmatched. */\n matches(): readonly RouteMatch[]\n /** The current leaf match, or `null`. */\n match(): RouteMatch | null\n /** The current path params (`{}` when unmatched). */\n params(): Record<string, string>\n /** The current search params (`{}` when unmatched). */\n search(): Record<string, unknown>\n /**\n * Subscribe to a derived slice of router state with re-render isolation. The\n * returned accessor (a memo) only changes when `selector(state)` changes under\n * `equals` (default `Object.is`) — the same selector-isolation technique as\n * core's Phase 2 `createProvider` (a computed memo over an `equals:false`\n * source), applied to route state.\n */\n select<S>(selector: (state: RouterState) => S, equals?: (a: S, b: S) => boolean): () => S\n /** Navigate to a typed target or a (possibly relative) href string. */\n navigate<P extends string>(target: string | NavTarget<P>, options?: NavigateOptions): void\n /** Reactively read a match's loader state (`idle` when the route has no loader). */\n loaderData(match: RouteMatch): LoaderData\n /** Re-run the current chain's loaders (marks their cached data stale first). */\n invalidate(): void\n /**\n * Run a target href's loaders WITHOUT navigating (intent prefetch). A\n * still-in-flight preload is aborted if you then navigate to a *different*\n * route; once it settles it warms the cache for that route's next visit\n * (subject to `staleTime`).\n */\n preload(to: string): void\n /**\n * Replace the route table and re-match the current location **in place** — the\n * location is preserved (dynamic reconfiguration without state reset).\n */\n setRoutes(routes: readonly RouteRecord[]): void\n /**\n * The active route table, as provided to {@link createRouter} / {@link Router.setRoutes}\n * (the nested tree, in insertion order). Matching internally flattens the tree\n * and orders leaves by specificity (static > dynamic > catch-all); that\n * precedence is an implementation detail, not the shape returned here.\n */\n routes(): readonly RouteRecord[]\n /** The underlying history adapter. */\n readonly history: RouterHistory\n /** Tear down the router's reactive scope and history subscription. */\n dispose(): void\n}\n\nconst EMPTY_PARAMS: Record<string, string> = Object.freeze({})\nconst EMPTY_SEARCH: Record<string, unknown> = Object.freeze({})\nconst EMPTY_MATCHES: readonly RouteMatch[] = Object.freeze([])\n\n/** A leaf route flattened from the tree: its full path and its root→leaf chain. */\ninterface FlatRoute {\n fullPath: string\n chain: readonly RouteRecord[]\n}\n\n/** Join a parent path and a (relative) child path into a normalized full path. */\nfunction joinPaths(parent: string, child: string): string {\n const base = parent.endsWith('/') ? parent.slice(0, -1) : parent\n const rel = child.startsWith('/') ? child.slice(1) : child\n if (rel.length === 0) return base.length === 0 ? '/' : base\n return `${base}/${rel}`\n}\n\n/**\n * Flatten a (possibly nested) route tree into leaf entries, each carrying its\n * full path and the root→leaf chain of records. A route with children\n * contributes only via its children (add an index child — `path: ''` — to match\n * the parent's own path).\n */\nfunction flattenRouteTree(\n routes: readonly RouteRecord[],\n parentPath: string,\n parentChain: readonly RouteRecord[],\n): FlatRoute[] {\n const out: FlatRoute[] = []\n for (const route of routes) {\n const fullPath = joinPaths(parentPath, route.path)\n const chain = [...parentChain, route]\n if (route.children && route.children.length > 0) {\n out.push(...flattenRouteTree(route.children, fullPath, chain))\n } else {\n out.push({ fullPath, chain })\n }\n }\n return out\n}\n\n/** Dev-only warning (silent in production). */\nfunction warnDev(message: string): void {\n const g = globalThis as {\n process?: { env?: Record<string, string | undefined> }\n console?: { warn?: (message: string) => void }\n }\n if (g.process?.env?.NODE_ENV === 'production') return\n g.console?.warn?.(`[router] ${message}`)\n}\n\n/** Shallow record equality — params/search only \"change\" when an entry actually changes. */\nfunction shallowEqualRecord(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\n if (a === b) return true\n const ak = Object.keys(a)\n if (ak.length !== Object.keys(b).length) return false\n for (const k of ak) if (a[k] !== b[k]) return false\n return true\n}\n\n/**\n * Flatten + sort a route tree most-specific first (static > dynamic > catch-all). Routes whose\n * synthesized pattern is invalid (a catch-all parent with children would place a catch-all\n * segment before a literal one) are dropped with a dev warning rather than thrown — one bad\n * route must not poison all router state.\n */\nfunction compileRoutes(routes: readonly RouteRecord[]): FlatRoute[] {\n return flattenRouteTree(routes, '', [])\n .filter((fr) => {\n try {\n parsePattern(fr.fullPath)\n return true\n } catch (error) {\n warnDev(`ignoring invalid route pattern ${fr.fullPath}: ${(error as Error).message}`)\n return false\n }\n })\n .sort((a, b) => compareSpecificity(a.fullPath, b.fullPath))\n}\n\n/**\n * Match a location against the compiled route table, returning the matched chain\n * (root → leaf), or an empty array if nothing matched. Search is validated\n * against the **leaf** route's schema and shared across the chain.\n */\nfunction matchLocation(\n flat: readonly FlatRoute[],\n location: RouterLocation,\n): readonly RouteMatch[] {\n for (const fr of flat) {\n const params = matchPattern(fr.fullPath, location.pathname)\n if (params === null) continue\n\n const searchRaw = parseQuery(location.search)\n let search: Record<string, unknown> = searchRaw\n let issues: ReadonlyArray<StandardSchemaV1.Issue> | undefined\n\n const leaf = fr.chain[fr.chain.length - 1]\n if (leaf?.searchSchema) {\n // searchSchema's output is constrained to Record<string, unknown>, so the\n // validated value is already correctly typed — no cast needed.\n try {\n const result = safeValidateSearch(leaf.searchSchema, searchRaw)\n if (result.ok) search = result.value\n else issues = result.issues\n } catch (err) {\n // safeValidateSearch THROWS on an async schema (ASYNC_SCHEMA), and a\n // schema could throw synchronously too. This runs inside matchMemo\n // (a computed read by stateMemo AND the loader effect), so an escaping\n // throw would poison every router-state accessor and wedge navigation.\n // Contain it: degrade to raw search and surface it via match.issues,\n // exactly like the invalid-input path.\n issues = [{ message: err instanceof Error ? err.message : 'search validation failed' }]\n }\n }\n\n return fr.chain.map((route) => {\n const base: RouteMatch = { route, pathname: location.pathname, params, search, searchRaw }\n return issues ? { ...base, issues } : base\n })\n }\n return EMPTY_MATCHES\n}\n\n/**\n * Resolve a (possibly relative) path against a base pathname. Absolute paths\n * (leading `/`) ignore the base; `.`/`..` segments are applied against it,\n * treating the base pathname as a directory.\n *\n * @example\n * resolvePath('/a/b', '/x') // '/a/b'\n * resolvePath('edit', '/posts/1') // '/posts/1/edit'\n * resolvePath('../', '/posts/1') // '/posts'\n */\nexport function resolvePath(to: string, from: string): string {\n const stack = to.startsWith('/') ? [] : from.split('/').filter((s) => s.length > 0)\n for (const seg of to.split('/')) {\n if (seg === '' || seg === '.') continue\n if (seg === '..') stack.pop()\n else stack.push(seg)\n }\n return `/${stack.join('/')}`\n}\n\n/** Resolve an href string (which may be relative and carry query/hash) against a location. */\nfunction resolveHref(to: string, from: RouterLocation): string {\n const hasPath = to.length > 0 && to[0] !== '?' && to[0] !== '#'\n let rest = to\n let hash = ''\n const hashIndex = rest.indexOf('#')\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex)\n rest = rest.slice(0, hashIndex)\n }\n let search = ''\n const queryIndex = rest.indexOf('?')\n if (queryIndex !== -1) {\n search = rest.slice(queryIndex)\n rest = rest.slice(0, queryIndex)\n }\n const pathname = hasPath ? resolvePath(rest, from.pathname) : from.pathname\n // RFC 3986: a fragment-only reference (no path, no query) keeps the current\n // path AND query, replacing only the fragment — so a `#anchor` navigation must\n // not drop the active search params.\n const finalSearch = !hasPath && queryIndex === -1 ? from.search : search\n return `${pathname}${finalSearch}${hash}`\n}\n\n/** Build an href from a structured navigation target. */\nfunction buildHref(target: NavTargetInput): string {\n const pathname = buildPath(target.to, target.params ?? {})\n const query = target.search ? stringifyQuery(target.search) : ''\n let hash = target.hash ?? ''\n if (hash.length > 0 && !hash.startsWith('#')) hash = `#${hash}`\n return `${pathname}${query ? `?${query}` : ''}${hash}`\n}\n\n/**\n * Create a router over a route table. State is reactive (signals); call\n * {@link Router.dispose} to tear it down.\n */\nexport function createRouter(options: CreateRouterOptions): Router {\n const history = options.history ?? createMemoryHistory()\n\n let routesSig!: Signal<readonly RouteRecord[]>\n let flatMemo!: Memo<FlatRoute[]>\n let locationSig!: Signal<RouterLocation>\n let stateMemo!: Memo<RouterState>\n let paramsMemo!: Memo<Record<string, string>>\n let searchMemo!: Memo<Record<string, unknown>>\n let loaders!: LoaderManager\n\n const dispose = createRoot((disposeRoot) => {\n routesSig = signal<readonly RouteRecord[]>(options.routes, { equals: false })\n flatMemo = computed(() => compileRoutes(routesSig()))\n locationSig = signal<RouterLocation>(history.location(), { equals: false })\n const matchMemo = computed(() => matchLocation(flatMemo(), locationSig()))\n stateMemo = computed<RouterState>(() => {\n const location = locationSig()\n const matches = matchMemo()\n const leaf = matches.length > 0 ? (matches[matches.length - 1] ?? null) : null\n return {\n location,\n matches,\n match: leaf,\n pathname: location.pathname,\n params: leaf ? leaf.params : EMPTY_PARAMS,\n search: leaf ? leaf.search : EMPTY_SEARCH,\n }\n })\n // Re-render isolation: params()/search() only emit when an entry actually changes,\n // not on every navigation (stateMemo returns a fresh object each time).\n paramsMemo = computed(() => stateMemo().params, { equals: shallowEqualRecord })\n searchMemo = computed(() => stateMemo().search, { equals: shallowEqualRecord })\n\n // Reactive data version: bumped on any loader-cache change; loader reads\n // subscribe to it so a component's data binding updates when its load\n // resolves (without re-mounting the component).\n const dataVersion = signal(0, { equals: false })\n loaders = createLoaderManager({\n location: () => locationSig(),\n onChange: () => dataVersion.set(0),\n track: () => {\n dataVersion()\n },\n })\n // Orchestrate loaders on every navigation. Owned by the router root (not a\n // re-running region), and it never reads `dataVersion` — so no loops.\n effect(() => loaders.sync(matchMemo()))\n\n const unsubscribe = history.subscribe((loc) => locationSig.set(loc))\n return () => {\n unsubscribe()\n loaders.dispose()\n disposeRoot()\n }\n })\n\n /** Apply the navigation guard, following redirects (capped). Returns the final href, or null to cancel. */\n const applyGuard = (href: string): string | null => {\n const guard = options.beforeNavigate\n if (!guard) return href\n let current = href\n for (let i = 0; i < 10; i++) {\n const result = guard(current, createHref(locationSig()))\n if (result === false) return null\n if (typeof result === 'string') {\n const next = resolveHref(result, locationSig())\n if (next === current) return current\n current = next\n continue\n }\n return current\n }\n // Redirect cap exceeded (a guard that keeps redirecting): cancel rather than\n // commit a location the guard never approved.\n return null\n }\n\n const navigate = (target: string | NavTargetInput, opts?: NavigateOptions): void => {\n const initial =\n typeof target === 'string' ? resolveHref(target, locationSig()) : buildHref(target)\n const href = applyGuard(initial)\n if (href === null) return\n // Idempotent: navigating to the current location is a no-op unless forced.\n if (opts?.force !== true && href === createHref(locationSig())) return\n\n const commit = (): void => {\n if (opts?.replace) history.replace(href)\n else history.push(href)\n }\n const wantsTransition = opts?.viewTransition ?? options.viewTransitions ?? false\n if (wantsTransition) startViewTransition(commit)\n else commit()\n }\n\n const preload = (to: string): void => {\n const href = resolveHref(to, locationSig())\n loaders.preload(matchLocation(flatMemo(), parseHref(href)))\n }\n\n const router: Router = {\n state: () => stateMemo(),\n location: () => locationSig(),\n matches: () => stateMemo().matches,\n match: () => stateMemo().match,\n params: () => paramsMemo(),\n search: () => searchMemo(),\n select: <S>(selector: (state: RouterState) => S, equals: (a: S, b: S) => boolean = Object.is) =>\n computed(() => selector(stateMemo()), { equals }),\n navigate: navigate as Router['navigate'],\n loaderData: (match) => loaders.read(match),\n invalidate: () => loaders.invalidate(stateMemo().matches),\n preload,\n setRoutes: (routes) => routesSig.set(routes),\n routes: () => routesSig(),\n history,\n // Tear down reactivity AND drop the active-router registration, so a disposed\n // router no longer answers useRouter()/useParams()/Link.\n dispose: () => {\n dispose()\n clearActiveRouter(router)\n },\n }\n // Register as the active router so hooks (useRouter, …) and the bound Link resolve it.\n setActiveRouter(router)\n return router\n}\n\n/** A document that may support the View Transitions API. */\ninterface ViewTransitionDocument {\n startViewTransition?: (callback: () => void) => unknown\n}\n\n/** The subset of a ViewTransition we touch (its eagerly-created promises). */\ninterface ViewTransitionLike {\n ready?: Promise<unknown>\n updateCallbackDone?: Promise<unknown>\n}\n\n/**\n * Run `commit` inside `document.startViewTransition` when available (web only),\n * else run it directly. The signals re-render is synchronous, so it happens\n * inside the transition. No-op-wrapping outside a DOM (SSR, native, tests).\n *\n * View transitions are a progressive enhancement, so this must NEVER throw out of\n * navigate() or leak an unhandled rejection: (1) a rapid second navigation aborts\n * the first transition and rejects its eagerly-created `ready`/`updateCallbackDone`\n * promises — we mark those handled; (2) some browsers throw synchronously (e.g. a\n * hidden/background document) — we fall back to a plain commit so the navigation\n * still lands (without committing twice).\n */\nfunction startViewTransition(commit: () => void): void {\n const doc =\n typeof document === 'undefined' ? undefined : (document as unknown as ViewTransitionDocument)\n if (!doc?.startViewTransition) {\n commit()\n return\n }\n let committed = false\n const runCommit = (): void => {\n committed = true\n commit()\n }\n try {\n const transition = doc.startViewTransition(runCommit) as ViewTransitionLike | undefined\n void transition?.ready?.catch(() => {})\n void transition?.updateCallbackDone?.catch(() => {})\n } catch {\n if (!committed) commit()\n }\n}\n\nexport { createHref }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["/**\n * The Quantum router — signals-native routing state with typed, validated\n * navigation and re-render isolation.\n *\n * Router state (location, params, search, matched route) is modeled as the\n * fine-grained signal graph from `@mindees/core` (Phase 1 `signal`/`computed`,\n * Phase 2 selector isolation). Consumers read a slice via {@link Router.select}\n * and re-run **only** when that slice changes — no whole-tree re-render on\n * navigation, no global-vs-local hook trap (cf. Expo Router). See ADR-0003.\n *\n * @module\n */\n\nimport {\n type Component,\n computed,\n createRoot,\n effect,\n type Memo,\n type MindeesNode,\n type Signal,\n signal,\n} from '@mindees/core'\nimport { clearActiveRouter, setActiveRouter } from './active'\nimport {\n createLoaderManager,\n type LoaderData,\n type LoaderDepsFn,\n type LoaderFn,\n type LoaderManager,\n} from './data'\nimport {\n createHref,\n createMemoryHistory,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\nimport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\nimport { parseQuery, type QueryValue, safeValidateSearch, stringifyQuery } from './search'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n// ---------------------------------------------------------------------------\n// Route table types\n// ---------------------------------------------------------------------------\n\n/**\n * Props a route's component receives from {@link createRouterView}. `params` and\n * `search` are **reactive accessors** (read them in a reactive scope so a\n * same-route param change updates in place, no re-mount); `children` is the\n * matched **child route** — render it wherever the nested route should appear\n * (the \"outlet\"). See ADR-0004.\n */\nexport interface RouteComponentProps {\n /** The router instance (for `navigate`/`select`). */\n router: Router\n /** Reactive accessor for the current (merged) path params. */\n params: () => Record<string, string>\n /** Reactive accessor for the current search params. */\n search: () => Record<string, unknown>\n /** Reactive accessor for this route's loader state (`idle` when it has no loader). */\n data: () => LoaderData\n /** The matched child route (the outlet); render it to show nested routes. */\n children: MindeesNode\n}\n\n/** A route: a path pattern, an optional component, optional search schema, and optional children. */\nexport interface RouteRecord {\n /**\n * The path pattern. At the top level this is absolute (`/posts/:postId`); a\n * nested route's path is **relative to its parent** (`settings`), and a child\n * with `''` or `'/'` is the parent's **index** route.\n */\n path: string\n /** The component to render for this route. A component-less route passes its child through. */\n component?: Component<RouteComponentProps>\n /**\n * A Standard Schema validating this route's search params. Its **output must\n * be object-shaped** (search params are a record), so a non-object schema like\n * `z.string()` is rejected at compile time and validated results need no cast.\n *\n * Note: only the **matched leaf** route's `searchSchema` is applied — a schema\n * on a parent/layout route is currently not used (search is global to the URL;\n * the leaf governs it). Per-route end-to-end `InferOutput` typing through\n * `Router.search()` arrives with the typed route registry (a later phase).\n */\n searchSchema?: StandardSchemaV1<unknown, Record<string, unknown>>\n /** Data loader for this route (sync or async); result is exposed via `data()`. */\n loader?: LoaderFn\n /** Declares which inputs key this route's loader cache (e.g. specific search params). */\n loaderDeps?: LoaderDepsFn\n /** Stale-while-revalidate window in ms; within it a successful load is reused. */\n staleTime?: number\n /** Nested child routes (their `path` is relative to this route). */\n children?: readonly RouteRecord[]\n /** Arbitrary route metadata. */\n meta?: Readonly<Record<string, unknown>>\n}\n\n/** The result of matching a location against the route table. */\nexport interface RouteMatch {\n /** The matched route record. */\n route: RouteRecord\n /** The matched pathname. */\n pathname: string\n /** Path params extracted from the pattern. */\n params: Record<string, string>\n /** Search params — validated output when a schema is present, else the raw parse. */\n search: Record<string, unknown>\n /** The raw parsed query, before schema validation. */\n searchRaw: Record<string, string | string[]>\n /** Search-validation issues, present only when validation failed. */\n issues?: ReadonlyArray<StandardSchemaV1.Issue>\n}\n\n/** The router's reactive state — a snapshot read through fine-grained signals. */\nexport interface RouterState {\n /** The current location. */\n location: RouterLocation\n /**\n * The matched route **chain**, root → leaf (one entry per nesting level), or\n * empty when nothing matched. Drives nested rendering ({@link createRouterView}).\n */\n matches: readonly RouteMatch[]\n /** The matched **leaf** route, or `null` if nothing matched. */\n match: RouteMatch | null\n /** Convenience: the current pathname. */\n pathname: string\n /** Convenience: the current path params (`{}` when unmatched). */\n params: Record<string, string>\n /** Convenience: the current search params (`{}` when unmatched). */\n search: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Navigation types — typed targets\n// ---------------------------------------------------------------------------\n\n/** Options that apply to any navigation. */\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. */\n replace?: boolean\n /** Navigate even if the target equals the current location (skips the idempotent no-op). */\n force?: boolean\n /**\n * Wrap the update in `document.startViewTransition` when available (web only).\n * Overrides the router-level `viewTransitions` default. No-op outside a DOM.\n */\n viewTransition?: boolean\n}\n\n/**\n * A navigation guard. Run before each navigation with the target and current\n * hrefs. Return `false` to cancel, a string to redirect, or nothing to proceed.\n */\nexport type BeforeNavigate = (to: string, from: string) => boolean | string | undefined\n\n/** The params/search/hash carried by a structured target, with params required iff the pattern has them. */\ntype NavExtras<P extends string> = {\n /** Search params to serialize into the query string. */\n search?: Record<string, QueryValue>\n /** A hash fragment (with or without a leading `#`). */\n hash?: string\n} & (HasPathParams<P> extends true ? { params: PathParams<P> } : { params?: Record<string, never> })\n\n/**\n * A fully-typed structured navigation target. `to` is a path pattern; `params`\n * is **required** when the pattern has dynamic segments and forbidden otherwise\n * — inferred from `to` with zero codegen.\n *\n * The param requirement is enforced when `to` is a **string literal**. If `to`\n * is a widened `string` (e.g. read from a variable typed `string`), its segments\n * can't be inferred, so `params` is not type-checked — a missing required param\n * then throws {@link RouterError} (`MISSING_PARAM`) at runtime.\n *\n * @example\n * router.navigate({ to: '/posts/:postId', params: { postId: '42' } })\n * router.navigate({ to: '/about' }) // no params allowed\n */\nexport type NavTarget<P extends string> = { to: P } & NavExtras<P>\n\n/** The broad runtime shape `navigate` accepts (the typed surface is {@link NavTarget}). */\ninterface NavTargetInput {\n to: string\n params?: Record<string, string | number>\n search?: Record<string, QueryValue>\n hash?: string\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createRouter}. */\nexport interface CreateRouterOptions {\n /** The route table. Order is irrelevant — routes are matched most-specific first. */\n routes: readonly RouteRecord[]\n /** The history adapter. Defaults to an in-memory history at `/`. */\n history?: RouterHistory\n /** A guard run before each navigation (cancel with `false`, redirect with a string). */\n beforeNavigate?: BeforeNavigate\n /** Wrap navigations in `document.startViewTransition` by default (web only). */\n viewTransitions?: boolean\n}\n\n/** A live router instance. */\nexport interface Router {\n /** The full reactive state snapshot. */\n state(): RouterState\n /** The current location. */\n location(): RouterLocation\n /** The matched route chain (root → leaf), or empty when unmatched. */\n matches(): readonly RouteMatch[]\n /** The current leaf match, or `null`. */\n match(): RouteMatch | null\n /** The current path params (`{}` when unmatched). */\n params(): Record<string, string>\n /** The current search params (`{}` when unmatched). */\n search(): Record<string, unknown>\n /**\n * Subscribe to a derived slice of router state with re-render isolation. The\n * returned accessor (a memo) only changes when `selector(state)` changes under\n * `equals` (default `Object.is`) — the same selector-isolation technique as\n * core's Phase 2 `createProvider` (a computed memo over an `equals:false`\n * source), applied to route state.\n */\n select<S>(selector: (state: RouterState) => S, equals?: (a: S, b: S) => boolean): () => S\n /** Navigate to a typed target or a (possibly relative) href string. */\n navigate<P extends string>(target: string | NavTarget<P>, options?: NavigateOptions): void\n /** Reactively read a match's loader state (`idle` when the route has no loader). */\n loaderData(match: RouteMatch): LoaderData\n /** Re-run the current chain's loaders (marks their cached data stale first). */\n invalidate(): void\n /**\n * Run a target href's loaders WITHOUT navigating (intent prefetch). A\n * still-in-flight preload is aborted if you then navigate to a *different*\n * route; once it settles it warms the cache for that route's next visit\n * (subject to `staleTime`).\n */\n preload(to: string): void\n /**\n * Replace the route table and re-match the current location **in place** — the\n * location is preserved (dynamic reconfiguration without state reset).\n */\n setRoutes(routes: readonly RouteRecord[]): void\n /**\n * The active route table, as provided to {@link createRouter} / {@link Router.setRoutes}\n * (the nested tree, in insertion order). Matching internally flattens the tree\n * and orders leaves by specificity (static > dynamic > catch-all); that\n * precedence is an implementation detail, not the shape returned here.\n */\n routes(): readonly RouteRecord[]\n /** The underlying history adapter. */\n readonly history: RouterHistory\n /** Tear down the router's reactive scope and history subscription. */\n dispose(): void\n}\n\nconst EMPTY_PARAMS: Record<string, string> = Object.freeze({})\nconst EMPTY_SEARCH: Record<string, unknown> = Object.freeze({})\nconst EMPTY_MATCHES: readonly RouteMatch[] = Object.freeze([])\n\n/** A leaf route flattened from the tree: its full path and its root→leaf chain. */\ninterface FlatRoute {\n fullPath: string\n chain: readonly RouteRecord[]\n}\n\n/** Join a parent path and a (relative) child path into a normalized full path. */\nfunction joinPaths(parent: string, child: string): string {\n const base = parent.endsWith('/') ? parent.slice(0, -1) : parent\n const rel = child.startsWith('/') ? child.slice(1) : child\n if (rel.length === 0) return base.length === 0 ? '/' : base\n return `${base}/${rel}`\n}\n\n/**\n * Flatten a (possibly nested) route tree into leaf entries, each carrying its\n * full path and the root→leaf chain of records. A route with children\n * contributes only via its children (add an index child — `path: ''` — to match\n * the parent's own path).\n */\nfunction flattenRouteTree(\n routes: readonly RouteRecord[],\n parentPath: string,\n parentChain: readonly RouteRecord[],\n): FlatRoute[] {\n const out: FlatRoute[] = []\n for (const route of routes) {\n const fullPath = joinPaths(parentPath, route.path)\n const chain = [...parentChain, route]\n if (route.children && route.children.length > 0) {\n out.push(...flattenRouteTree(route.children, fullPath, chain))\n } else {\n out.push({ fullPath, chain })\n }\n }\n return out\n}\n\n/** Dev-only warning (silent in production). */\nfunction warnDev(message: string): void {\n const g = globalThis as {\n process?: { env?: Record<string, string | undefined> }\n console?: { warn?: (message: string) => void }\n }\n if (g.process?.env?.NODE_ENV === 'production') return\n g.console?.warn?.(`[router] ${message}`)\n}\n\n/** Shallow record equality — params/search only \"change\" when an entry actually changes. */\nfunction shallowEqualRecord(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\n if (a === b) return true\n const ak = Object.keys(a)\n if (ak.length !== Object.keys(b).length) return false\n for (const k of ak) if (a[k] !== b[k]) return false\n return true\n}\n\n/**\n * Flatten + sort a route tree most-specific first (static > dynamic > catch-all). Routes whose\n * synthesized pattern is invalid (a catch-all parent with children would place a catch-all\n * segment before a literal one) are dropped with a dev warning rather than thrown — one bad\n * route must not poison all router state.\n */\nfunction compileRoutes(routes: readonly RouteRecord[]): FlatRoute[] {\n const valid = flattenRouteTree(routes, '', []).filter((fr) => {\n try {\n parsePattern(fr.fullPath)\n return true\n } catch (error) {\n warnDev(`ignoring invalid route pattern ${fr.fullPath}: ${(error as Error).message}`)\n return false\n }\n })\n // Warn on duplicate effective paths: matchLocation returns the FIRST match and the sort is stable,\n // so a second route at the same fullPath (e.g. `users.tsx` + `users/index.tsx`, or two `(group)`\n // indexes) is permanently unreachable — surface it instead of silently dropping it.\n const seen = new Set<string>()\n for (const fr of valid) {\n if (seen.has(fr.fullPath)) {\n warnDev(`duplicate route path \"${fr.fullPath || '/'}\" — only the first match is reachable`)\n } else {\n seen.add(fr.fullPath)\n }\n }\n return valid.sort((a, b) => compareSpecificity(a.fullPath, b.fullPath))\n}\n\n/**\n * Match a location against the compiled route table, returning the matched chain\n * (root → leaf), or an empty array if nothing matched. Search is validated\n * against the **leaf** route's schema and shared across the chain.\n */\nfunction matchLocation(\n flat: readonly FlatRoute[],\n location: RouterLocation,\n): readonly RouteMatch[] {\n for (const fr of flat) {\n const params = matchPattern(fr.fullPath, location.pathname)\n if (params === null) continue\n\n const searchRaw = parseQuery(location.search)\n let search: Record<string, unknown> = searchRaw\n let issues: ReadonlyArray<StandardSchemaV1.Issue> | undefined\n\n const leaf = fr.chain[fr.chain.length - 1]\n if (leaf?.searchSchema) {\n // searchSchema's output is constrained to Record<string, unknown>, so the\n // validated value is already correctly typed — no cast needed.\n try {\n const result = safeValidateSearch(leaf.searchSchema, searchRaw)\n if (result.ok) search = result.value\n else issues = result.issues\n } catch (err) {\n // safeValidateSearch THROWS on an async schema (ASYNC_SCHEMA), and a\n // schema could throw synchronously too. This runs inside matchMemo\n // (a computed read by stateMemo AND the loader effect), so an escaping\n // throw would poison every router-state accessor and wedge navigation.\n // Contain it: degrade to raw search and surface it via match.issues,\n // exactly like the invalid-input path.\n issues = [{ message: err instanceof Error ? err.message : 'search validation failed' }]\n }\n }\n\n return fr.chain.map((route) => {\n const base: RouteMatch = { route, pathname: location.pathname, params, search, searchRaw }\n return issues ? { ...base, issues } : base\n })\n }\n return EMPTY_MATCHES\n}\n\n/**\n * Resolve a (possibly relative) path against a base pathname. Absolute paths\n * (leading `/`) ignore the base; `.`/`..` segments are applied against it,\n * treating the base pathname as a directory.\n *\n * @example\n * resolvePath('/a/b', '/x') // '/a/b'\n * resolvePath('edit', '/posts/1') // '/posts/1/edit'\n * resolvePath('../', '/posts/1') // '/posts'\n */\nexport function resolvePath(to: string, from: string): string {\n const stack = to.startsWith('/') ? [] : from.split('/').filter((s) => s.length > 0)\n for (const seg of to.split('/')) {\n if (seg === '' || seg === '.') continue\n if (seg === '..') stack.pop()\n else stack.push(seg)\n }\n return `/${stack.join('/')}`\n}\n\n/** Resolve an href string (which may be relative and carry query/hash) against a location. */\nfunction resolveHref(to: string, from: RouterLocation): string {\n const hasPath = to.length > 0 && to[0] !== '?' && to[0] !== '#'\n let rest = to\n let hash = ''\n const hashIndex = rest.indexOf('#')\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex)\n rest = rest.slice(0, hashIndex)\n }\n let search = ''\n const queryIndex = rest.indexOf('?')\n if (queryIndex !== -1) {\n search = rest.slice(queryIndex)\n rest = rest.slice(0, queryIndex)\n }\n const pathname = hasPath ? resolvePath(rest, from.pathname) : from.pathname\n // RFC 3986: a fragment-only reference (no path, no query) keeps the current\n // path AND query, replacing only the fragment — so a `#anchor` navigation must\n // not drop the active search params.\n const finalSearch = !hasPath && queryIndex === -1 ? from.search : search\n return `${pathname}${finalSearch}${hash}`\n}\n\n/** Build an href from a structured navigation target. */\nfunction buildHref(target: NavTargetInput): string {\n const pathname = buildPath(target.to, target.params ?? {})\n const query = target.search ? stringifyQuery(target.search) : ''\n let hash = target.hash ?? ''\n if (hash.length > 0 && !hash.startsWith('#')) hash = `#${hash}`\n return `${pathname}${query ? `?${query}` : ''}${hash}`\n}\n\n/**\n * Create a router over a route table. State is reactive (signals); call\n * {@link Router.dispose} to tear it down.\n */\nexport function createRouter(options: CreateRouterOptions): Router {\n const history = options.history ?? createMemoryHistory()\n\n let routesSig!: Signal<readonly RouteRecord[]>\n let flatMemo!: Memo<FlatRoute[]>\n let locationSig!: Signal<RouterLocation>\n let stateMemo!: Memo<RouterState>\n let paramsMemo!: Memo<Record<string, string>>\n let searchMemo!: Memo<Record<string, unknown>>\n let loaders!: LoaderManager\n\n const dispose = createRoot((disposeRoot) => {\n routesSig = signal<readonly RouteRecord[]>(options.routes, { equals: false })\n flatMemo = computed(() => compileRoutes(routesSig()))\n locationSig = signal<RouterLocation>(history.location(), { equals: false })\n const matchMemo = computed(() => matchLocation(flatMemo(), locationSig()))\n stateMemo = computed<RouterState>(() => {\n const location = locationSig()\n const matches = matchMemo()\n const leaf = matches.length > 0 ? (matches[matches.length - 1] ?? null) : null\n return {\n location,\n matches,\n match: leaf,\n pathname: location.pathname,\n params: leaf ? leaf.params : EMPTY_PARAMS,\n search: leaf ? leaf.search : EMPTY_SEARCH,\n }\n })\n // Re-render isolation: params()/search() only emit when an entry actually changes,\n // not on every navigation (stateMemo returns a fresh object each time).\n paramsMemo = computed(() => stateMemo().params, { equals: shallowEqualRecord })\n searchMemo = computed(() => stateMemo().search, { equals: shallowEqualRecord })\n\n // Reactive data version: bumped on any loader-cache change; loader reads\n // subscribe to it so a component's data binding updates when its load\n // resolves (without re-mounting the component).\n const dataVersion = signal(0, { equals: false })\n loaders = createLoaderManager({\n location: () => locationSig(),\n onChange: () => dataVersion.set(0),\n track: () => {\n dataVersion()\n },\n })\n // Orchestrate loaders on every navigation. Owned by the router root (not a\n // re-running region), and it never reads `dataVersion` — so no loops.\n effect(() => loaders.sync(matchMemo()))\n\n const unsubscribe = history.subscribe((loc) => locationSig.set(loc))\n return () => {\n unsubscribe()\n loaders.dispose()\n disposeRoot()\n }\n })\n\n /** Apply the navigation guard, following redirects (capped). Returns the final href, or null to cancel. */\n const applyGuard = (href: string): string | null => {\n const guard = options.beforeNavigate\n if (!guard) return href\n let current = href\n for (let i = 0; i < 10; i++) {\n const result = guard(current, createHref(locationSig()))\n if (result === false) return null\n if (typeof result === 'string') {\n const next = resolveHref(result, locationSig())\n if (next === current) return current\n current = next\n continue\n }\n return current\n }\n // Redirect cap exceeded (a guard that keeps redirecting): cancel rather than\n // commit a location the guard never approved.\n return null\n }\n\n const navigate = (target: string | NavTargetInput, opts?: NavigateOptions): void => {\n const initial =\n typeof target === 'string' ? resolveHref(target, locationSig()) : buildHref(target)\n const href = applyGuard(initial)\n if (href === null) return\n // Idempotent: navigating to the current location is a no-op unless forced.\n if (opts?.force !== true && href === createHref(locationSig())) return\n\n const commit = (): void => {\n if (opts?.replace) history.replace(href)\n else history.push(href)\n }\n const wantsTransition = opts?.viewTransition ?? options.viewTransitions ?? false\n if (wantsTransition) startViewTransition(commit)\n else commit()\n }\n\n const preload = (to: string): void => {\n const href = resolveHref(to, locationSig())\n loaders.preload(matchLocation(flatMemo(), parseHref(href)))\n }\n\n const router: Router = {\n state: () => stateMemo(),\n location: () => locationSig(),\n matches: () => stateMemo().matches,\n match: () => stateMemo().match,\n params: () => paramsMemo(),\n search: () => searchMemo(),\n select: <S>(selector: (state: RouterState) => S, equals: (a: S, b: S) => boolean = Object.is) =>\n computed(() => selector(stateMemo()), { equals }),\n navigate: navigate as Router['navigate'],\n loaderData: (match) => loaders.read(match),\n invalidate: () => loaders.invalidate(stateMemo().matches),\n preload,\n setRoutes: (routes) => routesSig.set(routes),\n routes: () => routesSig(),\n history,\n // Tear down reactivity AND drop the active-router registration, so a disposed\n // router no longer answers useRouter()/useParams()/Link.\n dispose: () => {\n dispose()\n clearActiveRouter(router)\n },\n }\n // Register as the active router so hooks (useRouter, …) and the bound Link resolve it.\n setActiveRouter(router)\n return router\n}\n\n/** A document that may support the View Transitions API. */\ninterface ViewTransitionDocument {\n startViewTransition?: (callback: () => void) => unknown\n}\n\n/** The subset of a ViewTransition we touch (its eagerly-created promises). */\ninterface ViewTransitionLike {\n ready?: Promise<unknown>\n updateCallbackDone?: Promise<unknown>\n}\n\n/**\n * Run `commit` inside `document.startViewTransition` when available (web only),\n * else run it directly. The signals re-render is synchronous, so it happens\n * inside the transition. No-op-wrapping outside a DOM (SSR, native, tests).\n *\n * View transitions are a progressive enhancement, so this must NEVER throw out of\n * navigate() or leak an unhandled rejection: (1) a rapid second navigation aborts\n * the first transition and rejects its eagerly-created `ready`/`updateCallbackDone`\n * promises — we mark those handled; (2) some browsers throw synchronously (e.g. a\n * hidden/background document) — we fall back to a plain commit so the navigation\n * still lands (without committing twice).\n */\nfunction startViewTransition(commit: () => void): void {\n const doc =\n typeof document === 'undefined' ? undefined : (document as unknown as ViewTransitionDocument)\n if (!doc?.startViewTransition) {\n commit()\n return\n }\n let committed = false\n const runCommit = (): void => {\n committed = true\n commit()\n }\n try {\n const transition = doc.startViewTransition(runCommit) as ViewTransitionLike | undefined\n void transition?.ready?.catch(() => {})\n void transition?.updateCallbackDone?.catch(() => {})\n } catch {\n if (!committed) commit()\n }\n}\n\nexport { createHref }\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/router",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
4
4
|
"description": "Quantum — the typed, signals-native router for MindeesNative: codegen-free typed path params, Standard Schema validated search params, and selector-isolated reactive route state.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"router",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"directory": "packages/router"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@mindees/core": "0.22.
|
|
38
|
+
"@mindees/core": "0.22.4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"happy-dom": "20.9.0",
|
|
42
42
|
"valibot": "1.4.1",
|
|
43
43
|
"zod": "4.4.3",
|
|
44
|
-
"@mindees/renderer": "0.22.
|
|
44
|
+
"@mindees/renderer": "0.22.4"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsdown",
|