{"_id":"@openclaw/uirouter","_rev":"3-3706abf1fe731b6252c860135e4f8f29","name":"@openclaw/uirouter","dist-tags":{"latest":"0.1.1"},"versions":{"0.0.0":{"name":"@openclaw/uirouter","version":"0.0.0","keywords":["openclaw","router","typescript","ui"],"author":{"name":"OpenClaw Team","email":"dev@openclaw.ai"},"license":"MIT","_id":"@openclaw/uirouter@0.0.0","maintainers":[{"name":"steipete","email":"steipete@gmail.com"},{"name":"vincentkoc","email":"vincentkoc@ieee.org"}],"homepage":"https://github.com/openclaw/uirouter#readme","bugs":{"url":"https://github.com/openclaw/uirouter/issues"},"dist":{"shasum":"d041691a587efe1d7b6e6e280d0a00d10ac089bd","tarball":"https://registry.npmjs.org/@openclaw/uirouter/-/uirouter-0.0.0.tgz","fileCount":6,"integrity":"sha512-NTXCcIZVSDJVcNUdwWk4M2FYzN6gtt8r/S+d6zDumuMCfvjJTVRr8Q8bGQ4X/VgvyJWkgKisvUQQpw2yRsl/dg==","signatures":[{"sig":"MEQCIFOLF6wLxgvODLi846PRtJKXMh2lJKO+jmJkDVe8S9YiAiB4l4fwhg1nyoZCHOji2YUyiSJRxtebxnWL5SvsQNgYxw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":50090},"type":"module","_from":"file:/var/folders/6c/jw3y2byj3kn248398w310m2c0000gn/T/tmp.6fE9iRTTwp/openclaw-uirouter-0.0.0.tgz","engines":{"node":"^22.18.0 || >=24.11.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"lint":"oxlint --type-aware --deny-warnings src scripts test","test":"vitest run","build":"tsdown src/index.ts --format esm --dts --clean --target es2023 --no-fixedExtension","check":"pnpm run format:check && pnpm run build && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run pack:check","format":"oxfmt --write .","prepack":"pnpm run build","typecheck":"tsgo --noEmit","pack:check":"node scripts/check-pack.mjs","format:check":"oxfmt --check ."},"_npmUser":{"name":"vincentkoc","email":"vincentkoc@ieee.org"},"_resolved":"/var/folders/6c/jw3y2byj3kn248398w310m2c0000gn/T/tmp.6fE9iRTTwp/openclaw-uirouter-0.0.0.tgz","_integrity":"sha512-NTXCcIZVSDJVcNUdwWk4M2FYzN6gtt8r/S+d6zDumuMCfvjJTVRr8Q8bGQ4X/VgvyJWkgKisvUQQpw2yRsl/dg==","repository":{"url":"git+https://github.com/openclaw/uirouter.git","type":"git"},"_npmVersion":"11.16.0","description":"Small route matching, loading, and navigation state router for OpenClaw UI surfaces.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@11.9.0","devDependencies":{"oxfmt":"^0.56.0","oxlint":"^1.71.0","tsdown":"^0.22.3","vitest":"^4.1.9","typescript":"^6.0.3","@types/node":"^26.0.1","oxlint-tsgolint":"^0.23.0","@typescript/native-preview":"7.0.0-dev.20260627.2"},"_npmOperationalInternal":{"tmp":"tmp/uirouter_0.0.0_1782839028479_0.7244225275403304","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@openclaw/uirouter","version":"0.1.0","keywords":["openclaw","router","typescript","ui"],"author":{"name":"OpenClaw Team","email":"dev@openclaw.ai"},"license":"MIT","_id":"@openclaw/uirouter@0.1.0","maintainers":[{"name":"steipete","email":"steipete@gmail.com"},{"name":"vincentkoc","email":"vincentkoc@ieee.org"}],"homepage":"https://github.com/openclaw/uirouter#readme","bugs":{"url":"https://github.com/openclaw/uirouter/issues"},"dist":{"shasum":"df5033cc1ee160d7601382d0f9a4e868f26b11b2","tarball":"https://registry.npmjs.org/@openclaw/uirouter/-/uirouter-0.1.0.tgz","fileCount":6,"integrity":"sha512-w5tNj2FIukVJqJ1wt5wiDbrI6DI4tOkUbtqnnU5Fl4EgnRKFJtfHI3WrWTWTTJlXbbrwGSMptyrSZmQVdRo83Q==","signatures":[{"sig":"MEQCIG0GMJ+9ls0DkZS7rnz+NaJRMyyFYV09Oyt+hj90OgKYAiBCzdbg8IdObwliskYUn9DxtbTCrUN1Qe+zBCCQ3vPlkQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@openclaw%2fuirouter@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":50090},"type":"module","engines":{"node":"^22.18.0 || >=24.11.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"61fb4ea24131b96b7815ba3190078d840e93b2c9","scripts":{"lint":"oxlint --type-aware --deny-warnings src scripts test","test":"vitest run","build":"tsdown src/index.ts --format esm --dts --clean --target es2023 --no-fixedExtension","check":"pnpm run format:check && pnpm run build && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run pack:check","format":"oxfmt --write .","prepack":"pnpm run build","typecheck":"tsgo --noEmit","pack:check":"node scripts/check-pack.mjs","format:check":"oxfmt --check ."},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:fb9ef475-2f1a-4ee2-9842-c49fab1bbfca"}},"repository":{"url":"git+https://github.com/openclaw/uirouter.git","type":"git"},"_npmVersion":"11.13.0","description":"Small route matching, loading, and navigation state router for OpenClaw UI surfaces.","directories":{},"sideEffects":false,"_nodeVersion":"24.17.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@11.9.0","devDependencies":{"oxfmt":"^0.56.0","oxlint":"^1.71.0","tsdown":"^0.22.3","vitest":"^4.1.9","typescript":"^6.0.3","@types/node":"^26.0.1","oxlint-tsgolint":"^0.23.0","@typescript/native-preview":"7.0.0-dev.20260627.2"},"_npmOperationalInternal":{"tmp":"tmp/uirouter_0.1.0_1782839144612_0.27245875096557337","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@openclaw/uirouter","version":"0.1.1","description":"Small route matching, loading, and navigation state router for OpenClaw UI surfaces.","keywords":["openclaw","router","typescript","ui"],"homepage":"https://github.com/openclaw/uirouter#readme","bugs":{"url":"https://github.com/openclaw/uirouter/issues"},"license":"MIT","author":{"name":"OpenClaw Team","email":"dev@openclaw.ai"},"repository":{"type":"git","url":"git+https://github.com/openclaw/uirouter.git"},"type":"module","sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public","provenance":true},"scripts":{"build":"tsdown src/index.ts --format esm --dts --clean --target es2023 --no-fixedExtension","check":"pnpm run format:check && pnpm run build && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run pack:check","format":"oxfmt --write .","format:check":"oxfmt --check .","lint":"oxlint --type-aware --deny-warnings src scripts test","pack:check":"node scripts/check-pack.mjs","prepack":"pnpm run build","test":"vitest run","typecheck":"tsc --noEmit"},"devDependencies":{"@types/node":"^26.1.1","@typescript/native":"npm:typescript@^7.0.2","oxfmt":"^0.60.0","oxlint":"^1.75.0","oxlint-tsgolint":"^7.0.2001","tsdown":"^0.22.14","typescript":"npm:@typescript/typescript6@^6.0.2","vitest":"^4.1.10"},"engines":{"node":"^22.18.0 || >=24.11.0"},"packageManager":"pnpm@11.17.0","gitHead":"f5ce7c0d7c04049753b112121600b6fe1e13c99d","_id":"@openclaw/uirouter@0.1.1","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-aiZFvWmP/ndpS3em5xVtiPlj1k6asA8ueoNxGxekA1024EZlCwnICPZklG6g2sU4Wlntp9AYocLCr5/iuvL7hw==","shasum":"7dc1375d929d060eae712d5ba400e6309896dae3","tarball":"https://registry.npmjs.org/@openclaw/uirouter/-/uirouter-0.1.1.tgz","fileCount":6,"unpackedSize":50339,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@openclaw%2fuirouter@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEQFcqQvWsbeKl1f/lmQQDPdSLPSptQaxeLa5yH3eNw7AiAEkKKH8dQclboFDpIzW6/xTE9zoec+vzmzK7vx1hIkNw=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:fb9ef475-2f1a-4ee2-9842-c49fab1bbfca"}},"directories":{},"maintainers":[{"name":"steipete","email":"steipete@gmail.com"},{"name":"vincentkoc","email":"vincentkoc@ieee.org"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/uirouter_0.1.1_1786385457160_0.7266408329896494"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-30T17:03:48.332Z","modified":"2026-08-10T18:10:57.687Z","0.0.0":"2026-06-30T17:03:48.635Z","0.1.0":"2026-06-30T17:05:44.742Z","0.1.1":"2026-08-10T18:10:57.300Z"},"bugs":{"url":"https://github.com/openclaw/uirouter/issues"},"author":{"name":"OpenClaw Team","email":"dev@openclaw.ai"},"license":"MIT","homepage":"https://github.com/openclaw/uirouter#readme","keywords":["openclaw","router","typescript","ui"],"repository":{"type":"git","url":"git+https://github.com/openclaw/uirouter.git"},"description":"Small route matching, loading, and navigation state router for OpenClaw UI surfaces.","maintainers":[{"name":"steipete","email":"steipete@gmail.com"},{"name":"vincentkoc","email":"vincentkoc@ieee.org"}],"readme":"# @openclaw/uirouter\n\nSmall, framework-agnostic router for OpenClaw UI surfaces. It handles route\nmatching, lazy component loading, data loading with caching and revalidation,\nand reactive navigation state.\n\n- ESM-only, TypeScript-first, zero runtime dependencies\n- Bring-your-own history (browser, memory, or otherwise)\n- Per-route loaders with preload, invalidation, and stale reload\n- `notFound` / `redirect` loader control flow\n- Fine-grained subscriptions (whole state, selector, or single match)\n\n## Install\n\n```sh\npnpm add @openclaw/uirouter\n# or\nnpm install @openclaw/uirouter\n# or\nyarn add @openclaw/uirouter\n```\n\nRequires Node `^22.18.0 || >=24.11.0`. The package ships ESM and TypeScript\ndeclarations only — there is no CommonJS build.\n\n## Quick start\n\n```ts\nimport { createRouter, definePage } from \"@openclaw/uirouter\";\n\nconst home = definePage({\n  id: \"home\",\n  path: \"/\",\n  component: () => import(\"./pages/home.js\"),\n});\n\nconst chat = definePage({\n  id: \"chat\",\n  path: \"/chat\",\n  component: () => import(\"./pages/chat.js\"),\n  loader: async (context, { signal }) => {\n    const response = await fetch(`/api/threads/${context.userId}`, { signal });\n    return response.json();\n  },\n});\n\nconst router = createRouter<\"home\" | \"chat\", { userId: string }>({\n  routes: [home, chat],\n});\n\nawait router.navigate(\"chat\", { userId: \"u_1\" });\nconst { matches } = router.getState();\n```\n\nThe first generic is the union of route ids; the second is the loader\n**context** type — an arbitrary value you pass into every navigation so loaders\nand hooks can read from session, auth, or DI without reaching for globals.\n\n## History integration\n\nThe router does not bind to `window.history` directly. Provide a `RouterHistory`\nadapter and call `router.start(history, basePath, context)`:\n\n```ts\nimport type { RouterHistory, RouteLocation } from \"@openclaw/uirouter\";\n\nconst browserHistory: RouterHistory = {\n  location: () => ({\n    pathname: window.location.pathname,\n    search: window.location.search,\n    hash: window.location.hash,\n  }),\n  push: (loc) => window.history.pushState(null, \"\", serialize(loc)),\n  replace: (loc) => window.history.replaceState(null, \"\", serialize(loc)),\n  listen: (listener) => {\n    const onPop = () => listener(browserHistory.location());\n    window.addEventListener(\"popstate\", onPop);\n    return () => window.removeEventListener(\"popstate\", onPop);\n  },\n};\n\nfunction serialize(loc: RouteLocation): string {\n  return `${loc.pathname}${loc.search}${loc.hash}`;\n}\n\nawait router.start(browserHistory, \"/app\", { userId: \"u_1\" });\n```\n\n`start` matches the current location, runs its loader, and subscribes to\nhistory changes. Call `router.stop()` to detach and clear caches.\n\nFor programmatic navigation, use `navigate(routeId, context, options)` or\n`navigateLocation(location, context)`. Pass `{ history: \"push\" | \"replace\" }`\nto have the router update the underlying history.\n\n## Loaders, deps, and caching\n\nA `loader` returns the route data; `loaderDeps` derives a string key from\ncontext and location. Two navigations that produce the same `(routeId, deps)`\nshare a match, so dependency-driven re-fetching is just a matter of returning\na different deps string.\n\n```ts\ndefinePage({\n  id: \"thread\",\n  path: \"/thread\",\n  component: () => import(\"./pages/thread.js\"),\n  loaderDeps: (_, location) => new URLSearchParams(location.search).get(\"id\") ?? \"\",\n  loader: async (context, { signal, deps }) => {\n    const response = await fetch(`/api/threads/${deps}`, { signal });\n    return response.json();\n  },\n  staleTime: 30_000,\n  gcTime: 5 * 60_000,\n});\n```\n\nPer-route cache knobs (all optional, all in milliseconds):\n\n| Option             | Default      | Meaning                                                             |\n| ------------------ | ------------ | ------------------------------------------------------------------- |\n| `staleTime`        | `0`          | How long a successful match is considered fresh.                    |\n| `staleReloadMode`  | `background` | `background`: show cached data and refetch; `blocking`: wait.       |\n| `preloadStaleTime` | `30_000`     | Freshness for matches produced by `preloadRoute`/`preloadLocation`. |\n| `gcTime`           | `30 min`     | How long an unused cached match is kept.                            |\n| `preloadGcTime`    | `30 min`     | GC for preloaded matches.                                           |\n\nThe same defaults can be set router-wide via `createRouter({ staleTime, defaultStaleReloadMode, preloadStaleTime, preloadGcTime, gcTime })`.\n\n## Redirects and not-found\n\nLoaders signal control flow by **throwing** the result of `redirect()` or\n`notFound()`. Returning them works too; the router treats both equivalently.\n\n```ts\nimport { definePage, notFound, redirect } from \"@openclaw/uirouter\";\n\ndefinePage({\n  id: \"thread\",\n  path: \"/thread\",\n  component: () => import(\"./pages/thread.js\"),\n  loader: async (context, { signal }) => {\n    if (!context.session) {\n      throw redirect({ pathname: \"/login\", search: \"\", hash: \"\" });\n    }\n    const response = await fetch(`/api/thread`, { signal });\n    if (response.status === 404) throw notFound({ reason: \"thread-missing\" });\n    return response.json();\n  },\n});\n```\n\nWhen a redirect is thrown during a real navigation (not a preload), the router\nchases it with `history: \"replace\"`. A `notFound` sets the router status to\n`\"notFound\"` and exposes the payload on the match's `error`.\n\nUnmatched locations also produce `notFound` router state. Applications decide\nhow to present or redirect that state; the router does not choose a default\nroute.\n\n## Subscriptions\n\n`getState()` returns the current `RouterState`. To react to changes:\n\n```ts\nconst unsubscribe = router.subscribe((state) => {\n  render(state.matches[0]);\n});\n\n// Only fire when status changes\nrouter.subscribeSelector(\n  (state) => state.status,\n  (status) => console.log(status),\n);\n\n// Watch a single match by id (e.g. for a preloaded route)\nrouter.subscribeMatch(matchId, (match) => {\n  if (match?.status === \"success\") prefetchAssets(match.module);\n});\n```\n\nSelector subscriptions use `Object.is` by default; pass a custom `equal`\ncomparator for structural checks.\n\n## Preloading, invalidation, and revalidation\n\n```ts\nawait router.preloadRoute(\"chat\", context);\nawait router.preloadLocation({ pathname: \"/chat\", search: \"?t=1\", hash: \"\" }, context);\n\nawait router.invalidate(); // mark all matches stale\nawait router.invalidate(\"chat\"); // single route\nawait router.revalidate(context); // force refetch of the active match\n```\n\n`preload*` populates the cache without making the route active. If a cached\nmatch is fresh on the next navigation, it's promoted instantly; otherwise the\nrouter refetches in the background or blocks per `staleReloadMode`.\n\n## Lifecycle hooks\n\n`onEnter` runs after a successful navigation; `onLeave` runs when the previous\nmatch is being replaced by a different route. Both receive the load context,\nthe resolved data, and the standard `RouteHookOptions` (signal, location,\ndeps, cause, `shouldRun`).\n\n```ts\ndefinePage({\n  id: \"chat\",\n  path: \"/chat\",\n  component: () => import(\"./pages/chat.js\"),\n  onEnter: (context, data) => analytics.pageview(\"chat\", data),\n  onLeave: () => analytics.flush(),\n});\n```\n\nIf a hook throws, the match transitions to `\"error\"` and the error propagates\nout of the originating `navigate` call.\n\n## API reference\n\n### `createRouter(options)`\n\nReturns a `Router`. Options:\n\n- `routes: PageDefinition[]` — required.\n- `staleTime`, `defaultStaleReloadMode`, `preloadStaleTime`,\n  `preloadGcTime`, `gcTime` — router-wide defaults.\n\n### `Router`\n\n| Member                                            | Purpose                                              |\n| ------------------------------------------------- | ---------------------------------------------------- |\n| `routes`                                          | Compiled, normalized route definitions.              |\n| `getRoute(id)`                                    | Lookup a `PageDefinition` by id.                     |\n| `getMatch(matchId)`                               | Lookup a match across active/pending/cached pools.   |\n| `getState()`                                      | Current `RouterState`.                               |\n| `subscribe(listener)`                             | Subscribe to state changes.                          |\n| `subscribeSelector(selector, listener, equal?)`   | Subscribe to a derived slice.                        |\n| `subscribeMatch(matchId, listener)`               | Subscribe to a single match.                         |\n| `pathForRoute(id, basePath?)`                     | Build a URL pathname for a route.                    |\n| `routeIdFromPath(pathname, basePath?)`            | Resolve a path to a route id, or `null`.             |\n| `start(history, basePath, context)`               | Attach to history and load the current location.     |\n| `navigate(routeId, context, options?, location?)` | Navigate to a route.                                 |\n| `navigateLocation(location, context)`             | Navigate to an arbitrary location.                   |\n| `preloadRoute(routeId, context)`                  | Warm the cache for a route.                          |\n| `preloadLocation(location, context)`              | Warm the cache for a location.                       |\n| `revalidate(context, routeId?)`                   | Force refetch of the active or named route.          |\n| `invalidate(routeId?)`                            | Mark all (or one) match(es) stale.                   |\n| `stop()`                                          | Detach history, abort in-flight loads, clear caches. |\n\n### `definePage(page)`\n\nIdentity helper that returns its argument while inferring the strongest\ngeneric types. Use it instead of plain object literals so route ids stay\nnarrowed.\n\n### `notFound(data?)` / `redirect(location)`\n\nConstruct control-flow values for loaders. Throw them (or return them) from a\n`loader` to short-circuit a navigation.\n\n### Types\n\nThe package exports types for every shape it consumes or produces:\n\n`PageDefinition`, `Router`, `RouterOptions`, `RouterState`, `RouterHistory`,\n`RouterNavigationOptions`, `RouterStateSelector`, `RouteMatch`,\n`RouteMatchStatus`, `RouteMatchFetching`, `RouteLocation`, `RouteLoadCause`,\n`RouteLoaderOptions`, `RouteLoaderResult`, `RouteHookOptions`, `RouteNotFound`,\n`RouteRedirect`, `MaybePromise`.\n\nPath helpers: `normalizeRoutePath`, `normalizeRouteBasePath`.\n\n## Scripts\n\n```sh\npnpm install\npnpm run build\npnpm run typecheck\npnpm run lint\npnpm run test\npnpm run check\n```\n\n## License\n\n[MIT](LICENSE) © OpenClaw\n","readmeFilename":"README.md"}