@spfn/core 0.1.0-alpha.1 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.js +1 -3
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/index.d.ts +3 -3
- package/dist/codegen/index.js +56 -11
- package/dist/codegen/index.js.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scripts/index.js +56 -12
- package/dist/scripts/index.js.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/client/index.d.ts
CHANGED
|
@@ -78,10 +78,9 @@ interface CallOptions<TContract extends RouteContract> {
|
|
|
78
78
|
*/
|
|
79
79
|
declare class ApiClientError extends Error {
|
|
80
80
|
readonly status: number;
|
|
81
|
-
readonly statusText: string;
|
|
82
81
|
readonly url: string;
|
|
83
82
|
readonly response?: unknown | undefined;
|
|
84
|
-
constructor(message: string, status: number,
|
|
83
|
+
constructor(message: string, status: number, url: string, response?: unknown | undefined);
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
86
|
* Contract-based API Client
|
package/dist/client/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/client/contract-client.ts
|
|
2
2
|
var ApiClientError = class extends Error {
|
|
3
|
-
constructor(message, status,
|
|
3
|
+
constructor(message, status, url, response) {
|
|
4
4
|
super(message);
|
|
5
5
|
this.status = status;
|
|
6
|
-
this.statusText = statusText;
|
|
7
6
|
this.url = url;
|
|
8
7
|
this.response = response;
|
|
9
8
|
this.name = "ApiClientError";
|
|
@@ -140,7 +139,6 @@ var ContractClient = class _ContractClient {
|
|
|
140
139
|
throw new ApiClientError(
|
|
141
140
|
`${method} ${urlPath} failed: ${response.status} ${response.statusText}`,
|
|
142
141
|
response.status,
|
|
143
|
-
response.statusText,
|
|
144
142
|
url,
|
|
145
143
|
errorBody
|
|
146
144
|
);
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +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();"]}
|
|
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 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 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();"]}
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -109,17 +109,17 @@ declare function generateClient(mappings: RouteContractMapping[], options: Clien
|
|
|
109
109
|
interface WatchGenerateOptions {
|
|
110
110
|
/** Routes directory (default: src/server/routes) */
|
|
111
111
|
routesDir?: string;
|
|
112
|
-
/** Output path for generated client (default: src/lib/api
|
|
112
|
+
/** Output path for generated client (default: src/lib/api.ts) */
|
|
113
113
|
outputPath?: string;
|
|
114
114
|
/** Base URL for API client */
|
|
115
115
|
baseUrl?: string;
|
|
116
116
|
/** Enable debug logging */
|
|
117
117
|
debug?: boolean;
|
|
118
|
+
/** Watch mode (default: true) */
|
|
119
|
+
watch?: boolean;
|
|
118
120
|
}
|
|
119
121
|
/**
|
|
120
122
|
* Watch contracts and generate client code
|
|
121
|
-
*
|
|
122
|
-
* This file is meant to be run with tsx --watch
|
|
123
123
|
*/
|
|
124
124
|
declare function watchAndGenerate(options?: WatchGenerateOptions): Promise<void>;
|
|
125
125
|
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile, readdir, stat } from 'fs/promises';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import * as ts from 'typescript';
|
|
4
|
-
import {
|
|
4
|
+
import { existsSync, mkdirSync, createWriteStream, readFileSync } from 'fs';
|
|
5
|
+
import { watch } from 'chokidar';
|
|
5
6
|
import pino from 'pino';
|
|
6
7
|
|
|
7
8
|
// src/codegen/contract-scanner.ts
|
|
@@ -925,40 +926,84 @@ var logger = createAdapter(getAdapterType());
|
|
|
925
926
|
|
|
926
927
|
// src/codegen/watch-generate.ts
|
|
927
928
|
var codegenLogger = logger.child("codegen");
|
|
928
|
-
async function
|
|
929
|
+
async function generateOnce(options) {
|
|
929
930
|
const cwd = process.cwd();
|
|
930
931
|
const routesDir = options.routesDir ?? join(cwd, "src", "server", "routes");
|
|
931
|
-
const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api
|
|
932
|
-
const debug = options.debug ?? false;
|
|
933
|
-
if (debug) {
|
|
934
|
-
codegenLogger.info("Contract Watcher Started", { routesDir, outputPath });
|
|
935
|
-
}
|
|
932
|
+
const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api.ts");
|
|
936
933
|
try {
|
|
937
934
|
const contracts = await scanContracts(routesDir);
|
|
938
935
|
if (contracts.length === 0) {
|
|
939
|
-
if (debug) {
|
|
936
|
+
if (options.debug) {
|
|
940
937
|
codegenLogger.warn("No contracts found");
|
|
941
938
|
}
|
|
942
|
-
return;
|
|
939
|
+
return null;
|
|
943
940
|
}
|
|
944
941
|
const stats = await generateClient(contracts, {
|
|
945
942
|
outputPath,
|
|
946
943
|
includeTypes: true,
|
|
947
944
|
includeJsDoc: true
|
|
948
945
|
});
|
|
949
|
-
if (debug) {
|
|
946
|
+
if (options.debug) {
|
|
950
947
|
codegenLogger.info("Client generated", {
|
|
951
948
|
endpoints: stats.methodsGenerated,
|
|
952
949
|
resources: stats.resourcesGenerated,
|
|
953
950
|
duration: stats.duration
|
|
954
951
|
});
|
|
955
952
|
}
|
|
953
|
+
return stats;
|
|
956
954
|
} catch (error) {
|
|
957
955
|
codegenLogger.error(
|
|
958
956
|
"Generation failed",
|
|
959
957
|
error instanceof Error ? error : new Error(String(error))
|
|
960
958
|
);
|
|
961
|
-
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function watchAndGenerate(options = {}) {
|
|
963
|
+
const cwd = process.cwd();
|
|
964
|
+
const routesDir = options.routesDir ?? join(cwd, "src", "server", "routes");
|
|
965
|
+
const outputPath = options.outputPath ?? join(cwd, "src", "lib", "api.ts");
|
|
966
|
+
const watchMode = options.watch !== false;
|
|
967
|
+
if (options.debug) {
|
|
968
|
+
codegenLogger.info("Contract Watcher Started", { routesDir, outputPath, watch: watchMode });
|
|
969
|
+
}
|
|
970
|
+
await generateOnce(options);
|
|
971
|
+
if (watchMode) {
|
|
972
|
+
let isGenerating = false;
|
|
973
|
+
let pendingRegeneration = false;
|
|
974
|
+
const watcher = watch(routesDir, {
|
|
975
|
+
ignored: /(^|[\/\\])\../,
|
|
976
|
+
// ignore dotfiles
|
|
977
|
+
persistent: true,
|
|
978
|
+
ignoreInitial: true,
|
|
979
|
+
awaitWriteFinish: {
|
|
980
|
+
stabilityThreshold: 100,
|
|
981
|
+
pollInterval: 50
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
const regenerate = async () => {
|
|
985
|
+
if (isGenerating) {
|
|
986
|
+
pendingRegeneration = true;
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
isGenerating = true;
|
|
990
|
+
pendingRegeneration = false;
|
|
991
|
+
if (options.debug) {
|
|
992
|
+
codegenLogger.info("Contracts changed, regenerating...");
|
|
993
|
+
}
|
|
994
|
+
await generateOnce(options);
|
|
995
|
+
isGenerating = false;
|
|
996
|
+
if (pendingRegeneration) {
|
|
997
|
+
await regenerate();
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
watcher.on("add", regenerate).on("change", regenerate).on("unlink", regenerate);
|
|
1001
|
+
process.on("SIGINT", () => {
|
|
1002
|
+
watcher.close();
|
|
1003
|
+
process.exit(0);
|
|
1004
|
+
});
|
|
1005
|
+
await new Promise(() => {
|
|
1006
|
+
});
|
|
962
1007
|
}
|
|
963
1008
|
}
|
|
964
1009
|
if (import.meta.url === `file://${process.argv[1]}`) {
|