@spfn/core 0.1.0-alpha.1

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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +580 -0
  3. package/dist/auto-loader-C44TcLmM.d.ts +125 -0
  4. package/dist/bind-pssq1NRT.d.ts +34 -0
  5. package/dist/client/index.d.ts +174 -0
  6. package/dist/client/index.js +179 -0
  7. package/dist/client/index.js.map +1 -0
  8. package/dist/codegen/index.d.ts +126 -0
  9. package/dist/codegen/index.js +970 -0
  10. package/dist/codegen/index.js.map +1 -0
  11. package/dist/db/index.d.ts +83 -0
  12. package/dist/db/index.js +2099 -0
  13. package/dist/db/index.js.map +1 -0
  14. package/dist/index.d.ts +379 -0
  15. package/dist/index.js +13042 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/postgres-errors-CY_Es8EJ.d.ts +1703 -0
  18. package/dist/route/index.d.ts +72 -0
  19. package/dist/route/index.js +442 -0
  20. package/dist/route/index.js.map +1 -0
  21. package/dist/scripts/index.d.ts +24 -0
  22. package/dist/scripts/index.js +1157 -0
  23. package/dist/scripts/index.js.map +1 -0
  24. package/dist/scripts/templates/api-index.template.txt +10 -0
  25. package/dist/scripts/templates/api-tag.template.txt +11 -0
  26. package/dist/scripts/templates/contract.template.txt +87 -0
  27. package/dist/scripts/templates/entity-type.template.txt +31 -0
  28. package/dist/scripts/templates/entity.template.txt +19 -0
  29. package/dist/scripts/templates/index.template.txt +10 -0
  30. package/dist/scripts/templates/repository.template.txt +37 -0
  31. package/dist/scripts/templates/routes-id.template.txt +59 -0
  32. package/dist/scripts/templates/routes-index.template.txt +44 -0
  33. package/dist/server/index.d.ts +303 -0
  34. package/dist/server/index.js +12923 -0
  35. package/dist/server/index.js.map +1 -0
  36. package/dist/types-SlzTr8ZO.d.ts +143 -0
  37. package/package.json +119 -0
