silgi 0.1.0-beta.11 → 0.1.0-beta.13

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.
@@ -5,10 +5,6 @@ interface RPCLinkOptions<TClientContext extends ClientContext = ClientContext> {
5
5
  url: string | URL;
6
6
  headers?: Record<string, string> | ((options: ClientOptions<TClientContext>) => Record<string, string>);
7
7
  fetch?: typeof globalThis.fetch;
8
- method?: 'GET' | 'POST';
9
- maxUrlLength?: number;
10
- /** Route metadata — pass extractRoutes(router) or the full router. Required when procedures use $route({ path }) */
11
- routes?: unknown;
12
8
  }
13
9
  declare class RPCLink<TClientContext extends ClientContext = ClientContext> implements ClientLink<TClientContext> {
14
10
  #private;
@@ -1,5 +1,4 @@
1
1
  import { SilgiError, fromSilgiErrorJSON, isErrorStatus, isSilgiErrorJSON } from "../../../core/error.mjs";
2
- import { resolveRoute, substituteParams } from "../../../core/router-utils.mjs";
3
2
  import { parseEmptyableJSON, stringifyJSON } from "../../../core/utils.mjs";
4
3
  //#region src/client/adapters/fetch/index.ts
5
4
  /**
@@ -9,46 +8,22 @@ var RPCLink = class {
9
8
  #baseUrl;
10
9
  #headers;
11
10
  #fetch;
12
- #method;
13
- #maxUrlLength;
14
- #routes;
15
11
  constructor(options) {
16
12
  this.#baseUrl = typeof options.url === "string" ? options.url : options.url.href;
17
13
  if (this.#baseUrl.endsWith("/")) this.#baseUrl = this.#baseUrl.slice(0, -1);
18
14
  this.#headers = options.headers;
19
15
  this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
20
- this.#method = options.method ?? "POST";
21
- this.#maxUrlLength = options.maxUrlLength ?? 2083;
22
- this.#routes = options.routes;
23
16
  }
24
17
  async call(path, input, options) {
25
- const resolved = this.#routes ? resolveRoute(this.#routes, path) : void 0;
26
- let urlPath = resolved ? resolved.path : "/" + path.map(encodeURIComponent).join("/");
27
- if (resolved) {
28
- const sub = substituteParams(urlPath, input);
29
- urlPath = sub.url;
30
- input = sub.remainingInput;
31
- }
32
- let url = `${this.#baseUrl}${urlPath}`;
18
+ const url = `${this.#baseUrl}/${path.map(encodeURIComponent).join("/")}`;
33
19
  const headers = { ...typeof this.#headers === "function" ? this.#headers(options) : this.#headers };
34
- let method = resolved?.method ?? this.#method;
35
20
  let body;
36
- const hasInput = input !== void 0 && input !== null;
37
- if (method === "GET" && hasInput) {
38
- const data = stringifyJSON(input);
39
- const candidateUrl = `${url}?data=${encodeURIComponent(data)}`;
40
- if (candidateUrl.length <= this.#maxUrlLength) url = candidateUrl;
41
- else {
42
- method = "POST";
43
- headers["content-type"] = "application/json";
44
- body = data;
45
- }
46
- } else if (hasInput) {
21
+ if (input !== void 0 && input !== null) {
47
22
  headers["content-type"] = "application/json";
48
23
  body = stringifyJSON(input);
49
24
  }
50
25
  const response = await this.#fetch(url, {
51
- method,
26
+ method: "POST",
52
27
  headers,
53
28
  body,
54
29
  signal: options.signal
@@ -36,8 +36,6 @@ interface LinkOptions<TClientContext extends ClientContext = ClientContext> {
36
36
  onResponse?: FetchOptions['onResponse'];
37
37
  onRequestError?: FetchOptions['onRequestError'];
38
38
  onResponseError?: FetchOptions['onResponseError'];
39
- /** Route metadata — pass extractRoutes(router) or the full router. Required when procedures use $route({ path }) */
40
- routes?: unknown;
41
39
  }
42
40
  /**
43
41
  * Create a Silgi client link powered by ofetch.
@@ -1,5 +1,4 @@
1
1
  import { SilgiError, fromSilgiErrorJSON, isSilgiErrorJSON } from "../../../core/error.mjs";
2
- import { resolveRoute, substituteParams } from "../../../core/router-utils.mjs";
3
2
  import { MSGPACK_CONTENT_TYPE, decode, encode } from "../../../codec/msgpack.mjs";
4
3
  import { FetchError, ofetch } from "ofetch";
5
4
  //#region src/client/adapters/ofetch/index.ts
@@ -28,16 +27,8 @@ function createLink(options) {
28
27
  const defaultRetry = options.retry;
29
28
  const defaultRetryDelay = options.retryDelay ?? 0;
30
29
  const resolvedProtocol = options.protocol ?? (options.binary ? "messagepack" : void 0) ?? (options.devalue ? "devalue" : void 0) ?? "json";
31
- const routes = options.routes;
32
30
  return { async call(path, input, callOptions) {
33
- const resolved = routes ? resolveRoute(routes, path) : void 0;
34
- let urlPath = resolved ? resolved.path : "/" + path.map(encodeURIComponent).join("/");
35
- if (resolved) {
36
- const sub = substituteParams(urlPath, input);
37
- urlPath = sub.url;
38
- input = sub.remainingInput;
39
- }
40
- const url = `${baseUrl}${urlPath}`;
31
+ const url = `${baseUrl}/${path.map(encodeURIComponent).join("/")}`;
41
32
  const headers = { ...typeof options.headers === "function" ? options.headers(callOptions) : options.headers };
42
33
  let body;
43
34
  if (resolvedProtocol === "messagepack") {
@@ -52,7 +43,7 @@ function createLink(options) {
52
43
  } else body = input !== void 0 && input !== null ? input : void 0;
53
44
  try {
54
45
  const data = await ofetch(url, {
55
- method: resolved?.method ?? "POST",
46
+ method: "POST",
56
47
  headers,
57
48
  body,
58
49
  signal: callOptions.signal,
@@ -13,7 +13,7 @@ import { SilgiError } from "../core/error.mjs";
13
13
  *
14
14
  * const link = new OpenAPILink({
15
15
  * url: "https://api.example.com",
16
- * spec: await fetch("/openapi.json").then(r => r.json()),
16
+ * spec: await fetch("/api/openapi.json").then(r => r.json()),
17
17
  * })
18
18
  *
19
19
  * const client = createClient<ExternalAPI>(link)
@@ -1,4 +1,3 @@
1
- import { resolveRoute } from "../core/router-utils.mjs";
2
1
  import { compileRouter } from "../compile.mjs";
3
2
  //#region src/client/server.ts
4
3
  /**
@@ -31,9 +30,7 @@ function createServerClient(router, options) {
31
30
  function createServerProxy(router, flatRouter, contextFactory, path) {
32
31
  const cache = /* @__PURE__ */ new Map();
