{
  "$schema": "https://hyperjs.ai/schema/registry-item.json",
  "name": "client",
  "version": "0.1.0",
  "description": "Typed RPC client + codegen for Hyper.",
  "readme": "# @hyper/client\n\nTyped RPC client + codegen for Hyper apps. Optional TanStack Query bindings at `@hyper/client/tanstack-query`.\n\n## Install\n\nComponents are installed as source into your repo, not pulled from npm:\n\n```bash\nbunx hyper add client\n```\n\nWires the alias `@hyper/client` to `src/hyper/client/` (configurable in `hyper.config.json`). See [hyperjs.ai](https://hyperjs.ai) for the full registry.\n\n## Usage\n\nRuntime client:\n\n```ts\nimport { createClient, fetchTransport } from \"@hyper/client\"\n\nconst c = createClient(fetchTransport({ baseUrl: \"https://api.example.com\" }))\n\nconst res = await c.call({ method: \"GET\", path: \"/users/:id\", params: { id: \"abc\" } })\n```\n\nType-safe client via codegen:\n\n```bash\nbun x hyper client ./src/generated/client.ts\n```\n\n```ts\nimport { client } from \"./generated/client.ts\"\nconst me = await client.users.show({ id: \"abc\" })\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
  "registryDeps": [
    "core"
  ],
  "peerDeps": {},
  "optionalPeerDeps": {},
  "files": [
    {
      "path": "client/client.ts",
      "contents": "/**\n * createClient — a typed-ish RPC client. The generics are threaded so that\n * if you hand a `PlainRouter` shape to `createClient`, you get dot-paths.\n *\n * Example:\n *   const api = createClient<typeof router>(fetchTransport({ baseUrl }))\n *   await api.users.list()\n *   await api.users.create({ title: \"x\" })\n */\n\nimport type { Transport } from \"./types.ts\"\n\n// Stand-alone clone of the core PlainRouter shape to avoid a type dep.\ntype RouterLike = {\n  // biome-ignore lint/suspicious/noExplicitAny: structural router\n  [key: string]: any\n}\n\ntype CallableRouteLike = {\n  readonly method: string\n  readonly path: string\n  readonly meta?: unknown\n}\n\n/**\n * We expose the transport directly. Codegen emits a thin wrapper with\n * the proper TypeScript shape for the user; the runtime is identical.\n */\nexport interface ClientContract {\n  call<T = unknown>(input: {\n    method: string\n    path: string\n    params?: Record<string, string>\n    query?: Record<string, unknown>\n    body?: unknown\n    headers?: Record<string, string>\n    signal?: AbortSignal\n  }): Promise<T>\n}\n\nexport function createClient(transport: Transport): ClientContract {\n  return {\n    async call(input) {\n      const path = applyPathParams(input.path, input.params)\n      const url = input.query ? `${path}?${new URLSearchParams(stringifyQuery(input.query))}` : path\n      const res = await transport.request({\n        method: input.method,\n        url,\n        ...(input.headers ? { headers: input.headers } : {}),\n        ...(input.body !== undefined ? { body: input.body } : {}),\n        ...(input.signal ? { signal: input.signal } : {}),\n      })\n      if (res.status >= 400) {\n        const err = extractError(res.data, res.status)\n        throw Object.assign(new Error(err.message), err)\n      }\n      return res.data as never\n    },\n  }\n}\n\nexport function applyPathParams(path: string, params: Record<string, string> | undefined): string {\n  if (!params) return path\n  return path.replace(/:([A-Za-z0-9_]+)/g, (_, k: string) => {\n    const v = params[k]\n    if (v === undefined) throw new Error(`missing path param :${k}`)\n    return encodeURIComponent(v)\n  })\n}\n\nfunction stringifyQuery(q: Record<string, unknown>): Record<string, string> {\n  const out: Record<string, string> = {}\n  for (const [k, v] of Object.entries(q)) {\n    if (v === undefined || v === null) continue\n    out[k] = String(v)\n  }\n  return out\n}\n\nfunction extractError(\n  data: unknown,\n  status: number,\n): { status: number; code: string; message: string } {\n  if (data && typeof data === \"object\" && \"error\" in data) {\n    const e = (data as { error: Record<string, unknown> }).error\n    return {\n      status,\n      code: typeof e.code === \"string\" ? e.code : \"unknown\",\n      message: typeof e.message === \"string\" ? e.message : `HTTP ${status}`,\n    }\n  }\n  return { status, code: \"unknown\", message: `HTTP ${status}` }\n}\n\n/**\n * routerToClient — mirrors a plain-object router tree at runtime, replacing\n * each CallableRoute leaf with a function that invokes the transport.\n * Codegen emits a static `.d.ts` equivalent; this is the runtime twin.\n */\nexport function routerToClient<R extends RouterLike>(router: R, transport: Transport): RouterLike {\n  const client = createClient(transport)\n  const walk = (node: unknown): unknown => {\n    if (!node || typeof node !== \"object\") return node\n    if (isRoute(node)) {\n      const r = node as CallableRouteLike\n      return async (input?: {\n        params?: Record<string, string>\n        query?: Record<string, unknown>\n        body?: unknown\n        headers?: Record<string, string>\n        signal?: AbortSignal\n      }) =>\n        client.call({\n          method: r.method,\n          path: r.path,\n          ...(input?.params && { params: input.params }),\n          ...(input?.query && { query: input.query }),\n          ...(input?.body !== undefined && { body: input.body }),\n          ...(input?.headers && { headers: input.headers }),\n          ...(input?.signal && { signal: input.signal }),\n        })\n    }\n    const out: Record<string, unknown> = {}\n    for (const [k, v] of Object.entries(node as Record<string, unknown>)) out[k] = walk(v)\n    return out\n  }\n  return walk(router) as RouterLike\n}\n\nfunction isRoute(x: unknown): boolean {\n  return Boolean(\n    x &&\n      typeof x === \"object\" &&\n      typeof (x as { method?: unknown }).method === \"string\" &&\n      typeof (x as { path?: unknown }).path === \"string\" &&\n      typeof (x as { handler?: unknown }).handler === \"function\",\n  )\n}\n",
      "sha256": "718e43d48228b4a29c4e00d01ab4b02c31330968e6d466c753a2554d76f0f04d"
    },
    {
      "path": "client/codegen.ts",
      "contents": "/**\n * Codegen — consumes a `ClientManifest` (from @hyper/core) and emits a\n * pair of files:\n *\n *   client.ts       — a runtime proxy wrapping fetchTransport\n *   client.d.ts     — compile-time types for every route\n *\n * This is the moral equivalent of tRPC's router typings, minus reflection.\n *\n * Live-watch integration (hyper dev) is added in the CLI; this module stays\n * a pure function so it can be called from tests and from build pipelines.\n */\n\nimport type { ClientManifest } from \"@hyper/core\"\n\nexport interface CodegenOptions {\n  readonly manifest: ClientManifest\n  /** Default baseUrl embedded in the emitted file (overridable at runtime). */\n  readonly baseUrl?: string\n  /** Name for the generated root client (defaults to \"api\"). */\n  readonly rootName?: string\n  /**\n   * Emit `Result<T, Errors>` tagged-union leaf signatures alongside the\n   * exception-throwing leaves. Off by default for minimum-diff ergonomics;\n   * apps that prefer explicit error handling should opt in.\n   */\n  readonly resultTypes?: boolean\n}\n\nexport function generateClient(opts: CodegenOptions): {\n  runtime: string\n  declaration: string\n} {\n  const root = opts.rootName ?? \"api\"\n  const resultTypes = opts.resultTypes ?? false\n  const tree = buildTree(opts.manifest.routes)\n  const dts = emitDts(tree, root, resultTypes)\n  const js = emitRuntime(tree, root, opts.baseUrl, resultTypes)\n  return { runtime: js, declaration: dts }\n}\n\n// --- path-to-namespace tree --------------------------------------------------\n\ninterface Leaf {\n  readonly kind: \"leaf\"\n  readonly method: string\n  readonly path: string\n  readonly name: string\n  readonly errors?: readonly string[]\n  readonly throws?: readonly number[]\n}\ninterface Branch {\n  readonly kind: \"branch\"\n  readonly children: Map<string, Leaf | Branch>\n}\n\ntype Node = Leaf | Branch\n\nfunction buildTree(routes: readonly ClientManifest[\"routes\"][number][]): Branch {\n  const root: Branch = { kind: \"branch\", children: new Map() }\n  for (const r of routes) {\n    const name = r.name ?? defaultName(r.method, r.path)\n    const segments = splitName(name)\n    let cur: Branch = root\n    for (let i = 0; i < segments.length - 1; i++) {\n      const seg = segments[i]!\n      let next = cur.children.get(seg)\n      if (!next || next.kind !== \"branch\") {\n        next = { kind: \"branch\", children: new Map() }\n        cur.children.set(seg, next)\n      }\n      cur = next\n    }\n    cur.children.set(segments[segments.length - 1]!, {\n      kind: \"leaf\",\n      method: r.method,\n      path: r.path,\n      name,\n      ...(r.errors && r.errors.length > 0 && { errors: r.errors }),\n      ...(r.throws && r.throws.length > 0 && { throws: r.throws }),\n    })\n  }\n  return root\n}\n\nfunction splitName(name: string): string[] {\n  return name\n    .replace(/[^A-Za-z0-9._]+/g, \"_\")\n    .split(/[._]/)\n    .filter((s) => s.length > 0)\n}\n\nfunction defaultName(method: string, path: string): string {\n  return `${method.toLowerCase()}_${path.replace(/[^A-Za-z0-9]+/g, \"_\")}`\n}\n\n// --- emitters ---------------------------------------------------------------\n\nfunction emitRuntime(\n  root: Branch,\n  rootName: string,\n  baseUrl: string | undefined,\n  resultTypes: boolean,\n): string {\n  const leafHelper = resultTypes\n    ? `const leaf = (method, path) => async (input = {}) => {\\n    try {\\n      return { ok: true, value: await client.call({ method, path, ...input }) }\\n    } catch (e) {\\n      return { ok: false, error: { code: e.code ?? \"unknown\", status: e.status ?? 500, message: e.message ?? String(e) } }\\n    }\\n  }`\n    : \"const leaf = (method, path) => (input = {}) => client.call({ method, path, ...input })\"\n  const header = `/* eslint-disable */\\n/* Generated by @hyper/client/codegen. Do not edit. */\\nimport { createClient } from \"@hyper/client\"\\nimport { fetchTransport } from \"@hyper/client\"\\n\\nexport function build(opts = {}) {\\n  const transport = opts.transport ?? fetchTransport({ baseUrl: opts.baseUrl ?? ${JSON.stringify(baseUrl ?? \"\")}, headers: opts.headers })\\n  const client = createClient(transport)\\n  ${leafHelper}\\n  return ${runtimeLiteral(root)}\\n}\\n\\nexport const ${rootName} = build()\\n`\n  return header\n}\n\nfunction runtimeLiteral(node: Node): string {\n  if (node.kind === \"leaf\")\n    return `leaf(${JSON.stringify(node.method)}, ${JSON.stringify(node.path)})`\n  const entries: string[] = []\n  for (const [k, child] of node.children) {\n    entries.push(`  ${safeKey(k)}: ${runtimeLiteral(child)}`)\n  }\n  return `{\\n${entries.join(\",\\n\")}\\n}`\n}\n\nfunction emitDts(root: Branch, rootName: string, resultTypes: boolean): string {\n  const preamble = resultTypes\n    ? `export type Ok<T> = { readonly ok: true; readonly value: T }\\nexport type Err<C extends string> = { readonly ok: false; readonly error: { readonly code: C; readonly status: number; readonly message: string } }\\nexport type Result<T, C extends string> = Ok<T> | Err<C>\\nexport type Leaf<T = unknown, C extends string = \"unknown\"> = (input?: Input) => Promise<Result<T, C>>\\n`\n    : \"export type Leaf<T = unknown> = (input?: Input) => Promise<T>\\n\"\n  const header = `/* Generated by @hyper/client/codegen. Do not edit. */\\nimport type { Transport } from \"@hyper/client\"\\n\\nexport type Input = {\\n  params?: Record<string, string>\\n  query?: Record<string, unknown>\\n  body?: unknown\\n  headers?: Record<string, string>\\n  signal?: AbortSignal\\n}\\n\\n${preamble}\\nexport type Client = ${dtsLiteral(root, 0, resultTypes)}\\n\\nexport const ${rootName}: Client\\nexport function build(opts?: { baseUrl?: string; headers?: Record<string, string>; transport?: Transport }): Client\\n`\n  return header\n}\n\nfunction dtsLiteral(node: Node, indent: number, resultTypes: boolean): string {\n  if (node.kind === \"leaf\") {\n    if (!resultTypes) return \"Leaf\"\n    const codes = [...(node.errors ?? []), ...(node.throws ?? []).map((n) => `http_${n}`)]\n    if (codes.length === 0) return \"Leaf\"\n    const union = codes.map((c) => JSON.stringify(c)).join(\" | \")\n    return `Leaf<unknown, ${union}>`\n  }\n  const pad = \"  \".repeat(indent + 1)\n  const entries: string[] = []\n  for (const [k, child] of node.children) {\n    entries.push(`${pad}${safeKey(k)}: ${dtsLiteral(child, indent + 1, resultTypes)}`)\n  }\n  return `{\\n${entries.join(\"\\n\")}\\n${\"  \".repeat(indent)}}`\n}\n\nfunction safeKey(k: string): string {\n  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : JSON.stringify(k)\n}\n",
      "sha256": "9de687b7462628bfb5727289445d85ea720798073baf5ed2d79a8312d26e24bc"
    },
    {
      "path": "client/index.ts",
      "contents": "/**\n * @hyper/client — typed RPC client + codegen for Hyper apps.\n *\n * Runtime:  createClient(transport) gives a `.call({ method, path, ... })` primitive.\n * Typed:    `hyper client <out>` emits `client.ts` + `client.d.ts` from the\n *           running app's `toClientManifest()`.\n * TanStack: `@hyper/client/tanstack-query` ships queryOptions / mutationOptions.\n *\n * Exports are ergonomic re-exports; see individual files for detail.\n */\n\nexport { applyPathParams, createClient, routerToClient } from \"./client.ts\"\nexport type { ClientContract } from \"./client.ts\"\nexport { generateClient } from \"./codegen.ts\"\nexport type { CodegenOptions } from \"./codegen.ts\"\nexport { subscribe } from \"./sse.ts\"\nexport type { SubscribeOptions } from \"./sse.ts\"\nexport { fetchTransport } from \"./transport.ts\"\nexport type { FetchTransportConfig } from \"./transport.ts\"\nexport type {\n  HyperRpcError,\n  Result,\n  Transport,\n  TransportRequest,\n  TransportResponse,\n} from \"./types.ts\"\n\n// Re-export core typing utilities for convenience\nexport type {\n  InferRouterCtx,\n  InferRouterInputs,\n  InferRouterOutputs,\n} from \"@hyper/core\"\n",
      "sha256": "1ba23c2698106352d179cc209fe1c263cd7956663ca38cbb83a93fc9a3ee2fbe"
    },
    {
      "path": "client/sse.ts",
      "contents": "/**\n * SSE subscribe helper.\n *\n *   for await (const event of subscribe(\"/events\", { baseUrl })) { ... }\n */\n\nexport interface SubscribeOptions {\n  readonly baseUrl?: string\n  readonly headers?: Record<string, string>\n  readonly signal?: AbortSignal\n  readonly fetch?: typeof fetch\n}\n\nexport async function* subscribe(\n  path: string,\n  opts: SubscribeOptions = {},\n): AsyncGenerator<{ type: string; data: string; id?: string }> {\n  const url = opts.baseUrl\n    ? `${opts.baseUrl.replace(/\\/+$/, \"\")}${path.startsWith(\"/\") ? path : `/${path}`}`\n    : path\n  const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis)\n  const res = await fetchImpl(url, {\n    headers: { accept: \"text/event-stream\", ...opts.headers },\n    ...(opts.signal ? { signal: opts.signal } : {}),\n  })\n  if (!res.body) throw new Error(\"sse: empty response body\")\n\n  const decoder = new TextDecoder()\n  const reader = res.body.getReader()\n  let buf = \"\"\n  try {\n    while (true) {\n      const { value, done } = await reader.read()\n      if (done) break\n      buf += decoder.decode(value, { stream: true })\n      let idx = buf.indexOf(\"\\n\\n\")\n      while (idx >= 0) {\n        const raw = buf.slice(0, idx)\n        buf = buf.slice(idx + 2)\n        yield parseSseEvent(raw)\n        idx = buf.indexOf(\"\\n\\n\")\n      }\n    }\n  } finally {\n    reader.releaseLock()\n  }\n}\n\nfunction parseSseEvent(raw: string): { type: string; data: string; id?: string } {\n  let type = \"message\"\n  const dataLines: string[] = []\n  let id: string | undefined\n  for (const line of raw.split(\"\\n\")) {\n    if (!line || line.startsWith(\":\")) continue\n    const colon = line.indexOf(\":\")\n    const k = colon < 0 ? line : line.slice(0, colon)\n    const v = colon < 0 ? \"\" : line.slice(colon + 1).replace(/^ /, \"\")\n    if (k === \"event\") type = v\n    else if (k === \"data\") dataLines.push(v)\n    else if (k === \"id\") id = v\n  }\n  return { type, data: dataLines.join(\"\\n\"), ...(id !== undefined && { id }) }\n}\n",
      "sha256": "695238d750814c2400552b29fc993cff16ad4b7732ea89f302d9528164aefe2d"
    },
    {
      "path": "client/tanstack-query.ts",
      "contents": "/**\n * TanStack Query helpers — tiny wrappers that build queryKey/queryFn or\n * mutationFn pairs from a generated client function.\n *\n * These do not depend on @tanstack/react-query at compile time; the user\n * provides the types via the helper's generics.\n */\n\ntype Leaf<T = unknown> = (input?: {\n  params?: Record<string, string>\n  query?: Record<string, unknown>\n  body?: unknown\n  headers?: Record<string, string>\n  signal?: AbortSignal\n}) => Promise<T>\n\nexport function queryOptions<T>(\n  leaf: Leaf<T>,\n  input?: Parameters<Leaf<T>>[0],\n): { queryKey: readonly unknown[]; queryFn: () => Promise<T> } {\n  return {\n    queryKey: [leaf.name || \"hyper\", input],\n    queryFn: () => leaf(input),\n  }\n}\n\nexport function mutationOptions<T, I extends Parameters<Leaf<T>>[0] | undefined>(\n  leaf: Leaf<T>,\n): { mutationFn: (input: I) => Promise<T> } {\n  return {\n    mutationFn: (input: I) => leaf(input),\n  }\n}\n",
      "sha256": "53d024578c4d0fa88084230f63d5a732bafc7b48ff4f61c02b45005649c43e1d"
    },
    {
      "path": "client/transport.ts",
      "contents": "/**\n * Default fetch-backed transport.\n *\n * Supports JSON by default; a wire-format hook (MessagePack, etc.) can\n * be added by passing a custom `encode`/`decode` pair.\n */\n\nimport type { Transport, TransportRequest, TransportResponse } from \"./types.ts\"\n\nexport interface FetchTransportConfig {\n  readonly baseUrl: string\n  readonly headers?: Record<string, string>\n  readonly fetch?: typeof fetch\n  readonly encode?: (body: unknown) => BodyInit | undefined\n  readonly decode?: (res: Response) => Promise<unknown>\n}\n\nconst defaultEncode = (body: unknown): BodyInit | undefined => {\n  if (body === undefined || body === null) return undefined\n  if (typeof body === \"string\" || body instanceof ArrayBuffer || body instanceof Uint8Array) {\n    return body as BodyInit\n  }\n  return JSON.stringify(body)\n}\n\nconst defaultDecode = async (res: Response): Promise<unknown> => {\n  const ct = res.headers.get(\"content-type\") ?? \"\"\n  if (ct.includes(\"application/json\")) return res.json()\n  if (ct.startsWith(\"text/\")) return res.text()\n  if (res.status === 204) return undefined\n  return res.arrayBuffer()\n}\n\nexport function fetchTransport(cfg: FetchTransportConfig): Transport {\n  const encode = cfg.encode ?? defaultEncode\n  const decode = cfg.decode ?? defaultDecode\n  const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis)\n  return {\n    async request(input: TransportRequest): Promise<TransportResponse> {\n      const headers: Record<string, string> = {\n        \"content-type\": \"application/json\",\n        ...cfg.headers,\n        ...input.headers,\n      }\n      const body = encode(input.body)\n      const res = await fetchImpl(joinUrl(cfg.baseUrl, input.url), {\n        method: input.method,\n        headers,\n        ...(body !== undefined && { body }),\n        ...(input.signal ? { signal: input.signal } : {}),\n      })\n      const data = await decode(res)\n      return { status: res.status, data, headers: res.headers }\n    },\n  }\n}\n\nfunction joinUrl(base: string, path: string): string {\n  if (/^https?:/.test(path)) return path\n  const cleanBase = base.replace(/\\/+$/, \"\")\n  const cleanPath = path.startsWith(\"/\") ? path : `/${path}`\n  return `${cleanBase}${cleanPath}`\n}\n",
      "sha256": "ebc67de8a01e3a166e817f615c05d28f5f6a4d979e792b0c37b94f4e08323c65"
    },
    {
      "path": "client/types.ts",
      "contents": "/**\n * Client-side types.\n */\n\n/** Transport adapter — can swap fetch for something else (MessagePack, etc). */\nexport interface Transport {\n  readonly request: (input: TransportRequest) => Promise<TransportResponse>\n}\n\nexport interface TransportRequest {\n  readonly method: string\n  readonly url: string\n  readonly headers?: Record<string, string>\n  readonly body?: unknown\n  readonly signal?: AbortSignal\n}\n\nexport interface TransportResponse {\n  readonly status: number\n  readonly data: unknown\n  readonly headers: Headers\n}\n\n/** Standard error shape emitted by Hyper's HTTP layer. */\nexport interface HyperRpcError {\n  readonly status: number\n  readonly code: string\n  readonly message: string\n  readonly why?: string\n  readonly fix?: string\n  readonly details?: unknown\n}\n\n/** Result union for `.throws()`/`.errors()`. */\nexport type Result<T, E = HyperRpcError> =\n  | { readonly ok: true; readonly data: T }\n  | { readonly ok: false; readonly error: E }\n",
      "sha256": "b2f5bc51bddd81c066621f7cd24fea7a8a5d724542b05b868022dfa6a164db57"
    }
  ],
  "subpaths": {}
}