@@ -0,0 +1,125 @@
1
+ import { Hono } from 'hono';
2
+
3
+ /**
4
+ * Extend Hono Context to support skipMiddlewares metadata
5
+ */
6
+ declare module 'hono' {
7
+ interface ContextVariableMap {
8
+ _skipMiddlewares?: string[];
9
+ }
10
+ }
11
+ /**
12
+ * AutoRouteLoader: Simplified File-based Routing System
13
+ *
14
+ * ## Features
15
+ * - 📁 Auto-discovery: Scans routes directory and auto-registers
16
+ * - 🔄 Dynamic routes: [id] → :id, [...slug] → *
17
+ * - 📊 Statistics: Route registration stats for dashboard
18
+ * - 🏷️ Grouping: Natural grouping by directory structure
19
+ *
20
+ * ## Usage
21
+ * ```typescript
22
+ * const app = new Hono();
23
+ * await loadRoutes(app);
24
+ * ```
25
+ */
26
+ type RouteInfo = {
27
+ /** URL path (e.g., /users/:id) */
28
+ path: string;
29
+ /** File path relative to routes dir */
30
+ file: string;
31
+ /** Route metadata from export */
32
+ meta?: {
33
+ description?: string;
34
+ tags?: string[];
35
+ auth?: boolean;
36
+ [key: string]: unknown;
37
+ };
38
+ /** Priority (1=static, 2=dynamic, 3=catch-all) */
39
+ priority: number;
40
+ };
41
+ type RouteStats = {
42
+ total: number;
43
+ byPriority: {
44
+ static: number;
45
+ dynamic: number;
46
+ catchAll: number;
47
+ };
48
+ byTag: Record<string, number>;
49
+ routes: RouteInfo[];
50
+ };
51
+ declare class AutoRouteLoader {
52
+ private routesDir;
53
+ private routes;
54
+ private registeredRoutes;
55
+ private debug;
56
+ private readonly middlewares;
57
+ constructor(routesDir: string, debug?: boolean, middlewares?: Array<{
58
+ name: string;
59
+ handler: any;
60
+ }>);
61
+ /**
62
+ * Load all routes from directory
63
+ */
64
+ load(app: Hono): Promise<RouteStats>;
65
+ /**
66
+ * Get route statistics
67
+ */
68
+ getStats(): RouteStats;
69
+ /**
70
+ * Recursively scan directory for .ts files
71
+ */
72
+ private scanFiles;
73
+ /**
74
+ * Check if file is a valid route file
75
+ */
76
+ private isValidRouteFile;
77
+ /**
78
+ * Load and register a single route
79
+ * Returns true if successful, false if failed
80
+ */
81
+ private loadRoute;
82
+ /**
83
+ * Convert file path to URL path
84
+ *
85
+ * Examples:
86
+ * - users/index.ts → /users
87
+ * - users/[id].ts → /users/:id
88
+ * - posts/[...slug].ts → /posts/*
89
+ */
90
+ private fileToPath;
91
+ /**
92
+ * Calculate route priority
93
+ * 1 = static, 2 = dynamic, 3 = catch-all
94
+ */
95
+ private calculatePriority;
96
+ /**
97
+ * Normalize path for conflict detection
98
+ *
99
+ * Converts dynamic parameter names to generic placeholders:
100
+ * - /users/:id → /users/:param
101
+ * - /users/:userId → /users/:param (conflict!)
102
+ * - /posts/* → /posts/* (unchanged)
103
+ *
104
+ * This allows detection of routes with different param names
105
+ * that would match the same URL patterns.
106
+ */
107
+ private normalizePath;
108
+ /**
109
+ * Log statistics
110
+ */
111
+ private logStats;
112
+ }
113
+ /**
114
+ * Load routes from default location (src/server/routes)
115
+ */
116
+ declare function loadRoutes(app: Hono, options?: {
117
+ routesDir?: string;
118
+ debug?: boolean;
119
+ middlewares?: Array<{
120
+ name: string;
121
+ handler: any;
122
+ }>;
123
+ }): Promise<RouteStats>;
124
+
125
+ export { AutoRouteLoader as A, type RouteInfo as R, type RouteStats as a, loadRoutes as l };
@@ -0,0 +1,34 @@
1
+ import { Context } from 'hono';
2
+ import { R as RouteContract, c as RouteContext } from './types-SlzTr8ZO.js';
3
+
4
+ /**
5
+ * Contract-based Route Handler Wrapper
6
+ *
7
+ * Binds a contract to a route handler, providing automatic validation
8
+ * and type-safe context creation.
9
+ *
10
+ * ## Features
11
+ * - ✅ Automatic params/query/body validation using TypeBox
12
+ * - ✅ Type-safe RouteContext with contract-based inference
13
+ * - ✅ Clean separation: bind() for validation, Hono for middleware
14
+ *
15
+ * ## Usage
16
+ *
17
+ * ```typescript
18
+ * // Basic usage
19
+ * export const GET = bind(contract, async (c) => {
20
+ * return c.json({ data: 'public' });
21
+ * });
22
+ *
23
+ * // For middleware, use Hono's app-level or route-level middleware:
24
+ * // app.use('/api/*', authMiddleware);
25
+ * // app.get('/users/:id', authMiddleware, bind(contract, handler));
26
+ * ```
27
+ *
28
+ * @param contract - Route contract defining params, query, body, response schemas
29
+ * @param handler - Route handler function
30
+ * @returns Hono-compatible handler function
31
+ */
32
+ declare function bind<TContract extends RouteContract>(contract: TContract, handler: (c: RouteContext<TContract>) => Response | Promise<Response>): (rawContext: Context) => Promise<Response>;
33
+
34
+ export { bind as b };
@@ -0,0 +1,174 @@
1
+ import '../auto-loader-C44TcLmM.js';
2
+ import { R as RouteContract, I as InferContract } from '../types-SlzTr8ZO.js';
3
+ import 'hono';
4
+ import 'hono/utils/http-status';
5
+ import '@sinclair/typebox';
6
+
7
+ /**
8
+ * Contract-Based API Client
9
+ *
10
+ * Type-safe HTTP client that works with RouteContract for full end-to-end type safety
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { createClient } from '@spfn/core/client';
15
+ * import { getUserContract } from './contracts';
16
+ *
17
+ * const client = createClient({ baseUrl: 'http://localhost:4000' });
18
+ * const user = await client.call(getUserContract, { params: { id: '123' } });
19
+ * // ✅ user is fully typed based on contract.response
20
+ * ```
21
+ */
22
+
23
+ /**
24
+ * Request interceptor function
25
+ *
26
+ * Allows modifying request before it's sent
27
+ */
28
+ type RequestInterceptor = (url: string, init: RequestInit) => Promise<RequestInit> | RequestInit;
29
+ /**
30
+ * Client configuration
31
+ */
32
+ interface ClientConfig {
33
+ /**
34
+ * API base URL (e.g., http://localhost:4000)
35
+ * Can be overridden per request
36
+ */
37
+ baseUrl?: string;
38
+ /**
39
+ * Default headers to include in all requests
40
+ */
41
+ headers?: Record<string, string>;
42
+ /**
43
+ * Request timeout in milliseconds
44
+ */
45
+ timeout?: number;
46
+ /**
47
+ * Custom fetch implementation (for testing or custom behavior)
48
+ */
49
+ fetch?: typeof fetch;
50
+ }
51
+ /**
52
+ * Request options for API calls
53
+ */
54
+ interface CallOptions<TContract extends RouteContract> {
55
+ /**
56
+ * Path parameters (for dynamic routes like /users/:id)
57
+ */
58
+ params?: InferContract<TContract>['params'];
59
+ /**
60
+ * Query parameters (for URL query strings)
61
+ */
62
+ query?: InferContract<TContract>['query'];
63
+ /**
64
+ * Request body (for POST, PUT, PATCH)
65
+ */
66
+ body?: InferContract<TContract>['body'];
67
+ /**
68
+ * Additional headers for this specific request
69
+ */
70
+ headers?: Record<string, string>;
71
+ /**
72
+ * Override base URL for this request
73
+ */
74
+ baseUrl?: string;
75
+ }
76
+ /**
77
+ * API Client Error
78
+ */
79
+ declare class ApiClientError extends Error {
80
+ readonly status: number;
81
+ readonly statusText: string;
82
+ readonly url: string;
83
+ readonly response?: unknown | undefined;
84
+ constructor(message: string, status: number, statusText: string, url: string, response?: unknown | undefined);
85
+ }
86
+ /**
87
+ * Contract-based API Client
88
+ */
89
+ declare class ContractClient {
90
+ private readonly config;
91
+ private readonly interceptors;
92
+ constructor(config?: ClientConfig);
93
+ /**
94
+ * Add request interceptor
95
+ *
96
+ * Interceptors are executed in the order they are added
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * client.use(async (url, init) => {
101
+ * // Add auth header
102
+ * return {
103
+ * ...init,
104
+ * headers: {
105
+ * ...init.headers,
106
+ * Authorization: `Bearer ${token}`
107
+ * }
108
+ * };
109
+ * });
110
+ * ```
111
+ */
112
+ use(interceptor: RequestInterceptor): void;
113
+ /**
114
+ * Make a type-safe API call using a contract
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const getUserContract = {
119
+ * params: Type.Object({ id: Type.String() }),
120
+ * response: Type.Object({ id: Type.Number(), name: Type.String() })
121
+ * } as const satisfies RouteContract;
122
+ *
123
+ * const user = await client.call('/users/:id', getUserContract, {
124
+ * params: { id: '123' }
125
+ * });
126
+ * // ✅ user.name is typed as string
127
+ * ```
128
+ */
129
+ call<TContract extends RouteContract>(path: string, contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>;
130
+ /**
131
+ * Create a new client with merged configuration
132
+ *
133
+ * Useful for creating clients with specific auth tokens or custom headers
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const authClient = client.withConfig({
138
+ * headers: { Authorization: `Bearer ${token}` }
139
+ * });
140
+ * ```
141
+ */
142
+ withConfig(config: Partial<ClientConfig>): ContractClient;
143
+ }
144
+ /**
145
+ * Create a new contract-based API client
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const client = createClient({
150
+ * baseUrl: 'http://localhost:4000',
151
+ * headers: { 'X-Custom': 'header' }
152
+ * });
153
+ *
154
+ * const user = await client.call('/users/:id', getUserContract, {
155
+ * params: { id: '123' }
156
+ * });
157
+ * ```
158
+ */
159
+ declare function createClient(config?: ClientConfig): ContractClient;
160
+ /**
161
+ * Default client instance
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * import { client } from '@spfn/core/client';
166
+ *
167
+ * const user = await client.call('/users/:id', getUserContract, {
168
+ * params: { id: '123' }
169
+ * });
170
+ * ```
171
+ */
172
+ declare const client: ContractClient;
173
+
174
+ export { ApiClientError, type CallOptions, type ClientConfig, ContractClient, type RequestInterceptor, client, createClient };
@@ -0,0 +1,179 @@
1
+ // src/client/contract-client.ts
2
+ var ApiClientError = class extends Error {
3
+ constructor(message, status, statusText, url, response) {
4
+ super(message);
5
+ this.status = status;
6
+ this.statusText = statusText;
7
+ this.url = url;
8
+ this.response = response;
9
+ this.name = "ApiClientError";
10
+ }
11
+ };
12
+ function buildUrl(path, params) {
13
+ if (!params) return path;
14
+ let url = path;
15
+ for (const [key, value] of Object.entries(params)) {
16
+ url = url.replace(`:${key}`, String(value));
17
+ }
18
+ return url;
19
+ }
20
+ function buildQuery(query) {
21
+ if (!query || Object.keys(query).length === 0) return "";
22
+ const params = new URLSearchParams();
23
+ for (const [key, value] of Object.entries(query)) {
24
+ if (Array.isArray(value)) {
25
+ value.forEach((v) => params.append(key, String(v)));
26
+ } else if (value !== void 0 && value !== null) {
27
+ params.append(key, String(value));
28
+ }
29
+ }
30
+ const queryString = params.toString();
31
+ return queryString ? `?${queryString}` : "";
32
+ }
33
+ function getHttpMethod(contract, options) {
34
+ if ("method" in contract && typeof contract.method === "string") {
35
+ return contract.method.toUpperCase();
36
+ }
37
+ if (options?.body !== void 0) {
38
+ return "POST";
39
+ }
40
+ return "GET";
41
+ }
42
+ var ContractClient = class _ContractClient {
43
+ config;
44
+ interceptors = [];
45
+ constructor(config = {}) {
46
+ this.config = {
47
+ baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000",
48
+ headers: config.headers || {},
49
+ timeout: config.timeout || 3e4,
50
+ fetch: config.fetch || globalThis.fetch
51
+ };
52
+ }
53
+ /**
54
+ * Add request interceptor
55
+ *
56
+ * Interceptors are executed in the order they are added
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * client.use(async (url, init) => {
61
+ * // Add auth header
62
+ * return {
63
+ * ...init,
64
+ * headers: {
65
+ * ...init.headers,
66
+ * Authorization: `Bearer ${token}`
67
+ * }
68
+ * };
69
+ * });
70
+ * ```
71
+ */
72
+ use(interceptor) {
73
+ this.interceptors.push(interceptor);
74
+ }
75
+ /**
76
+ * Make a type-safe API call using a contract
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const getUserContract = {
81
+ * params: Type.Object({ id: Type.String() }),
82
+ * response: Type.Object({ id: Type.Number(), name: Type.String() })
83
+ * } as const satisfies RouteContract;
84
+ *
85
+ * const user = await client.call('/users/:id', getUserContract, {
86
+ * params: { id: '123' }
87
+ * });
88
+ * // ✅ user.name is typed as string
89
+ * ```
90
+ */
91
+ async call(path, contract, options) {
92
+ const baseUrl = options?.baseUrl || this.config.baseUrl;
93
+ const urlPath = buildUrl(path, options?.params);
94
+ const queryString = buildQuery(options?.query);
95
+ const url = `${baseUrl}${urlPath}${queryString}`;
96
+ const method = getHttpMethod(contract, options);
97
+ const headers = {
98
+ ...this.config.headers,
99
+ ...options?.headers
100
+ };
101
+ if (options?.body !== void 0 && !headers["Content-Type"]) {
102
+ headers["Content-Type"] = "application/json";
103
+ }
104
+ let init = {
105
+ method,
106
+ headers
107
+ };
108
+ if (options?.body !== void 0) {
109
+ init.body = JSON.stringify(options.body);
110
+ }
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
113
+ init.signal = controller.signal;
114
+ for (const interceptor of this.interceptors) {
115
+ init = await interceptor(url, init);
116
+ }
117
+ const response = await this.config.fetch(url, init).catch((error) => {
118
+ clearTimeout(timeoutId);
119
+ if (error instanceof Error && error.name === "AbortError") {
120
+ throw new ApiClientError(
121
+ `${method} ${urlPath} timed out after ${this.config.timeout}ms`,
122
+ 0,
123
+ "Timeout",
124
+ url
125
+ );
126
+ }
127
+ if (error instanceof Error) {
128
+ throw new ApiClientError(
129
+ `${method} ${urlPath} network error: ${error.message}`,
130
+ 0,
131
+ "Network Error",
132
+ url
133
+ );
134
+ }
135
+ throw error;
136
+ });
137
+ clearTimeout(timeoutId);
138
+ if (!response.ok) {
139
+ const errorBody = await response.json().catch(() => null);
140
+ throw new ApiClientError(
141
+ `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,
142
+ response.status,
143
+ response.statusText,
144
+ url,
145
+ errorBody
146
+ );
147
+ }
148
+ const data = await response.json();
149
+ return data;
150
+ }
151
+ /**
152
+ * Create a new client with merged configuration
153
+ *
154
+ * Useful for creating clients with specific auth tokens or custom headers
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const authClient = client.withConfig({
159
+ * headers: { Authorization: `Bearer ${token}` }
160
+ * });
161
+ * ```
162
+ */
163
+ withConfig(config) {
164
+ return new _ContractClient({
165
+ baseUrl: config.baseUrl || this.config.baseUrl,
166
+ headers: { ...this.config.headers, ...config.headers },
167
+ timeout: config.timeout || this.config.timeout,
168
+ fetch: config.fetch || this.config.fetch
169
+ });
170
+ }
171
+ };
172
+ function createClient(config) {
173
+ return new ContractClient(config);
174
+ }
175
+ var client = createClient();
176
+
177
+ export { ApiClientError, ContractClient, client, createClient };
178
+ //# sourceMappingURL=index.js.map
179
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/contract-client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract-Based API Client\n *\n * Type-safe HTTP client that works with RouteContract for full end-to-end type safety\n *\n * @example\n * ```ts\n * import { createClient } from '@spfn/core/client';\n * import { getUserContract } from './contracts';\n *\n * const client = createClient({ baseUrl: 'http://localhost:4000' });\n * const user = await client.call(getUserContract, { params: { id: '123' } });\n * // ✅ user is fully typed based on contract.response\n * ```\n */\n\nimport type { RouteContract, InferContract } from '../route';\n\n/**\n * Request interceptor function\n *\n * Allows modifying request before it's sent\n */\nexport type RequestInterceptor = (\n url: string,\n init: RequestInit\n) => Promise<RequestInit> | RequestInit;\n\n/**\n * Client configuration\n */\nexport interface ClientConfig\n{\n /**\n * API base URL (e.g., http://localhost:4000)\n * Can be overridden per request\n */\n baseUrl?: string;\n\n /**\n * Default headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation (for testing or custom behavior)\n */\n fetch?: typeof fetch;\n}\n\n/**\n * Request options for API calls\n */\nexport interface CallOptions<TContract extends RouteContract>\n{\n /**\n * Path parameters (for dynamic routes like /users/:id)\n */\n params?: InferContract<TContract>['params'];\n\n /**\n * Query parameters (for URL query strings)\n */\n query?: InferContract<TContract>['query'];\n\n /**\n * Request body (for POST, PUT, PATCH)\n */\n body?: InferContract<TContract>['body'];\n\n /**\n * Additional headers for this specific request\n */\n headers?: Record<string, string>;\n\n /**\n * Override base URL for this request\n */\n baseUrl?: string;\n}\n\n/**\n * API Client Error\n */\nexport class ApiClientError extends Error\n{\n constructor(\n message: string,\n public readonly status: number,\n public readonly statusText: string,\n public readonly url: string,\n public readonly response?: unknown\n )\n {\n super(message);\n this.name = 'ApiClientError';\n }\n}\n\n/**\n * Build URL with path parameters replaced\n *\n * @example\n * buildUrl('/users/:id', { id: '123' }) → '/users/123'\n * buildUrl('/posts/:postId/comments/:id', { postId: '1', id: '2' }) → '/posts/1/comments/2'\n */\nfunction buildUrl(path: string, params?: Record<string, string | number>): string\n{\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n}\n\n/**\n * Build query string from object\n *\n * @example\n * buildQuery({ page: '1', limit: '10' }) → '?page=1&limit=10'\n */\nfunction buildQuery(query?: Record<string, string | string[] | number | boolean>): string\n{\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n}\n\n/**\n * Extract HTTP method from contract or infer from request type\n */\nfunction getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n): string\n{\n // If contract has explicit method, use it\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n // Infer from presence of body\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n // Default to GET\n return 'GET';\n}\n\n/**\n * Contract-based API Client\n */\nexport class ContractClient\n{\n private readonly config: Required<ClientConfig>;\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(config: ClientConfig = {})\n {\n this.config = {\n baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n headers: config.headers || {},\n timeout: config.timeout || 30000,\n fetch: config.fetch || globalThis.fetch,\n };\n }\n\n /**\n * Add request interceptor\n *\n * Interceptors are executed in the order they are added\n *\n * @example\n * ```ts\n * client.use(async (url, init) => {\n * // Add auth header\n * return {\n * ...init,\n * headers: {\n * ...init.headers,\n * Authorization: `Bearer ${token}`\n * }\n * };\n * });\n * ```\n */\n use(interceptor: RequestInterceptor): void\n {\n this.interceptors.push(interceptor);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * @example\n * ```ts\n * const getUserContract = {\n * params: Type.Object({ id: Type.String() }),\n * response: Type.Object({ id: Type.Number(), name: Type.String() })\n * } as const satisfies RouteContract;\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * // ✅ user.name is typed as string\n * ```\n */\n async call<TContract extends RouteContract>(\n path: string,\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n // Build URL\n const baseUrl = options?.baseUrl || this.config.baseUrl;\n const urlPath = buildUrl(path, options?.params as Record<string, string | number>);\n const queryString = buildQuery(options?.query as Record<string, string | string[] | number | boolean>);\n const url = `${baseUrl}${urlPath}${queryString}`;\n\n // Determine HTTP method\n const method = getHttpMethod(contract, options);\n\n // Build headers\n const headers: Record<string, string> = {\n ...this.config.headers,\n ...options?.headers,\n };\n\n // Add Content-Type for requests with body\n if (options?.body !== undefined && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n // Build request init\n let init: RequestInit = {\n method,\n headers,\n };\n\n // Add body for POST/PUT/PATCH\n if (options?.body !== undefined)\n {\n init.body = JSON.stringify(options.body);\n }\n\n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n\n // Execute interceptors\n for (const interceptor of this.interceptors)\n {\n init = await interceptor(url, init);\n }\n\n // Make request\n const response = await this.config.fetch(url, init).catch((error) =>\n {\n clearTimeout(timeoutId);\n\n // Handle abort (timeout)\n if (error instanceof Error && error.name === 'AbortError')\n {\n throw new ApiClientError(\n `${method} ${urlPath} timed out after ${this.config.timeout}ms`,\n 0,\n 'Timeout',\n url\n );\n }\n\n // Handle network errors\n if (error instanceof Error)\n {\n throw new ApiClientError(\n `${method} ${urlPath} network error: ${error.message}`,\n 0,\n 'Network Error',\n url\n );\n }\n\n // Unknown error\n throw error;\n });\n\n // Clear timeout\n clearTimeout(timeoutId);\n\n // Handle non-OK responses\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,\n response.status,\n response.statusText,\n url,\n errorBody\n );\n }\n\n // Parse and return response\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Create a new client with merged configuration\n *\n * Useful for creating clients with specific auth tokens or custom headers\n *\n * @example\n * ```ts\n * const authClient = client.withConfig({\n * headers: { Authorization: `Bearer ${token}` }\n * });\n * ```\n */\n withConfig(config: Partial<ClientConfig>): ContractClient\n {\n return new ContractClient({\n baseUrl: config.baseUrl || this.config.baseUrl,\n headers: { ...this.config.headers, ...config.headers },\n timeout: config.timeout || this.config.timeout,\n fetch: config.fetch || this.config.fetch,\n });\n }\n}\n\n/**\n * Create a new contract-based API client\n *\n * @example\n * ```ts\n * const client = createClient({\n * baseUrl: 'http://localhost:4000',\n * headers: { 'X-Custom': 'header' }\n * });\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * ```\n */\nexport function createClient(config?: ClientConfig): ContractClient\n{\n return new ContractClient(config);\n}\n\n/**\n * Default client instance\n *\n * @example\n * ```ts\n * import { client } from '@spfn/core/client';\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * ```\n */\nexport const client = createClient();"]}
@@ -0,0 +1,126 @@
1
+ import { d as HttpMethod } from '../types-SlzTr8ZO.js';
2
+ import 'hono';
3
+ import 'hono/utils/http-status';
4
+ import '@sinclair/typebox';
5
+
6
+ /**
7
+ * Code Generation Types
8
+ *
9
+ * Types for contract detection and client code generation
10
+ */
11
+
12
+ /**
13
+ * Route-Contract mapping extracted from contract files
14
+ */
15
+ interface RouteContractMapping {
16
+ /** HTTP method (GET, POST, etc.) */
17
+ method: HttpMethod;
18
+ /** URL path (e.g., /users/:id) */
19
+ path: string;
20
+ /** Contract variable name (e.g., getUserContract) */
21
+ contractName: string;
22
+ /** Import path for the contract (e.g., @/contracts/users) */
23
+ contractImportPath: string;
24
+ /** Route file path */
25
+ routeFile: string;
26
+ /** Contract source file path (resolved) */
27
+ contractFile?: string;
28
+ }
29
+ /**
30
+ * Grouped routes by resource
31
+ */
32
+ interface ResourceRoutes {
33
+ [resource: string]: RouteContractMapping[];
34
+ }
35
+ /**
36
+ * Client generation options
37
+ */
38
+ interface ClientGenerationOptions {
39
+ /** Routes directory to scan */
40
+ routesDir: string;
41
+ /** Output file path for generated client */
42
+ outputPath: string;
43
+ /** Base URL for the API client */
44
+ baseUrl?: string;
45
+ /** Include type imports? */
46
+ includeTypes?: boolean;
47
+ /** Generate JSDoc comments? */
48
+ includeJsDoc?: boolean;
49
+ }
50
+ /**
51
+ * Generation statistics
52
+ */
53
+ interface GenerationStats {
54
+ /** Total routes scanned */
55
+ routesScanned: number;
56
+ /** Routes with contracts found */
57
+ contractsFound: number;
58
+ /** Unique contract files */
59
+ contractFiles: number;
60
+ /** Resources generated */
61
+ resourcesGenerated: number;
62
+ /** Total methods generated */
63
+ methodsGenerated: number;
64
+ /** Generation time in ms */
65
+ duration: number;
66
+ }
67
+
68
+ /**
69
+ * Contract Scanner
70
+ *
71
+ * Scans server/contracts directory and extracts exported contracts
72
+ */
73
+
74
+ /**
75
+ * Scan routes directory for contract.ts files and extract contract exports
76
+ *
77
+ * @param routesDir - Path to server/routes directory
78
+ * @returns Array of contract-to-route mappings
79
+ */
80
+ declare function scanContracts(routesDir: string): Promise<RouteContractMapping[]>;
81
+
82
+ /**
83
+ * Route Scanner Utilities
84
+ *
85
+ * Helper functions for grouping and organizing route-contract mappings
86
+ */
87
+
88
+ /**
89
+ * Group mappings by resource
90
+ */
91
+ declare function groupByResource(mappings: RouteContractMapping[]): Record<string, RouteContractMapping[]>;
92
+
93
+ /**
94
+ * Client Code Generator
95
+ *
96
+ * Generates type-safe API client code from route-contract mappings
97
+ */
98
+
99
+ /**
100
+ * Generate API client code
101
+ */
102
+ declare function generateClient(mappings: RouteContractMapping[], options: ClientGenerationOptions): Promise<GenerationStats>;
103
+
104
+ /**
105
+ * Contract Watcher & Client Generator
106
+ *
107
+ * Watches contract files and regenerates client code
108
+ */
109
+ interface WatchGenerateOptions {
110
+ /** Routes directory (default: src/server/routes) */
111
+ routesDir?: string;
112
+ /** Output path for generated client (default: src/lib/api/client.ts) */
113
+ outputPath?: string;
114
+ /** Base URL for API client */
115
+ baseUrl?: string;
116
+ /** Enable debug logging */
117
+ debug?: boolean;
118
+ }
119
+ /**
120
+ * Watch contracts and generate client code
121
+ *
122
+ * This file is meant to be run with tsx --watch
123
+ */
124
+ declare function watchAndGenerate(options?: WatchGenerateOptions): Promise<void>;
125
+
126
+ export { type ClientGenerationOptions, type GenerationStats, HttpMethod, type ResourceRoutes, type RouteContractMapping, generateClient, groupByResource, scanContracts, watchAndGenerate };