33
32
  const callProcedure = async (input) => {
34
- const resolved = resolveRoute(router, path);
35
- const routePath = resolved?.path ?? "/" + path.join("/");
36
- const route = flatRouter(resolved?.method ?? "POST", routePath)?.data;
33
+ const route = flatRouter("POST", "/" + path.join("/"))?.data;
37
34
  if (!route) throw new Error(`Procedure not found: ${path.join("/")}`);
38
35
  const ctx = Object.create(null);
39
36
  const baseCtx = await contextFactory();
@@ -120,6 +120,10 @@ function createFetchHandler(routerDef, contextFactory, hooks) {
120
120
  const reqTrace = analyticsTraceMap.get(request);
121
121
  if (reqTrace) ctx.__analyticsTrace = reqTrace;
122
122
  if (!route.passthrough) rawInput = await parseInput(request, url, qMark);
123
+ if (match.params && Object.keys(match.params).length > 0) rawInput = rawInput != null && typeof rawInput === "object" ? {
124
+ ...match.params,
125
+ ...rawInput
126
+ } : match.params;
123
127
  callHook("request", {
124
128
  path: pathname,
125
129
  input: rawInput
@@ -18,81 +18,5 @@ function assignPaths(def, prefix = []) {
18
18
  }
19
19
  return result;
20
20
  }
21
- /**
22
- * Extract route metadata from a router definition.
23
- * Returns a lightweight nested object safe for client bundles — no resolve functions,
24
- * no guards, no schemas, no server code.
25
- *
26
- * ```ts
27
- * // server/routes.ts — import this on the client
28
- * export const routes = extractRoutes(appRouter)
29
- * ```
30
- */
31
- function extractRoutes(def, prefix = []) {
32
- const result = {};
33
- if (def == null || typeof def !== "object") return result;
34
- for (const [key, value] of Object.entries(def)) if (isProcedureDef(value)) {
35
- const route = value.route;
36
- if (route?.path) result[key] = {
37
- path: route.path,
38
- method: (route.method ?? "POST").toUpperCase()
39
- };
40
- } else if (typeof value === "object" && value !== null) {
41
- const nested = extractRoutes(value, [...prefix, key]);
42
- if (Object.keys(nested).length > 0) result[key] = nested;
43
- }
44
- return result;
45
- }
46
- /** Check if a value is an extracted route leaf (has path + method, not a nested object) */
47
- function isExtractedRoute(value) {
48
- return typeof value === "object" && value !== null && "path" in value && "method" in value && !("resolve" in value);
49
- }
50
- /** Walk a route tree by path segments and return the route metadata. Works with both full routers and extracted routes. */
51
- function resolveRoute(routes, path) {
52
- let current = routes;
53
- for (const segment of path) {
54
- if (current == null || typeof current !== "object") return void 0;
55
- current = current[segment];
56
- }
57
- if (isExtractedRoute(current)) return current;
58
- if (isProcedureDef(current)) {
59
- const route = current.route;
60
- if (!route?.path) return void 0;
61
- return {
62
- path: route.path,
63
- method: (route.method ?? "POST").toUpperCase()
64
- };
65
- }
66
- }
67
- /**
68
- * Substitute :param placeholders in a route path with values from input.
69
- * Returns the resolved URL path and the input with used params removed.
70
- */
71
- function substituteParams(routePath, input) {
72
- if (input == null || typeof input !== "object" || Array.isArray(input)) return {
73
- url: routePath,
74
- remainingInput: input
75
- };
76
- const obj = input;
77
- const used = /* @__PURE__ */ new Set();
78
- const url = routePath.replace(/:([a-zA-Z_]\w*)/g, (_match, name) => {
79
- const val = obj[name];
80
- if (val !== void 0) {
81
- used.add(name);
82
- return encodeURIComponent(String(val));
83
- }
84
- return `:${name}`;
85
- });
86
- if (used.size === 0) return {
87
- url,
88
- remainingInput: input
89
- };
90
- const remaining = {};
91
- for (const key of Object.keys(obj)) if (!used.has(key)) remaining[key] = obj[key];
92
- return {
93
- url,
94
- remainingInput: Object.keys(remaining).length > 0 ? remaining : void 0
95
- };
96
- }
97
21
  //#endregion
98
- export { assignPaths, extractRoutes, isProcedureDef, resolveRoute, routerCache, substituteParams };
22
+ export { assignPaths, isProcedureDef, routerCache };
@@ -22,9 +22,9 @@ interface SilgiServer {
22
22
  interface ServeOptions {
23
23
  port?: number;
24
24
  hostname?: string;
25
- /** Enable Scalar API Reference UI at /reference and /openapi.json */
25
+ /** Enable Scalar API Reference UI at /api/reference and /api/openapi.json */
26
26
  scalar?: boolean | ScalarOptions;
27
- /** Enable analytics dashboard at /analytics */
27
+ /** Enable analytics dashboard at /api/analytics */
28
28
  analytics?: boolean | AnalyticsOptions;
29
29
  /** Enable WebSocket RPC (requires crossws) */
30
30
  ws?: boolean | WSAdapterOptions;
@@ -50,8 +50,8 @@ async function createServeHandler(routerDef, contextFactory, hooks, options) {
50
50
  console.log(`\nSilgi server running at ${url}`);
51
51
  if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
52
52
  if (options?.ws) console.log(` WebSocket RPC at ws://${hostname}:${resolvedPort}`);
53
- if (options?.scalar) console.log(` Scalar API Reference at ${url}/reference`);
54
- if (options?.analytics) console.log(` Analytics dashboard at ${url}/analytics`);
53
+ if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
54
+ if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
55
55
  console.log();
56
56
  await hooks.callHook("serve:start", {
57
57
  url,
package/dist/index.d.mts CHANGED
@@ -13,6 +13,5 @@ import { CallableOptions, callable } from "./callable.mjs";
13
13
  import { LifecycleHooks, lifecycleWrap } from "./lifecycle.mjs";
14
14
  import { mapInput } from "./map-input.mjs";
15
15
  import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
16
- import { ExtractedRoute, ExtractedRoutes, extractRoutes } from "./core/router-utils.mjs";
17
16
  import { LazyRouter, isLazy, lazy, resolveLazy } from "./lazy.mjs";
18
- export { type AnySchema, AsyncIteratorClass, type CallableOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type ExtractedRoute, type ExtractedRoutes, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, compileProcedure, compileRouter, createContext, extractRoutes, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
17
+ export { type AnySchema, AsyncIteratorClass, type CallableOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  import { ValidationError, type, validateSchema } from "./core/schema.mjs";
2
2
  import { collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
3
3
  import { SilgiError, isDefinedError, toSilgiError } from "./core/error.mjs";
4
- import { extractRoutes } from "./core/router-utils.mjs";
5
4
  import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
6
5
  import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
7
6
  import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
@@ -12,4 +11,4 @@ import { lifecycleWrap } from "./lifecycle.mjs";
12
11
  import { mapInput } from "./map-input.mjs";
13
12
  import { isLazy, lazy, resolveLazy } from "./lazy.mjs";
14
13
  import { generateOpenAPI, scalarHTML } from "./scalar.mjs";
15
- export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, compileProcedure, compileRouter, createContext, extractRoutes, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
14
+ export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
@@ -11,7 +11,7 @@ import { FetchHandler } from "../core/handler.mjs";
11
11
  * - HTTP-level request tracking with procedure grouping (batch support)
12
12
  * - Unique request IDs via `x-request-id` response header
13
13
  *
14
- * Dashboard at /analytics, JSON API at /analytics/api, errors at /analytics/errors.
14
+ * Dashboard at /api/analytics, JSON API at /api/analytics/stats, errors at /api/analytics/errors.
15
15
  */
16
16
  interface TimeWindow {
17
17
  time: number;
@@ -16,7 +16,7 @@ import { parse } from "cookie-es";
16
16
  * - HTTP-level request tracking with procedure grouping (batch support)
17
17
  * - Unique request IDs via `x-request-id` response header
18
18
  *
19
- * Dashboard at /analytics, JSON API at /analytics/api, errors at /analytics/errors.
19
+ * Dashboard at /api/analytics, JSON API at /api/analytics/stats, errors at /api/analytics/errors.
20
20
  */
21
21
  var RingBuffer = class {
22
22
  #data;
@@ -665,8 +665,8 @@ document.getElementById('f').onsubmit=function(e){
665
665
  e.preventDefault();
666
666
  var t=document.getElementById('t').value.trim();
667
667
  if(!t)return;
668
- document.cookie='silgi-auth='+encodeURIComponent(t)+';path=/analytics;samesite=strict';
669
- fetch('/analytics/_api/stats',{headers:{'cookie':'silgi-auth='+encodeURIComponent(t)}}).then(function(){
668
+ document.cookie='silgi-auth='+encodeURIComponent(t)+';path=/api/analytics;samesite=strict';
669
+ fetch('/api/analytics/stats',{headers:{'cookie':'silgi-auth='+encodeURIComponent(t)}}).then(function(){
670
670
  location.reload();
671
671
  }).catch(function(){location.reload()});
672
672
  };
@@ -676,7 +676,7 @@ location.reload();
676
676
  /** Return auth-failure response for analytics routes. */
677
677
  function analyticsAuthResponse(pathname) {
678
678
  const jsonHeaders = { "content-type": "application/json" };
679
- if (pathname.includes("_api/")) return new Response(JSON.stringify({
679
+ if (pathname !== "api/analytics") return new Response(JSON.stringify({
680
680
  code: "UNAUTHORIZED",
681
681
  status: 401,
682
682
  message: "Invalid token"
@@ -702,27 +702,27 @@ async function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
702
702
  "content-type": "text/markdown; charset=utf-8",
703
703
  "cache-control": "no-cache"
704
704
  };
705
- if (pathname === "analytics/_api/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
706
- if (pathname === "analytics/_api/errors") return jsonResponse(await collector.getErrors(), jsonCacheHeaders);
707
- if (pathname === "analytics/_api/requests") return jsonResponse(await collector.getRequests(), jsonCacheHeaders);
708
- if (pathname === "analytics/_api/tasks") return jsonResponse(await collector.getTaskExecutions(), jsonCacheHeaders);
709
- if (pathname === "analytics/_api/scheduled") {
705
+ if (pathname === "api/analytics/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
706
+ if (pathname === "api/analytics/errors") return jsonResponse(await collector.getErrors(), jsonCacheHeaders);
707
+ if (pathname === "api/analytics/requests") return jsonResponse(await collector.getRequests(), jsonCacheHeaders);
708
+ if (pathname === "api/analytics/tasks") return jsonResponse(await collector.getTaskExecutions(), jsonCacheHeaders);
709
+ if (pathname === "api/analytics/scheduled") {
710
710
  const { getScheduledTasks } = await import("../core/task.mjs");
711
711
  return jsonResponse(getScheduledTasks(), jsonCacheHeaders);
712
712
  }
713
- if (pathname.startsWith("analytics/_api/requests/") && pathname.endsWith("/md")) {
714
- const id = Number(pathname.slice(24, -3));
713
+ if (pathname.startsWith("api/analytics/requests/") && pathname.endsWith("/md")) {
714
+ const id = Number(pathname.slice(23, -3));
715
715
  const entry = (await collector.getRequests()).find((r) => r.id === id);
716
716
  if (entry) return new Response(requestToMarkdown(entry), { headers: mdHeaders });
717
717
  return new Response("not found", { status: 404 });
718
718
  }
719
- if (pathname.startsWith("analytics/_api/errors/") && pathname.endsWith("/md")) {
720
- const id = Number(pathname.slice(22, -3));
719
+ if (pathname.startsWith("api/analytics/errors/") && pathname.endsWith("/md")) {
720
+ const id = Number(pathname.slice(21, -3));
721
721
  const entry = (await collector.getErrors()).find((e) => e.id === id);
722
722
  if (entry) return new Response(errorToMarkdown(entry), { headers: mdHeaders });
723
723
  return new Response("not found", { status: 404 });
724
724
  }
725
- if (pathname === "analytics/_api/errors/md") {
725
+ if (pathname === "api/analytics/errors/md") {
726
726
  const errors = await collector.getErrors();
727
727
  const md = errors.length === 0 ? "No errors.\n" : `# Errors (${errors.length})\n\n` + errors.map((e) => errorToMarkdown(e)).join("\n\n---\n\n");
728
728
  return new Response(md, { headers: mdHeaders });
@@ -749,7 +749,7 @@ function wrapWithAnalytics(handler, options = {}) {
749
749
  const qMark = url.indexOf("?", pathStart);
750
750
  const fullPath = qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
751
751
  const pathname = fullPath.length > 1 ? fullPath.slice(1) : "";
752
- if (pathname.startsWith("analytics")) {
752
+ if (pathname.startsWith("api/analytics")) {
753
753
  if (auth) {
754
754
  const authResult = checkAnalyticsAuth(request, auth);
755
755
  if (!(authResult instanceof Promise ? await authResult : authResult)) return analyticsAuthResponse(pathname);
package/dist/scalar.mjs CHANGED
@@ -290,20 +290,20 @@ function objectSchemaToParams(schema) {
290
290
  }));
291
291
  }
292
292
  /**
293
- * Wrap a fetch handler to serve Scalar API Reference at /reference and /openapi.json.
293
+ * Wrap a fetch handler to serve Scalar API Reference at /api/reference and /api/openapi.json.
294
294
  * Scalar routes are intercepted before the handler — zero overhead for normal requests.
295
295
  */
296
296
  function wrapWithScalar(handler, routerDef, options = {}) {
297
297
  const specJson = JSON.stringify(generateOpenAPI(routerDef, options));
298
- const specHtml = scalarHTML("/openapi.json", options);
298
+ const specHtml = scalarHTML("/api/openapi.json", options);
299
299
  return (request) => {
300
300
  const url = request.url;
301
301
  const pathStart = url.indexOf("/", url.indexOf("//") + 2);
302
302
  const qMark = url.indexOf("?", pathStart);
303
303
  const fullPath = qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
304
304
  const pathname = fullPath.length > 1 ? fullPath.slice(1) : "";
305
- if (pathname === "openapi.json") return new Response(specJson, { headers: { "content-type": "application/json" } });
306
- if (pathname === "reference") return new Response(specHtml, { headers: { "content-type": "text/html" } });
305
+ if (pathname === "api/openapi.json") return new Response(specJson, { headers: { "content-type": "application/json" } });
306
+ if (pathname === "api/reference") return new Response(specHtml, { headers: { "content-type": "text/html" } });
307
307
  return handler(request);
308
308
  };
309
309
  }
package/dist/silgi.d.mts CHANGED
@@ -95,7 +95,7 @@ interface SilgiInstance<TBaseCtx extends Record<string, unknown>> {
95
95
  router: <T extends RouterDef>(def: T) => T;
96
96
  /** Create a Fetch API handler: (Request) => Response */
97
97
  handler: (router: RouterDef, options?: {
98
- /** Enable Scalar API Reference UI at /reference and /openapi.json */scalar?: boolean | ScalarOptions; /** Enable analytics dashboard at /analytics */
98
+ /** Enable Scalar API Reference UI at /api/reference and /api/openapi.json */scalar?: boolean | ScalarOptions; /** Enable analytics dashboard at /api/analytics */
99
99
  analytics?: boolean | AnalyticsOptions;
100
100
  }) => (request: Request) => Response | Promise<Response>;
101
101
  /** Create a direct caller — call procedures without HTTP. For testing and server-side usage. */
@@ -35,7 +35,7 @@ Error generating stack: `+e.message+`
35
35
  at compiledPipeline (src/compile.ts:209:18)`,input:{id:42},headers:{"content-type":`application/json`,authorization:`Bearer eyJ...`,"user-agent":`Mozilla/5.0`},durationMs:3.62,spans:[{kind:`db`,name:`db.todos.findById`,durationMs:2.1},{kind:`db`,name:`auth.checkOwnership`,durationMs:1.3,error:`Forbidden: you do not own this todo`}]},{id:2,requestId:`req-2-b7d4e9`,timestamp:Kf(30*Wf),procedure:`todos/create`,error:`Validation failed: title must be at least 1 character`,code:`BAD_REQUEST`,status:400,stack:`ValidationError: Validation failed
36
36
  at validateSchema (src/core/schema.ts:12:49)`,input:{title:``},headers:{"content-type":`application/json`,"user-agent":`curl/8.7.1`},durationMs:.35,spans:[]},{id:3,requestId:`req-4-d9e3b7`,timestamp:Kf(60*Wf),procedure:`todos/toggle`,error:`Not Found`,code:`NOT_FOUND`,status:404,stack:`SilgiError: Not Found
37
37
  at Object.handler (src/procedures/todos.ts:28:9)
38
- at compiledPipeline (src/compile.ts:209:18)`,input:{id:99999},headers:{"content-type":`application/json`,"user-agent":`curl/8.7.1`},durationMs:3.54,spans:[{kind:`db`,name:`db.todos.findById`,durationMs:3.43}]}],Yf=[{id:1,requestId:`req-1-a3f2c1`,sessionId:`ses-1-x7k9m2`,timestamp:Kf(2*Wf),durationMs:4.82,method:`GET`,path:`/todos/list`,ip:`127.0.0.1`,headers:{"content-type":`application/json`,"user-agent":`Mozilla/5.0`},responseHeaders:{"content-type":`application/json`},userAgent:`Mozilla/5.0`,status:200,isBatch:!1,procedures:[{procedure:`todos/list`,durationMs:4.82,status:200,input:void 0,output:null,spans:[{kind:`cache`,name:`cache.get`,durationMs:.12,startOffsetMs:.1},{kind:`db`,name:`db.todos.findMany`,durationMs:3.87,startOffsetMs:.3,detail:`SELECT * FROM todos ORDER BY created_at DESC`},{kind:`cache`,name:`cache.set`,durationMs:.41,startOffsetMs:4.2}]}]},{id:2,requestId:`req-2-b7d4e9`,sessionId:`ses-1-x7k9m2`,timestamp:Kf(3*Wf),durationMs:18.3,method:`POST`,path:`/todos/create`,ip:`127.0.0.1`,headers:{"content-type":`application/json`},responseHeaders:{},userAgent:`Mozilla/5.0`,status:200,isBatch:!1,procedures:[{procedure:`todos/create`,durationMs:18.3,status:200,input:{title:`Buy groceries`},output:{id:42,title:`Buy groceries`,done:!1},spans:[{kind:`db`,name:`db.todos.create`,durationMs:12.4,startOffsetMs:.5,detail:`INSERT INTO todos (title) VALUES ($1)`},{kind:`cache`,name:`cache.invalidate`,durationMs:.8,startOffsetMs:13.1},{kind:`queue`,name:`queue.publish:todo.created`,durationMs:4.1,startOffsetMs:14}]}]},{id:3,requestId:`req-3-c1f8a2`,sessionId:`ses-2-p4r8w1`,timestamp:Kf(5*Wf),durationMs:24.5,method:`POST`,path:`/batch`,ip:`192.168.1.50`,headers:{"content-type":`application/json`,authorization:`[REDACTED]`},responseHeaders:{},userAgent:`curl/8.7.1`,status:200,isBatch:!0,procedures:[{procedure:`users/me`,durationMs:2.1,status:200,input:void 0,output:{id:1,name:`Alice`},spans:[{kind:`cache`,name:`cache.get:session`,durationMs:.3,startOffsetMs:.1},{kind:`db`,name:`db.users.findById`,durationMs:1.6,startOffsetMs:.5,detail:`SELECT * FROM users WHERE id = $1`}]},{procedure:`todos/list`,durationMs:5.2,status:200,input:void 0,output:null,spans:[{kind:`db`,name:`db.todos.findMany`,durationMs:4.8,startOffsetMs:2.3,detail:`SELECT * FROM todos WHERE user_id = $1`}]},{procedure:`notifications/unread`,durationMs:1.1,status:200,input:void 0,output:{count:3},spans:[{kind:`cache`,name:`cache.get:unread`,durationMs:.2,startOffsetMs:7.6}]}]},{id:4,requestId:`req-4-d9e3b7`,sessionId:`ses-1-x7k9m2`,timestamp:Kf(7*Wf),durationMs:9.7,method:`POST`,path:`/todos/toggle`,ip:`127.0.0.1`,headers:{},responseHeaders:{},userAgent:`Mozilla/5.0`,status:200,isBatch:!1,procedures:[{procedure:`todos/toggle`,durationMs:9.7,status:200,input:{id:5},output:null,spans:[{kind:`db`,name:`db.todos.findById`,durationMs:2.1,startOffsetMs:.2},{kind:`db`,name:`db.todos.update`,durationMs:5.8,startOffsetMs:2.5,detail:`UPDATE todos SET done = NOT done WHERE id = $1`},{kind:`cache`,name:`cache.invalidate`,durationMs:.6,startOffsetMs:8.5},{kind:`http`,name:`http.webhook`,durationMs:.9,startOffsetMs:9.2,detail:`POST https://hooks.example.com/todo-updated`}]}]}],Xf={stats:`/analytics/_api/stats`,errors:`/analytics/_api/errors`,requests:`/analytics/_api/requests`,tasks:`/analytics/_api/tasks`,scheduled:`/analytics/_api/scheduled`};function Zf(e=2e3){let[t,n]=(0,U.useState)(null),[r,i]=(0,U.useState)([]),[a,o]=(0,U.useState)([]),[s,c]=(0,U.useState)([]),[l,u]=(0,U.useState)([]),[d,f]=(0,U.useState)(!0),p=(0,U.useRef)(!1),m=(0,U.useCallback)(async()=>{if(!p.current)try{let[e,t,r,a]=await Promise.all([fetch(Xf.stats),fetch(Xf.errors),fetch(Xf.requests),fetch(Xf.tasks)]);if(e.status===401||t.status===401||r.status===401){window.location.reload();return}if(!e.ok||!t.ok||!r.ok){h();return}let[s,l,d]=await Promise.all([e.json(),t.json(),r.json()]);n(s),i(l),o(d),a.ok&&c(await a.json());let f=await fetch(Xf.scheduled).catch(()=>null);f?.ok&&u(await f.json())}catch{h()}},[]);function h(){p.current=!0,n(qf),i(Jf),o(Yf),f(!1)}return(0,U.useEffect)(()=>{if(p.current||(m(),!d))return;let t=setInterval(m,e);return()=>clearInterval(t)},[m,e,d]),{data:t,errors:r,requests:a,taskExecutions:s,scheduledTasks:l,autoRefresh:d,setAutoRefresh:f}}var Qf=2e3;function $f(){let[e,t]=(0,U.useState)(null),n=(0,U.useRef)(void 0);return{copiedId:e,copy:(0,U.useCallback)((e,r)=>{navigator.clipboard.writeText(r).then(()=>{t(e),clearTimeout(n.current),n.current=setTimeout(()=>t(null),Qf)})},[])}}function ep({navigate:e,toggleRefresh:t}){(0,U.useEffect)(()=>{function n(n){let r=n.target.tagName;if(!(r===`INPUT`||r===`TEXTAREA`||r===`SELECT`)&&!(n.metaKey||n.ctrlKey||n.altKey))switch(n.key){case`1`:e(`overview`);break;case`2`:e(`requests`);break;case`3`:e(`errors`);break;case`r`:t();break;case`/`:n.preventDefault(),document.querySelector(`input[type="search"]`)?.focus();break}}return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[e,t])}function tp(){let[e=`/`,t=``]=(window.location.hash.slice(1)||`/`).split(`?`),n=e.split(`/`).filter(Boolean),r={};if(t)for(let e of t.split(`&`)){let[t,n]=e.split(`=`);t&&(r[t]=decodeURIComponent(n??``))}return n.length===0?{page:`overview`,params:r}:n.length===2?{page:n[0],id:n[1],params:r}:{page:n[0],params:r}}function np(){let[e,t]=(0,U.useState)(tp);return(0,U.useEffect)(()=>{let e=()=>t(tp());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),{route:e,navigate:(0,U.useCallback)((e,t,n)=>{let r=t?`${e}/${t}`:e;if(n){let e=Object.entries(n).map(([e,t])=>`${e}=${encodeURIComponent(t)}`).join(`&`);e&&(r+=`?${e}`)}window.location.hash=r},[])}}function rp(){return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function ip(){return localStorage.getItem(`silgi-theme`)??rp()}function ap(e){let t=document.documentElement;t.classList.add(`theme-transition`),t.classList.toggle(`dark`,e===`dark`),setTimeout(()=>t.classList.remove(`theme-transition`),250)}function op(){let[e,t]=(0,U.useState)(ip);(0,U.useEffect)(()=>{ap(e)},[e]);let n=(0,U.useCallback)(e=>{localStorage.setItem(`silgi-theme`,e),t(e)},[]);return{theme:e,setTheme:n,toggle:(0,U.useCallback)(()=>{n(e===`dark`?`light`:`dark`)},[e,n])}}var sp=`[REDACTED]`,cp=new Set([`authorization`,`cookie`,`x-api-key`]);function lp(e,t){return cp.has(e.toLowerCase())?sp:t}function up(e){try{return JSON.stringify(e,null,2)}catch{return String(e)}}function dp(e,t){if(e.length===0)return``;let n=[],r=new Map;for(let t of e)r.set(t.kind,(r.get(t.kind)??0)+t.durationMs);let i=[...r.values()].reduce((e,t)=>e+t,0),a=Math.max(0,t-i),o=Math.max(t,.1);n.push(`#### Timing`),n.push(``),n.push(`| Category | Duration | % |`),n.push(`|----------|----------|---|`),n.push(`| **Total** | **${t}ms** | 100% |`);for(let[e,t]of r)n.push(`| ${e} | ${t.toFixed(1)}ms | ${(t/o*100).toFixed(0)}% |`);n.push(`| App Logic | ${a.toFixed(1)}ms | ${(a/o*100).toFixed(0)}% |`),n.push(``);for(let t=0;t<e.length;t++){let r=e[t],i=r.error?` ❌ ${r.error}`:``,a=r.startOffsetMs==null?``:` (at +${r.startOffsetMs}ms)`;n.push(`**${t+1}. [${r.kind}] ${r.name}** — ${r.durationMs}ms${a}${i}`),r.detail&&n.push("```",r.detail,"```")}return n.push(``),n.join(`
38
+ at compiledPipeline (src/compile.ts:209:18)`,input:{id:99999},headers:{"content-type":`application/json`,"user-agent":`curl/8.7.1`},durationMs:3.54,spans:[{kind:`db`,name:`db.todos.findById`,durationMs:3.43}]}],Yf=[{id:1,requestId:`req-1-a3f2c1`,sessionId:`ses-1-x7k9m2`,timestamp:Kf(2*Wf),durationMs:4.82,method:`GET`,path:`/todos/list`,ip:`127.0.0.1`,headers:{"content-type":`application/json`,"user-agent":`Mozilla/5.0`},responseHeaders:{"content-type":`application/json`},userAgent:`Mozilla/5.0`,status:200,isBatch:!1,procedures:[{procedure:`todos/list`,durationMs:4.82,status:200,input:void 0,output:null,spans:[{kind:`cache`,name:`cache.get`,durationMs:.12,startOffsetMs:.1},{kind:`db`,name:`db.todos.findMany`,durationMs:3.87,startOffsetMs:.3,detail:`SELECT * FROM todos ORDER BY created_at DESC`},{kind:`cache`,name:`cache.set`,durationMs:.41,startOffsetMs:4.2}]}]},{id:2,requestId:`req-2-b7d4e9`,sessionId:`ses-1-x7k9m2`,timestamp:Kf(3*Wf),durationMs:18.3,method:`POST`,path:`/todos/create`,ip:`127.0.0.1`,headers:{"content-type":`application/json`},responseHeaders:{},userAgent:`Mozilla/5.0`,status:200,isBatch:!1,procedures:[{procedure:`todos/create`,durationMs:18.3,status:200,input:{title:`Buy groceries`},output:{id:42,title:`Buy groceries`,done:!1},spans:[{kind:`db`,name:`db.todos.create`,durationMs:12.4,startOffsetMs:.5,detail:`INSERT INTO todos (title) VALUES ($1)`},{kind:`cache`,name:`cache.invalidate`,durationMs:.8,startOffsetMs:13.1},{kind:`queue`,name:`queue.publish:todo.created`,durationMs:4.1,startOffsetMs:14}]}]},{id:3,requestId:`req-3-c1f8a2`,sessionId:`ses-2-p4r8w1`,timestamp:Kf(5*Wf),durationMs:24.5,method:`POST`,path:`/batch`,ip:`192.168.1.50`,headers:{"content-type":`application/json`,authorization:`[REDACTED]`},responseHeaders:{},userAgent:`curl/8.7.1`,status:200,isBatch:!0,procedures:[{procedure:`users/me`,durationMs:2.1,status:200,input:void 0,output:{id:1,name:`Alice`},spans:[{kind:`cache`,name:`cache.get:session`,durationMs:.3,startOffsetMs:.1},{kind:`db`,name:`db.users.findById`,durationMs:1.6,startOffsetMs:.5,detail:`SELECT * FROM users WHERE id = $1`}]},{procedure:`todos/list`,durationMs:5.2,status:200,input:void 0,output:null,spans:[{kind:`db`,name:`db.todos.findMany`,durationMs:4.8,startOffsetMs:2.3,detail:`SELECT * FROM todos WHERE user_id = $1`}]},{procedure:`notifications/unread`,durationMs:1.1,status:200,input:void 0,output:{count:3},spans:[{kind:`cache`,name:`cache.get:unread`,durationMs:.2,startOffsetMs:7.6}]}]},{id:4,requestId:`req-4-d9e3b7`,sessionId:`ses-1-x7k9m2`,timestamp:Kf(7*Wf),durationMs:9.7,method:`POST`,path:`/todos/toggle`,ip:`127.0.0.1`,headers:{},responseHeaders:{},userAgent:`Mozilla/5.0`,status:200,isBatch:!1,procedures:[{procedure:`todos/toggle`,durationMs:9.7,status:200,input:{id:5},output:null,spans:[{kind:`db`,name:`db.todos.findById`,durationMs:2.1,startOffsetMs:.2},{kind:`db`,name:`db.todos.update`,durationMs:5.8,startOffsetMs:2.5,detail:`UPDATE todos SET done = NOT done WHERE id = $1`},{kind:`cache`,name:`cache.invalidate`,durationMs:.6,startOffsetMs:8.5},{kind:`http`,name:`http.webhook`,durationMs:.9,startOffsetMs:9.2,detail:`POST https://hooks.example.com/todo-updated`}]}]}],Xf={stats:`/api/analytics/stats`,errors:`/api/analytics/errors`,requests:`/api/analytics/requests`,tasks:`/api/analytics/tasks`,scheduled:`/api/analytics/scheduled`};function Zf(e=2e3){let[t,n]=(0,U.useState)(null),[r,i]=(0,U.useState)([]),[a,o]=(0,U.useState)([]),[s,c]=(0,U.useState)([]),[l,u]=(0,U.useState)([]),[d,f]=(0,U.useState)(!0),p=(0,U.useRef)(!1),m=(0,U.useCallback)(async()=>{if(!p.current)try{let[e,t,r,a]=await Promise.all([fetch(Xf.stats),fetch(Xf.errors),fetch(Xf.requests),fetch(Xf.tasks)]);if(e.status===401||t.status===401||r.status===401){window.location.reload();return}if(!e.ok||!t.ok||!r.ok){h();return}let[s,l,d]=await Promise.all([e.json(),t.json(),r.json()]);n(s),i(l),o(d),a.ok&&c(await a.json());let f=await fetch(Xf.scheduled).catch(()=>null);f?.ok&&u(await f.json())}catch{h()}},[]);function h(){p.current=!0,n(qf),i(Jf),o(Yf),f(!1)}return(0,U.useEffect)(()=>{if(p.current||(m(),!d))return;let t=setInterval(m,e);return()=>clearInterval(t)},[m,e,d]),{data:t,errors:r,requests:a,taskExecutions:s,scheduledTasks:l,autoRefresh:d,setAutoRefresh:f}}var Qf=2e3;function $f(){let[e,t]=(0,U.useState)(null),n=(0,U.useRef)(void 0);return{copiedId:e,copy:(0,U.useCallback)((e,r)=>{navigator.clipboard.writeText(r).then(()=>{t(e),clearTimeout(n.current),n.current=setTimeout(()=>t(null),Qf)})},[])}}function ep({navigate:e,toggleRefresh:t}){(0,U.useEffect)(()=>{function n(n){let r=n.target.tagName;if(!(r===`INPUT`||r===`TEXTAREA`||r===`SELECT`)&&!(n.metaKey||n.ctrlKey||n.altKey))switch(n.key){case`1`:e(`overview`);break;case`2`:e(`requests`);break;case`3`:e(`errors`);break;case`r`:t();break;case`/`:n.preventDefault(),document.querySelector(`input[type="search"]`)?.focus();break}}return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[e,t])}function tp(){let[e=`/`,t=``]=(window.location.hash.slice(1)||`/`).split(`?`),n=e.split(`/`).filter(Boolean),r={};if(t)for(let e of t.split(`&`)){let[t,n]=e.split(`=`);t&&(r[t]=decodeURIComponent(n??``))}return n.length===0?{page:`overview`,params:r}:n.length===2?{page:n[0],id:n[1],params:r}:{page:n[0],params:r}}function np(){let[e,t]=(0,U.useState)(tp);return(0,U.useEffect)(()=>{let e=()=>t(tp());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),{route:e,navigate:(0,U.useCallback)((e,t,n)=>{let r=t?`${e}/${t}`:e;if(n){let e=Object.entries(n).map(([e,t])=>`${e}=${encodeURIComponent(t)}`).join(`&`);e&&(r+=`?${e}`)}window.location.hash=r},[])}}function rp(){return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function ip(){return localStorage.getItem(`silgi-theme`)??rp()}function ap(e){let t=document.documentElement;t.classList.add(`theme-transition`),t.classList.toggle(`dark`,e===`dark`),setTimeout(()=>t.classList.remove(`theme-transition`),250)}function op(){let[e,t]=(0,U.useState)(ip);(0,U.useEffect)(()=>{ap(e)},[e]);let n=(0,U.useCallback)(e=>{localStorage.setItem(`silgi-theme`,e),t(e)},[]);return{theme:e,setTheme:n,toggle:(0,U.useCallback)(()=>{n(e===`dark`?`light`:`dark`)},[e,n])}}var sp=`[REDACTED]`,cp=new Set([`authorization`,`cookie`,`x-api-key`]);function lp(e,t){return cp.has(e.toLowerCase())?sp:t}function up(e){try{return JSON.stringify(e,null,2)}catch{return String(e)}}function dp(e,t){if(e.length===0)return``;let n=[],r=new Map;for(let t of e)r.set(t.kind,(r.get(t.kind)??0)+t.durationMs);let i=[...r.values()].reduce((e,t)=>e+t,0),a=Math.max(0,t-i),o=Math.max(t,.1);n.push(`#### Timing`),n.push(``),n.push(`| Category | Duration | % |`),n.push(`|----------|----------|---|`),n.push(`| **Total** | **${t}ms** | 100% |`);for(let[e,t]of r)n.push(`| ${e} | ${t.toFixed(1)}ms | ${(t/o*100).toFixed(0)}% |`);n.push(`| App Logic | ${a.toFixed(1)}ms | ${(a/o*100).toFixed(0)}% |`),n.push(``);for(let t=0;t<e.length;t++){let r=e[t],i=r.error?` ❌ ${r.error}`:``,a=r.startOffsetMs==null?``:` (at +${r.startOffsetMs}ms)`;n.push(`**${t+1}. [${r.kind}] ${r.name}** — ${r.durationMs}ms${a}${i}`),r.detail&&n.push("```",r.detail,"```")}return n.push(``),n.join(`
39
39
  `)}function fp(e,t){let n=[],r=e.status>=500?`💥`:e.status>=400?`⚠️`:`✅`;return n.push(`### ${r} ${t+1}. \`${e.procedure}\` → ${e.status} (${e.durationMs}ms)`),n.push(``),e.input!==void 0&&e.input!==null&&n.push(`#### Input`,``,"```json",up(e.input),"```",``),e.output!==void 0&&e.output!==null&&n.push(`#### Output`,``,"```json",up(e.output),"```",``),e.error&&n.push(`#### Error`,``,"```",e.error,"```",``),n.push(dp(e.spans,e.durationMs)),n.join(`
40
40
  `)}function pp(e){let t=[];return t.push(`---`,``),t.push(`**Analyze this request and suggest performance optimizations:**`),t.push(`- Redundant or slow operations that could be combined?`),t.push(`- N+1 query pattern?`),t.push(`- Data that should be cached?`),t.push(`- Sequential calls that could run in parallel?`),e>100&&t.push(`- This request took ${e}ms — what is the bottleneck?`),t.join(`
41
41
  `)}function mp(e){let t=[],n=new Date(e.timestamp).toISOString();t.push(`## Error in \`${e.procedure}\``),t.push(``),t.push(`**Time:** ${n} `),t.push(`**Error:** ${e.code} `),t.push(`**Status:** ${e.status} `),t.push(`**Duration:** ${e.durationMs}ms`),t.push(``),e.input!==void 0&&e.input!==null&&t.push(`### Input`,``,"```json",up(e.input),"```",``),e.stack&&t.push(`### Stack Trace`,``,"```",e.stack,"```",``);let r=Object.entries(e.headers??{});if(r.length>0){t.push(`### Request Headers`,``);for(let[e,n]of r)t.push(`- \`${e}\`: \`${lp(e,n)}\``);t.push(``)}return t.push(dp(e.spans,e.durationMs)),t.push(`### Error Message`,``,"```",e.error,"```",``),t.push(pp(e.durationMs)),t.join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silgi",
3
- "version": "0.1.0-beta.11",
3
+ "version": "0.1.0-beta.13",
4
4
  "private": false,
5
5
  "description": "The fastest end-to-end type-safe RPC framework for TypeScript — compiled pipelines, single package, every runtime",
6
6
  "keywords": [
@@ -139,10 +139,6 @@
139
139
  "import": "./dist/integrations/better-auth/index.mjs",
140
140
  "types": "./dist/integrations/better-auth/index.d.mts"
141
141
  },
142
- "./contract": {
143
- "import": "./dist/contract.mjs",
144
- "types": "./dist/contract.d.mts"
145
- },
146
142
  "./react": {
147
143
  "import": "./dist/integrations/react/index.mjs",
148
144
  "types": "./dist/integrations/react/index.d.mts"
@@ -270,6 +266,7 @@
270
266
  "ocache": "^0.1.4",
271
267
  "ofetch": "2.0.0-alpha.3",
272
268
  "ohash": "^2.0.11",
269
+ "silgi": "link:",
273
270
  "srvx": "^0.11.13",
274
271
  "unstorage": "^2.0.0-alpha.7"
275
272
  },
@@ -333,7 +330,7 @@
333
330
  "packageManager": "pnpm@10.33.0",
334
331
  "pnpm": {
335
332
  "overrides": {
336
- "silgi": "workspace:*"
333
+ "silgi": "link:"
337
334
  }
338
335
  }
339
336
  }
@@ -1,36 +0,0 @@
1
- import { AnySchema, InferSchemaInput, InferSchemaOutput } from "./core/schema.mjs";
2
- import { ErrorDef, ProcedureType, Route, RouterDef } from "./types.mjs";
3
-
4
- //#region src/contract.d.ts
5
- interface ProcedureContract<TType extends ProcedureType = ProcedureType, TInput extends AnySchema | undefined = AnySchema | undefined, TOutput extends AnySchema | undefined = AnySchema | undefined, TErrors extends ErrorDef = ErrorDef> {
6
- type?: TType;
7
- input?: TInput;
8
- output?: TOutput;
9
- errors?: TErrors;
10
- route?: Route;
11
- description?: string;
12
- }
13
- type ContractRouter = {
14
- [key: string]: ProcedureContract<any, any, any, any> | ContractRouter;
15
- };
16
- /**
17
- * Define an API contract — shared between client and server.
18
- * Pure type information, no runtime behavior.
19
- */
20
- declare function contract<T extends ContractRouter>(definition: T): T;
21
- type ImplementProcedure<T extends ProcedureContract> = T extends ProcedureContract<infer _TType, infer TInput, infer TOutput, infer TErrors> ? (opts: {
22
- input: TInput extends AnySchema ? InferSchemaOutput<TInput> : undefined;
23
- ctx: Record<string, unknown>;
24
- fail: TErrors extends ErrorDef ? <K extends keyof TErrors & string>(code: K, ...args: any[]) => never : never;
25
- signal: AbortSignal;
26
- }) => TOutput extends AnySchema ? InferSchemaOutput<TOutput> | Promise<InferSchemaOutput<TOutput>> : unknown : never;
27
- type ImplementRouter<T extends ContractRouter> = { [K in keyof T]: T[K] extends ProcedureContract ? ImplementProcedure<T[K]> : T[K] extends ContractRouter ? ImplementRouter<T[K]> : never };
28
- /**
29
- * Implement a contract — returns a silgi RouterDef.
30
- * Type-safe: implementation must match the contract.
31
- */
32
- declare function implement<T extends ContractRouter>(contractDef: T, implementations: ImplementRouter<T>): RouterDef;
33
- /** Infer client type from a contract (no server code needed) */
34
- type InferContractClient<T extends ContractRouter> = { [K in keyof T]: T[K] extends ProcedureContract<any, infer TInput, infer TOutput> ? TInput extends AnySchema ? (input: InferSchemaInput<TInput>) => Promise<TOutput extends AnySchema ? InferSchemaOutput<TOutput> : unknown> : () => Promise<TOutput extends AnySchema ? InferSchemaOutput<TOutput> : unknown> : T[K] extends ContractRouter ? InferContractClient<T[K]> : never };
35
- //#endregion
36
- export { ContractRouter, InferContractClient, ProcedureContract, contract, implement };
package/dist/contract.mjs DELETED
@@ -1,40 +0,0 @@
1
- //#region src/contract.ts
2
- /**
3
- * Define an API contract — shared between client and server.
4
- * Pure type information, no runtime behavior.
5
- */
6
- function contract(definition) {
7
- return definition;
8
- }
9
- /**
10
- * Implement a contract — returns a silgi RouterDef.
11
- * Type-safe: implementation must match the contract.
12
- */
13
- function implement(contractDef, implementations) {
14
- return buildRouter(contractDef, implementations);
15
- }
16
- function buildRouter(contractDef, impls) {
17
- const router = {};
18
- for (const [key, contractEntry] of Object.entries(contractDef)) {
19
- const impl = impls[key];
20
- if (!impl) continue;
21
- if (isProcedureContract(contractEntry)) router[key] = {
22
- type: contractEntry.type ?? "query",
23
- input: contractEntry.input ?? null,
24
- output: contractEntry.output ?? null,
25
- errors: contractEntry.errors ?? null,
26
- use: null,
27
- resolve: impl,
28
- route: contractEntry.route ?? null,
29
- meta: null
30
- };
31
- else router[key] = buildRouter(contractEntry, impl);
32
- }
33
- return router;
34
- }
35
- function isProcedureContract(v) {
36
- if (typeof v !== "object" || v === null) return false;
37
- return "input" in v || "output" in v || "type" in v || "errors" in v;
38
- }
39
- //#endregion
40
- export { contract, implement };
@@ -1,25 +0,0 @@
1
- import { compileRouter } from "../compile.mjs";
2
-
3
- //#region src/core/router-utils.d.ts
4
- /** Extracted route metadata — safe for client bundles (no server code) */
5
- interface ExtractedRoute {
6
- path: string;
7
- method: string;
8
- }
9
- /** Nested route map — mirrors the router tree structure with only route metadata */
10
- type ExtractedRoutes = {
11
- [key: string]: ExtractedRoute | ExtractedRoutes;
12
- };
13
- /**
14
- * Extract route metadata from a router definition.
15
- * Returns a lightweight nested object safe for client bundles — no resolve functions,
16
- * no guards, no schemas, no server code.
17
- *
18
- * ```ts
19
- * // server/routes.ts — import this on the client
20
- * export const routes = extractRoutes(appRouter)
21
- * ```
22
- */
23
- declare function extractRoutes(def: unknown, prefix?: string[]): ExtractedRoutes;
24
- //#endregion
25
- export { ExtractedRoute, ExtractedRoutes, extractRoutes };