@mpen/routekit 0.1.0 → 0.1.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/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -1,986 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
import { parseArgs } from 'util'
|
|
6
|
-
import { $ } from 'bun'
|
|
7
|
-
import { compile } from 'json-schema-to-typescript'
|
|
8
|
-
import { HttpMethod } from '@mpen/http'
|
|
9
|
-
import type { JsonSchema, NormalizedRoute, RouteSchema } from '../router/types'
|
|
10
|
-
|
|
11
|
-
const DEFAULT_RESPONSE_TYPE = 'ApiResponsePromise'
|
|
12
|
-
const DEFAULT_FORMAT = 'rk-api-client'
|
|
13
|
-
const OUTPUT_FORMATS = ['rk-api-client', 'ts-query-rk-problem'] as const
|
|
14
|
-
|
|
15
|
-
type OutputFormat = (typeof OUTPUT_FORMATS)[number]
|
|
16
|
-
|
|
17
|
-
type ExtractedRouteMeta = {
|
|
18
|
-
name: string[]
|
|
19
|
-
method: HttpMethod
|
|
20
|
-
path: string
|
|
21
|
-
requestSchema?: RouteSchema['request']
|
|
22
|
-
responseBodySchemas?: NonNullable<RouteSchema['response']>['body']
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type ProcessedRouteMeta = ExtractedRouteMeta & {
|
|
26
|
-
typeBase: string
|
|
27
|
-
pathParams: string[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type RouteNode = {
|
|
31
|
-
routes: ProcessedRouteMeta[]
|
|
32
|
-
children: Map<string, RouteNode>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type ImportType = {
|
|
36
|
-
names: string[]
|
|
37
|
-
module: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type BuildOptions = {
|
|
41
|
-
clientName: string
|
|
42
|
-
responseType: string
|
|
43
|
-
importTypes: ImportType[]
|
|
44
|
-
commandText: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type GeneratedRouteTypes = {
|
|
48
|
-
route: ProcessedRouteMeta
|
|
49
|
-
pathTypeSource?: string
|
|
50
|
-
queryTypeSource?: string
|
|
51
|
-
requestTypeSource?: string
|
|
52
|
-
responseTypesByStatusSource?: string
|
|
53
|
-
responseTypeSources: string[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type RoutableModule = {
|
|
57
|
-
getRoutes(): NormalizedRoute<any>[]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function printHelp(): void {
|
|
61
|
-
console.log(`Usage: bun run packages/routekit/src/bin/gen-api-client.ts <router-file> [options]
|
|
62
|
-
|
|
63
|
-
Generate a typed API client from a routekit router module.
|
|
64
|
-
|
|
65
|
-
Arguments:
|
|
66
|
-
router-file Router module that exports a router with getRoutes()
|
|
67
|
-
|
|
68
|
-
Options:
|
|
69
|
-
-o, --output <file> File to write. Prints to stdout when omitted.
|
|
70
|
-
-w, --write Write to <router-file>.gen.ts beside the router file.
|
|
71
|
-
-p, --pretty Format written output with Prettier.
|
|
72
|
-
-f, --format <format> Output format: rk-api-client or ts-query-rk-problem. Defaults to rk-api-client.
|
|
73
|
-
--client-name <Name> Generated client class name. Defaults to ApiClient.
|
|
74
|
-
--import-type <Type:module> Import a type used by generated schemas. Can be repeated.
|
|
75
|
-
--response-type <Type> Generic response wrapper type. Defaults to ApiResponsePromise.
|
|
76
|
-
--help Show this help message.`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function routeTypeBaseName(route: ExtractedRouteMeta): string {
|
|
80
|
-
const parts = route.name.length > 0 ? route.name : ['index']
|
|
81
|
-
return upperFirst(route.method.toLowerCase()) + parts.map(upperFirst).join('')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getPathParamNames(routePath: string): string[] {
|
|
85
|
-
const matches = routePath.match(/:([a-zA-Z0-9_]+)/g) ?? []
|
|
86
|
-
return matches.map((match) => match.slice(1))
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function buildRouteTree(routes: ProcessedRouteMeta[]): RouteNode {
|
|
90
|
-
const root: RouteNode = { routes: [], children: new Map() }
|
|
91
|
-
for (const route of routes) {
|
|
92
|
-
let node = root
|
|
93
|
-
for (const segment of route.name) {
|
|
94
|
-
if (!node.children.has(segment)) {
|
|
95
|
-
node.children.set(segment, { routes: [], children: new Map() })
|
|
96
|
-
}
|
|
97
|
-
node = node.children.get(segment)!
|
|
98
|
-
}
|
|
99
|
-
node.routes.push(route)
|
|
100
|
-
}
|
|
101
|
-
return root
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function classNameForParts(parts: string[], baseName: string): string {
|
|
105
|
-
if (parts.length === 0) return baseName
|
|
106
|
-
return `${baseName}_${parts.map(upperFirst).join('_')}`
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function parseImportTypeOption(value: string): ImportType {
|
|
110
|
-
const colonIdx = value.indexOf(':')
|
|
111
|
-
if (colonIdx === -1) {
|
|
112
|
-
throw new Error(`Invalid --import-type value: "${value}"`)
|
|
113
|
-
}
|
|
114
|
-
const names = value.slice(0, colonIdx)
|
|
115
|
-
const module = value.slice(colonIdx + 1).trim()
|
|
116
|
-
const parsedNames = names
|
|
117
|
-
.split(',')
|
|
118
|
-
.map((name) => name.trim())
|
|
119
|
-
.filter(Boolean)
|
|
120
|
-
if (parsedNames.length === 0 || module.length === 0) {
|
|
121
|
-
throw new Error(`Invalid --import-type value: "${value}"`)
|
|
122
|
-
}
|
|
123
|
-
return { names: parsedNames, module }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeClientName(name: string | undefined): string {
|
|
127
|
-
if (!name) return 'ApiClient'
|
|
128
|
-
return (
|
|
129
|
-
name
|
|
130
|
-
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
131
|
-
.trim()
|
|
132
|
-
.split(/\s+/)
|
|
133
|
-
.map(upperFirst)
|
|
134
|
-
.join('') || 'ApiClient'
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function parseOutputFormat(value: string | undefined): OutputFormat {
|
|
139
|
-
const format = (value?.trim() || DEFAULT_FORMAT) as OutputFormat
|
|
140
|
-
if ((OUTPUT_FORMATS as readonly string[]).includes(format)) return format
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Invalid --format value: "${value}". Expected one of: ${OUTPUT_FORMATS.join(', ')}`,
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function upperFirst(str: string): string {
|
|
147
|
-
return str.slice(0, 1).toUpperCase() + str.slice(1)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function patternToUrlTemplate(routePath: string, pathVar = 'path'): string {
|
|
151
|
-
const templated = routePath.replace(
|
|
152
|
-
/:([a-zA-Z0-9_]+)/g,
|
|
153
|
-
`\${encodeURIComponent(String(${pathVar}.$1))}`,
|
|
154
|
-
)
|
|
155
|
-
if (templated.includes('${')) {
|
|
156
|
-
return '`' + templated + '`'
|
|
157
|
-
}
|
|
158
|
-
return `"${routePath}"`
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function isUnknown(text: string): boolean {
|
|
162
|
-
return text === 'unknown' || text === 'any'
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isUnconstrainedJsonSchema(schema: JsonSchema): boolean {
|
|
166
|
-
return (
|
|
167
|
-
schema === true ||
|
|
168
|
-
(schema !== false &&
|
|
169
|
-
typeof schema === 'object' &&
|
|
170
|
-
!Array.isArray(schema) &&
|
|
171
|
-
Object.keys(schema).length === 0)
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function isNeverJsonSchema(schema: JsonSchema): boolean {
|
|
176
|
-
return (
|
|
177
|
-
schema === false ||
|
|
178
|
-
(schema !== true &&
|
|
179
|
-
typeof schema === 'object' &&
|
|
180
|
-
!Array.isArray(schema) &&
|
|
181
|
-
Object.keys(schema).length === 1 &&
|
|
182
|
-
typeof schema.not === 'object' &&
|
|
183
|
-
schema.not !== null &&
|
|
184
|
-
!Array.isArray(schema.not) &&
|
|
185
|
-
Object.keys(schema.not).length === 0)
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function isJsonSchemaObject(schema: JsonSchema): schema is Record<string, unknown> {
|
|
190
|
-
return (
|
|
191
|
-
schema !== true && schema !== false && typeof schema === 'object' && !Array.isArray(schema)
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function isNumericStringSchema(schema: unknown): boolean {
|
|
196
|
-
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return false
|
|
197
|
-
const schemaObject = schema as Record<string, unknown>
|
|
198
|
-
return (
|
|
199
|
-
schemaObject.type === 'string' &&
|
|
200
|
-
typeof schemaObject.pattern === 'string' &&
|
|
201
|
-
['^\\d+$', '^[0-9]+$'].includes(schemaObject.pattern)
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function widenNumericStringPathParams(schema: JsonSchema, pathParams: string[]): JsonSchema {
|
|
206
|
-
if (!isJsonSchemaObject(schema)) return schema
|
|
207
|
-
const properties = schema.properties
|
|
208
|
-
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
|
|
209
|
-
return schema
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const nextProperties: Record<string, unknown> = { ...properties }
|
|
213
|
-
let widened = false
|
|
214
|
-
for (const pathParam of pathParams) {
|
|
215
|
-
const propertySchema = nextProperties[pathParam]
|
|
216
|
-
if (!isNumericStringSchema(propertySchema)) continue
|
|
217
|
-
nextProperties[pathParam] = {
|
|
218
|
-
anyOf: [propertySchema, { type: 'number' }],
|
|
219
|
-
}
|
|
220
|
-
widened = true
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!widened) return schema
|
|
224
|
-
return {
|
|
225
|
-
...schema,
|
|
226
|
-
properties: nextProperties,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function normalizeMethod(routeMethod: NormalizedRoute<any>['method']): HttpMethod[] {
|
|
231
|
-
if (!routeMethod) return [HttpMethod.GET]
|
|
232
|
-
return Array.isArray(routeMethod) ? routeMethod : [routeMethod]
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function extractRoutes(routes: NormalizedRoute<any>[]): ExtractedRouteMeta[] {
|
|
236
|
-
const extracted: ExtractedRouteMeta[] = []
|
|
237
|
-
for (const route of routes) {
|
|
238
|
-
for (const method of normalizeMethod(route.method)) {
|
|
239
|
-
extracted.push({
|
|
240
|
-
name: route.name,
|
|
241
|
-
method,
|
|
242
|
-
path: route.path.pathname,
|
|
243
|
-
...(route.schema?.request ? { requestSchema: route.schema.request } : {}),
|
|
244
|
-
...(route.schema?.response?.body
|
|
245
|
-
? { responseBodySchemas: route.schema.response.body }
|
|
246
|
-
: {}),
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return extracted
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function resolveRouterModule(module: Record<string, unknown>): RoutableModule {
|
|
254
|
-
const candidates = [module.default, module.router, ...Object.values(module)]
|
|
255
|
-
for (const candidate of candidates) {
|
|
256
|
-
if (
|
|
257
|
-
candidate &&
|
|
258
|
-
typeof candidate === 'object' &&
|
|
259
|
-
typeof (candidate as RoutableModule).getRoutes === 'function'
|
|
260
|
-
) {
|
|
261
|
-
return candidate as RoutableModule
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
throw new Error('Unable to find an exported router with a getRoutes() method')
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function loadRuntimeRoutes(routerPath: string): Promise<NormalizedRoute<any>[]> {
|
|
268
|
-
const moduleUrl = pathToFileURL(routerPath).href
|
|
269
|
-
const module = await import(moduleUrl)
|
|
270
|
-
const router = resolveRouterModule(module as Record<string, unknown>)
|
|
271
|
-
return router.getRoutes()
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function compileSchemaType(name: string, schema: JsonSchema): Promise<string> {
|
|
275
|
-
if (isUnconstrainedJsonSchema(schema)) {
|
|
276
|
-
return `export type ${name} = unknown`
|
|
277
|
-
}
|
|
278
|
-
if (isNeverJsonSchema(schema)) {
|
|
279
|
-
return `export type ${name} = never`
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const source = (
|
|
283
|
-
await compile(schema as Record<string, unknown>, name, {
|
|
284
|
-
bannerComment: '',
|
|
285
|
-
additionalProperties: false,
|
|
286
|
-
style: {
|
|
287
|
-
singleQuote: true,
|
|
288
|
-
},
|
|
289
|
-
})
|
|
290
|
-
).trim()
|
|
291
|
-
return source.replace(/\bNoName\b/g, `${name}Schema`)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function formatWithPrettier(source: string, outputPath: string): Promise<string> {
|
|
295
|
-
let prettier
|
|
296
|
-
try {
|
|
297
|
-
prettier = await import('prettier')
|
|
298
|
-
} catch (cause) {
|
|
299
|
-
throw new Error('The --pretty option requires prettier to be installed.', { cause })
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const options = (await prettier.resolveConfig(outputPath)) ?? {}
|
|
303
|
-
return prettier.format(source, { ...options, filepath: outputPath })
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function responseStatusAlias(typeBase: string, status: string): string {
|
|
307
|
-
return `${typeBase}Response${status}`
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function isSuccessfulStatus(status: string): boolean {
|
|
311
|
-
const statusNumber = Number(status)
|
|
312
|
-
return Number.isInteger(statusNumber) && statusNumber >= 200 && statusNumber < 300
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function responseStatusAliases(
|
|
316
|
-
route: ProcessedRouteMeta,
|
|
317
|
-
statusFilter: (status: string) => boolean,
|
|
318
|
-
): string[] {
|
|
319
|
-
if (!route.responseBodySchemas) return []
|
|
320
|
-
return Object.entries(route.responseBodySchemas)
|
|
321
|
-
.filter(([status, responseSchema]) => responseSchema !== undefined && statusFilter(status))
|
|
322
|
-
.map(([status]) => responseStatusAlias(route.typeBase, status))
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function unionType(types: string[], fallback: string): string {
|
|
326
|
-
if (types.length === 0) return fallback
|
|
327
|
-
if (types.length === 1) return types[0]!
|
|
328
|
-
return types.join(' | ')
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function generateRouteTypes(route: ProcessedRouteMeta): Promise<GeneratedRouteTypes> {
|
|
332
|
-
const generated: GeneratedRouteTypes = {
|
|
333
|
-
route,
|
|
334
|
-
responseTypeSources: [],
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (route.requestSchema?.path) {
|
|
338
|
-
generated.pathTypeSource = await compileSchemaType(
|
|
339
|
-
`${route.typeBase}PathParams`,
|
|
340
|
-
widenNumericStringPathParams(route.requestSchema.path, route.pathParams),
|
|
341
|
-
)
|
|
342
|
-
}
|
|
343
|
-
if (route.requestSchema?.query) {
|
|
344
|
-
generated.queryTypeSource = await compileSchemaType(
|
|
345
|
-
`${route.typeBase}Query`,
|
|
346
|
-
route.requestSchema.query,
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
if (route.requestSchema?.body !== undefined) {
|
|
350
|
-
generated.requestTypeSource = await compileSchemaType(
|
|
351
|
-
`${route.typeBase}Request`,
|
|
352
|
-
route.requestSchema.body,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (route.responseBodySchemas && Object.keys(route.responseBodySchemas).length > 0) {
|
|
357
|
-
const responseTypesByStatus: string[] = []
|
|
358
|
-
for (const [status, responseSchema] of Object.entries(route.responseBodySchemas)) {
|
|
359
|
-
if (responseSchema === undefined) continue
|
|
360
|
-
const alias = responseStatusAlias(route.typeBase, status)
|
|
361
|
-
generated.responseTypeSources.push(await compileSchemaType(alias, responseSchema))
|
|
362
|
-
responseTypesByStatus.push(` ${JSON.stringify(status)}: ${alias}`)
|
|
363
|
-
}
|
|
364
|
-
generated.responseTypesByStatusSource =
|
|
365
|
-
responseTypesByStatus.length > 0
|
|
366
|
-
? [
|
|
367
|
-
`export interface ${route.typeBase}ResponsesByStatus {`,
|
|
368
|
-
...responseTypesByStatus,
|
|
369
|
-
`}`,
|
|
370
|
-
`export type ${route.typeBase}Response = ${route.typeBase}ResponsesByStatus[keyof ${route.typeBase}ResponsesByStatus]`,
|
|
371
|
-
].join('\n')
|
|
372
|
-
: `export type ${route.typeBase}Response = unknown`
|
|
373
|
-
} else {
|
|
374
|
-
const fallback = route.method === HttpMethod.HEAD ? 'never' : 'unknown'
|
|
375
|
-
generated.responseTypesByStatusSource = `export type ${route.typeBase}Response = ${fallback}`
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return generated
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function buildMethodLines(
|
|
382
|
-
route: ProcessedRouteMeta,
|
|
383
|
-
options: BuildOptions,
|
|
384
|
-
indent: string,
|
|
385
|
-
): string[] {
|
|
386
|
-
const lines: string[] = []
|
|
387
|
-
const methodName = route.method.toLowerCase()
|
|
388
|
-
const bodyType =
|
|
389
|
-
route.requestSchema?.body !== undefined ? `${route.typeBase}Request` : undefined
|
|
390
|
-
const queryType = route.requestSchema?.query ? `${route.typeBase}Query` : undefined
|
|
391
|
-
const hasPathParams = route.pathParams.length > 0
|
|
392
|
-
const hasSinglePathParam = route.pathParams.length === 1
|
|
393
|
-
let pathVar = 'path'
|
|
394
|
-
|
|
395
|
-
if (hasPathParams) {
|
|
396
|
-
if (hasSinglePathParam) {
|
|
397
|
-
pathVar = '_path'
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const shouldResolveApiResponse = options.responseType === DEFAULT_RESPONSE_TYPE
|
|
402
|
-
const hasResponsesByStatus =
|
|
403
|
-
!!route.responseBodySchemas && Object.keys(route.responseBodySchemas).length > 0
|
|
404
|
-
const returnType =
|
|
405
|
-
shouldResolveApiResponse && hasResponsesByStatus
|
|
406
|
-
? `ApiResponseByStatusPromise<${route.typeBase}ResponsesByStatus>`
|
|
407
|
-
: `${options.responseType}<${route.typeBase}Response>`
|
|
408
|
-
const params: string[] = []
|
|
409
|
-
if (hasPathParams) {
|
|
410
|
-
let pathParamType = pathTypeForRoute(route)
|
|
411
|
-
if (hasSinglePathParam) {
|
|
412
|
-
const singleParamType = route.requestSchema?.path
|
|
413
|
-
? `SinglePathParam<${route.typeBase}PathParams, "${route.pathParams[0]}">`
|
|
414
|
-
: 'any'
|
|
415
|
-
pathParamType = `${pathParamType} | ${singleParamType}`
|
|
416
|
-
}
|
|
417
|
-
params.push(`path: ${pathParamType}`)
|
|
418
|
-
}
|
|
419
|
-
if (queryType) params.push(`query: ${queryType}`)
|
|
420
|
-
if (bodyType) params.push(`body: ${bodyType}`)
|
|
421
|
-
|
|
422
|
-
lines.push(`${indent}${methodName} = (${params.join(', ')}): ${returnType} => {`)
|
|
423
|
-
if (hasSinglePathParam) {
|
|
424
|
-
lines.push(
|
|
425
|
-
`${indent} const _path = typeof path === 'object' && path !== null && !Array.isArray(path) ? path : { ${route.pathParams[0]}: path } as any`,
|
|
426
|
-
)
|
|
427
|
-
}
|
|
428
|
-
const urlExpr = patternToUrlTemplate(route.path, pathVar)
|
|
429
|
-
const finalUrlExpr = queryType ? `withQuery(${urlExpr}, query)` : urlExpr
|
|
430
|
-
const resolverExpression =
|
|
431
|
-
shouldResolveApiResponse && hasResponsesByStatus
|
|
432
|
-
? `resolveApiResponseByStatus<${route.typeBase}ResponsesByStatus>`
|
|
433
|
-
: 'resolveApiResponse'
|
|
434
|
-
lines.push(
|
|
435
|
-
`${indent} return ${shouldResolveApiResponse ? `${resolverExpression}(` : ''}this.transport.request({`,
|
|
436
|
-
)
|
|
437
|
-
lines.push(`${indent} url: ${finalUrlExpr},`)
|
|
438
|
-
lines.push(`${indent} init: {`)
|
|
439
|
-
lines.push(`${indent} method: "${route.method}",`)
|
|
440
|
-
lines.push(`${indent} },`)
|
|
441
|
-
if (bodyType) {
|
|
442
|
-
lines.push(`${indent} body,`)
|
|
443
|
-
}
|
|
444
|
-
if (shouldResolveApiResponse) {
|
|
445
|
-
lines.push(`${indent} }))`)
|
|
446
|
-
} else {
|
|
447
|
-
lines.push(`${indent} }) as ${returnType}`)
|
|
448
|
-
}
|
|
449
|
-
lines.push(`${indent}}`)
|
|
450
|
-
|
|
451
|
-
return lines
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function pathTypeForRoute(route: ProcessedRouteMeta): string {
|
|
455
|
-
return route.requestSchema?.path ? `${route.typeBase}PathParams` : 'any'
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function pathParameterTypeForRoute(route: ProcessedRouteMeta): string {
|
|
459
|
-
let pathParamType = pathTypeForRoute(route)
|
|
460
|
-
if (route.pathParams.length === 1) {
|
|
461
|
-
const singleParamType = route.requestSchema?.path
|
|
462
|
-
? `SinglePathParam<${route.typeBase}PathParams, "${route.pathParams[0]}">`
|
|
463
|
-
: 'any'
|
|
464
|
-
pathParamType = `${pathParamType} | ${singleParamType}`
|
|
465
|
-
}
|
|
466
|
-
return pathParamType
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
type RouteRequestComponent = {
|
|
470
|
-
name: 'path' | 'query' | 'body'
|
|
471
|
-
type: string
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function routeRequestComponents(route: ProcessedRouteMeta): RouteRequestComponent[] {
|
|
475
|
-
const components: RouteRequestComponent[] = []
|
|
476
|
-
if (route.pathParams.length > 0) {
|
|
477
|
-
components.push({ name: 'path', type: pathParameterTypeForRoute(route) })
|
|
478
|
-
}
|
|
479
|
-
if (route.requestSchema?.query) {
|
|
480
|
-
components.push({ name: 'query', type: `${route.typeBase}Query` })
|
|
481
|
-
}
|
|
482
|
-
if (route.requestSchema?.body !== undefined) {
|
|
483
|
-
components.push({ name: 'body', type: `${route.typeBase}Request` })
|
|
484
|
-
}
|
|
485
|
-
return components
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function emitClass(node: RouteNode, parts: string[], options: BuildOptions, lines: string[]): void {
|
|
489
|
-
const className = classNameForParts(parts, options.clientName)
|
|
490
|
-
const exportKeyword = parts.length === 0 ? 'export ' : ''
|
|
491
|
-
const childNames = Array.from(node.children.keys())
|
|
492
|
-
|
|
493
|
-
lines.push(`${exportKeyword}class ${className} {`)
|
|
494
|
-
if (parts.length === 0) {
|
|
495
|
-
lines.push(` private readonly transport: ClientTransport`)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
for (const childName of childNames) {
|
|
499
|
-
const childClass = classNameForParts([...parts, childName], options.clientName)
|
|
500
|
-
lines.push(` private _${childName}?: ${childClass}`)
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (parts.length === 0 || childNames.length > 0) {
|
|
504
|
-
lines.push(``)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (parts.length === 0) {
|
|
508
|
-
lines.push(` constructor(transport?: ClientTransport) {`)
|
|
509
|
-
lines.push(` this.transport = transport ?? new FetchTransport()`)
|
|
510
|
-
lines.push(` }`)
|
|
511
|
-
} else {
|
|
512
|
-
lines.push(` constructor(private readonly transport: ClientTransport) {}`)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (node.children.size > 0) {
|
|
516
|
-
lines.push(``)
|
|
517
|
-
childNames.forEach((childName, idx) => {
|
|
518
|
-
const childClass = classNameForParts([...parts, childName], options.clientName)
|
|
519
|
-
lines.push(` get ${childName}(): ${childClass} {`)
|
|
520
|
-
lines.push(` return (this._${childName} ??= new ${childClass}(this.transport))`)
|
|
521
|
-
lines.push(` }`)
|
|
522
|
-
if (idx !== childNames.length - 1 || node.routes.length > 0) {
|
|
523
|
-
lines.push(``)
|
|
524
|
-
}
|
|
525
|
-
})
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (node.routes.length > 0) {
|
|
529
|
-
node.routes.forEach((route, idx) => {
|
|
530
|
-
const methodLines = buildMethodLines(route, options, ' ')
|
|
531
|
-
for (const line of methodLines) lines.push(line)
|
|
532
|
-
if (idx !== node.routes.length - 1) lines.push(``)
|
|
533
|
-
})
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
lines.push(`}`)
|
|
537
|
-
lines.push(``)
|
|
538
|
-
|
|
539
|
-
for (const [childName, childNode] of node.children) {
|
|
540
|
-
emitClass(childNode, [...parts, childName], options, lines)
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function emitGeneratedRouteTypeSources(
|
|
545
|
-
generatedTypes: GeneratedRouteTypes[],
|
|
546
|
-
lines: string[],
|
|
547
|
-
): void {
|
|
548
|
-
for (const generated of generatedTypes) {
|
|
549
|
-
if (generated.pathTypeSource && !isUnknown(generated.pathTypeSource))
|
|
550
|
-
lines.push(generated.pathTypeSource, ``)
|
|
551
|
-
if (generated.queryTypeSource && !isUnknown(generated.queryTypeSource))
|
|
552
|
-
lines.push(generated.queryTypeSource, ``)
|
|
553
|
-
if (generated.requestTypeSource && !isUnknown(generated.requestTypeSource))
|
|
554
|
-
lines.push(generated.requestTypeSource, ``)
|
|
555
|
-
for (const responseTypeSource of generated.responseTypeSources) {
|
|
556
|
-
if (!isUnknown(responseTypeSource)) lines.push(responseTypeSource, ``)
|
|
557
|
-
}
|
|
558
|
-
if (generated.responseTypesByStatusSource)
|
|
559
|
-
lines.push(generated.responseTypesByStatusSource, ``)
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function routeSuccessBodyType(route: ProcessedRouteMeta): string {
|
|
564
|
-
return unionType(responseStatusAliases(route, isSuccessfulStatus), 'unknown')
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function routeProblemBodyType(route: ProcessedRouteMeta): string {
|
|
568
|
-
return unionType(
|
|
569
|
-
responseStatusAliases(route, (status) => !isSuccessfulStatus(status)),
|
|
570
|
-
'unknown',
|
|
571
|
-
)
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function isQueryRoute(route: ProcessedRouteMeta): boolean {
|
|
575
|
-
return route.method === HttpMethod.GET || route.method === HttpMethod.HEAD
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function emitTsQueryRouteTypeAliases(generatedTypes: GeneratedRouteTypes[], lines: string[]): void {
|
|
579
|
-
for (const { route } of generatedTypes) {
|
|
580
|
-
const successBodyType = routeSuccessBodyType(route)
|
|
581
|
-
const problemBodyType = routeProblemBodyType(route)
|
|
582
|
-
lines.push(
|
|
583
|
-
`export type ${route.typeBase}Data = RoutekitProblemSuccessData<${successBodyType}>`,
|
|
584
|
-
)
|
|
585
|
-
lines.push(`export type ${route.typeBase}Problem = ${problemBodyType}`)
|
|
586
|
-
lines.push(
|
|
587
|
-
`export type ${route.typeBase}Error = RoutekitProblemError<${route.typeBase}Problem>`,
|
|
588
|
-
)
|
|
589
|
-
lines.push(``)
|
|
590
|
-
|
|
591
|
-
const components = routeRequestComponents(route)
|
|
592
|
-
if (!isQueryRoute(route) && components.length > 1) {
|
|
593
|
-
lines.push(`export interface ${route.typeBase}Variables {`)
|
|
594
|
-
for (const component of components) {
|
|
595
|
-
lines.push(` ${component.name}: ${component.type}`)
|
|
596
|
-
}
|
|
597
|
-
lines.push(`}`)
|
|
598
|
-
lines.push(``)
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function buildTransportRequestLines(
|
|
604
|
-
route: ProcessedRouteMeta,
|
|
605
|
-
indent: string,
|
|
606
|
-
pathVar: string,
|
|
607
|
-
queryVar: string | undefined,
|
|
608
|
-
bodyVar: string | undefined,
|
|
609
|
-
): string[] {
|
|
610
|
-
const lines: string[] = []
|
|
611
|
-
const urlExpr = patternToUrlTemplate(route.path, pathVar)
|
|
612
|
-
const finalUrlExpr = queryVar ? `withQuery(${urlExpr}, ${queryVar})` : urlExpr
|
|
613
|
-
|
|
614
|
-
lines.push(`${indent}transport.request({`)
|
|
615
|
-
lines.push(`${indent} url: ${finalUrlExpr},`)
|
|
616
|
-
lines.push(`${indent} init: {`)
|
|
617
|
-
lines.push(`${indent} method: "${route.method}",`)
|
|
618
|
-
lines.push(`${indent} },`)
|
|
619
|
-
if (bodyVar) {
|
|
620
|
-
lines.push(`${indent} body: ${bodyVar},`)
|
|
621
|
-
}
|
|
622
|
-
lines.push(`${indent}})`)
|
|
623
|
-
return lines
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function pushSinglePathParamNormalization(
|
|
627
|
-
route: ProcessedRouteMeta,
|
|
628
|
-
lines: string[],
|
|
629
|
-
indent: string,
|
|
630
|
-
): string {
|
|
631
|
-
if (route.pathParams.length !== 1) return 'path'
|
|
632
|
-
|
|
633
|
-
lines.push(
|
|
634
|
-
`${indent}const _path = typeof path === 'object' && path !== null && !Array.isArray(path) ? path : { ${route.pathParams[0]}: path } as any`,
|
|
635
|
-
)
|
|
636
|
-
return '_path'
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function tsQueryKey(route: ProcessedRouteMeta, components: string[]): string {
|
|
640
|
-
return [
|
|
641
|
-
JSON.stringify('routekit'),
|
|
642
|
-
JSON.stringify(route.method),
|
|
643
|
-
JSON.stringify(route.path),
|
|
644
|
-
...components,
|
|
645
|
-
].join(', ')
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function emitQueryHelper(route: ProcessedRouteMeta, lines: string[], indent: string): void {
|
|
649
|
-
const components = routeRequestComponents(route)
|
|
650
|
-
const params = components.map((component) => `${component.name}: ${component.type}`).join(', ')
|
|
651
|
-
const methodName = route.method.toLowerCase()
|
|
652
|
-
const successBodyType = routeSuccessBodyType(route)
|
|
653
|
-
const queryKeyComponents: string[] = []
|
|
654
|
-
|
|
655
|
-
lines.push(`${indent}${methodName}: (${params}) => {`)
|
|
656
|
-
const pathVar = pushSinglePathParamNormalization(route, lines, `${indent} `)
|
|
657
|
-
for (const component of components) {
|
|
658
|
-
queryKeyComponents.push(component.name === 'path' ? pathVar : component.name)
|
|
659
|
-
}
|
|
660
|
-
lines.push(`${indent} return queryOptions<${route.typeBase}Data, ${route.typeBase}Error>({`)
|
|
661
|
-
lines.push(`${indent} queryKey: [${tsQueryKey(route, queryKeyComponents)}] as const,`)
|
|
662
|
-
lines.push(`${indent} queryFn: (): Promise<${route.typeBase}Data> => {`)
|
|
663
|
-
lines.push(
|
|
664
|
-
`${indent} return resolveRoutekitProblemData<${successBodyType}, ${route.typeBase}Problem>(`,
|
|
665
|
-
)
|
|
666
|
-
for (const requestLine of buildTransportRequestLines(
|
|
667
|
-
route,
|
|
668
|
-
`${indent} `,
|
|
669
|
-
pathVar,
|
|
670
|
-
route.requestSchema?.query ? 'query' : undefined,
|
|
671
|
-
route.requestSchema?.body !== undefined ? 'body' : undefined,
|
|
672
|
-
)) {
|
|
673
|
-
lines.push(requestLine)
|
|
674
|
-
}
|
|
675
|
-
lines.push(`${indent} )`)
|
|
676
|
-
lines.push(`${indent} },`)
|
|
677
|
-
lines.push(`${indent} })`)
|
|
678
|
-
lines.push(`${indent}},`)
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function emitMutationHelper(route: ProcessedRouteMeta, lines: string[], indent: string): void {
|
|
682
|
-
const components = routeRequestComponents(route)
|
|
683
|
-
const methodName = route.method.toLowerCase()
|
|
684
|
-
const successBodyType = routeSuccessBodyType(route)
|
|
685
|
-
const variablesType =
|
|
686
|
-
components.length === 0
|
|
687
|
-
? 'void'
|
|
688
|
-
: components.length === 1
|
|
689
|
-
? components[0]!.type
|
|
690
|
-
: `${route.typeBase}Variables`
|
|
691
|
-
|
|
692
|
-
lines.push(`${indent}${methodName}: () => {`)
|
|
693
|
-
lines.push(
|
|
694
|
-
`${indent} return mutationOptions<${route.typeBase}Data, ${route.typeBase}Error, ${variablesType}>({`,
|
|
695
|
-
)
|
|
696
|
-
lines.push(`${indent} mutationKey: [${tsQueryKey(route, [])}] as const,`)
|
|
697
|
-
|
|
698
|
-
let mutationParam = ''
|
|
699
|
-
if (components.length === 1) {
|
|
700
|
-
const component = components[0]!
|
|
701
|
-
mutationParam = `${component.name}: ${component.type}`
|
|
702
|
-
} else if (components.length > 1) {
|
|
703
|
-
mutationParam = `variables: ${route.typeBase}Variables`
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
lines.push(
|
|
707
|
-
`${indent} mutationFn: (${mutationParam}): Promise<${route.typeBase}Data> => {`,
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
if (components.length > 1) {
|
|
711
|
-
for (const component of components) {
|
|
712
|
-
lines.push(`${indent} const ${component.name} = variables.${component.name}`)
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const pathVar =
|
|
717
|
-
components.some((component) => component.name === 'path') && route.pathParams.length === 1
|
|
718
|
-
? pushSinglePathParamNormalization(route, lines, `${indent} `)
|
|
719
|
-
: 'path'
|
|
720
|
-
|
|
721
|
-
lines.push(
|
|
722
|
-
`${indent} return resolveRoutekitProblemData<${successBodyType}, ${route.typeBase}Problem>(`,
|
|
723
|
-
)
|
|
724
|
-
for (const requestLine of buildTransportRequestLines(
|
|
725
|
-
route,
|
|
726
|
-
`${indent} `,
|
|
727
|
-
pathVar,
|
|
728
|
-
route.requestSchema?.query ? 'query' : undefined,
|
|
729
|
-
route.requestSchema?.body !== undefined ? 'body' : undefined,
|
|
730
|
-
)) {
|
|
731
|
-
lines.push(requestLine)
|
|
732
|
-
}
|
|
733
|
-
lines.push(`${indent} )`)
|
|
734
|
-
lines.push(`${indent} },`)
|
|
735
|
-
lines.push(`${indent} })`)
|
|
736
|
-
lines.push(`${indent}},`)
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function emitTsQueryNodeProperties(node: RouteNode, lines: string[], indent: string): void {
|
|
740
|
-
for (const [childName, childNode] of node.children) {
|
|
741
|
-
lines.push(`${indent}${childName}: {`)
|
|
742
|
-
emitTsQueryNodeProperties(childNode, lines, `${indent} `)
|
|
743
|
-
lines.push(`${indent}},`)
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
for (const route of node.routes) {
|
|
747
|
-
if (isQueryRoute(route)) {
|
|
748
|
-
emitQueryHelper(route, lines, indent)
|
|
749
|
-
} else {
|
|
750
|
-
emitMutationHelper(route, lines, indent)
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
async function buildApiClientSource(
|
|
756
|
-
routes: ExtractedRouteMeta[],
|
|
757
|
-
options: BuildOptions,
|
|
758
|
-
): Promise<string> {
|
|
759
|
-
const processedRoutes: ProcessedRouteMeta[] = routes.map((route) => ({
|
|
760
|
-
...route,
|
|
761
|
-
typeBase: routeTypeBaseName(route),
|
|
762
|
-
pathParams: getPathParamNames(route.path),
|
|
763
|
-
}))
|
|
764
|
-
const needsSinglePathHelper = processedRoutes.some((route) => route.pathParams.length === 1)
|
|
765
|
-
const needsQueryHelper = processedRoutes.some((route) => route.requestSchema?.query)
|
|
766
|
-
const needsResponseByStatusHelper = processedRoutes.some(
|
|
767
|
-
(route) => route.responseBodySchemas && Object.keys(route.responseBodySchemas).length > 0,
|
|
768
|
-
)
|
|
769
|
-
const needsDefaultResponsePromise = processedRoutes.some(
|
|
770
|
-
(route) =>
|
|
771
|
-
!route.responseBodySchemas || Object.keys(route.responseBodySchemas).length === 0,
|
|
772
|
-
)
|
|
773
|
-
const generatedTypes = await Promise.all(processedRoutes.map(generateRouteTypes))
|
|
774
|
-
|
|
775
|
-
const lines: string[] = []
|
|
776
|
-
lines.push(`// Do not modify this file. It was auto-generated with the following command:`)
|
|
777
|
-
lines.push(`// $ ${options.commandText}`)
|
|
778
|
-
lines.push(``)
|
|
779
|
-
|
|
780
|
-
for (const importType of options.importTypes) {
|
|
781
|
-
lines.push(`import type { ${importType.names.join(', ')} } from '${importType.module}'`)
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const clientImports = [
|
|
785
|
-
'FetchTransport',
|
|
786
|
-
options.responseType === DEFAULT_RESPONSE_TYPE && needsDefaultResponsePromise
|
|
787
|
-
? 'resolveApiResponse'
|
|
788
|
-
: undefined,
|
|
789
|
-
options.responseType === DEFAULT_RESPONSE_TYPE && needsResponseByStatusHelper
|
|
790
|
-
? 'resolveApiResponseByStatus'
|
|
791
|
-
: undefined,
|
|
792
|
-
needsQueryHelper ? 'withQuery' : undefined,
|
|
793
|
-
'type ClientTransport',
|
|
794
|
-
options.responseType === DEFAULT_RESPONSE_TYPE && needsResponseByStatusHelper
|
|
795
|
-
? 'type ApiResponseByStatusPromise'
|
|
796
|
-
: undefined,
|
|
797
|
-
options.responseType === DEFAULT_RESPONSE_TYPE && needsDefaultResponsePromise
|
|
798
|
-
? `type ${options.responseType}`
|
|
799
|
-
: undefined,
|
|
800
|
-
needsSinglePathHelper ? 'type SinglePathParam' : undefined,
|
|
801
|
-
].filter(Boolean)
|
|
802
|
-
lines.push(`import { ${clientImports.join(', ')} } from '@mpen/routekit/client'`)
|
|
803
|
-
|
|
804
|
-
if (options.importTypes.length > 0) {
|
|
805
|
-
lines.push(``)
|
|
806
|
-
}
|
|
807
|
-
lines.push(``)
|
|
808
|
-
|
|
809
|
-
lines.push(``)
|
|
810
|
-
emitGeneratedRouteTypeSources(generatedTypes, lines)
|
|
811
|
-
|
|
812
|
-
const tree = buildRouteTree(processedRoutes)
|
|
813
|
-
emitClass(tree, [], options, lines)
|
|
814
|
-
|
|
815
|
-
return lines.join('\n')
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
async function buildTsQueryRkProblemSource(
|
|
819
|
-
routes: ExtractedRouteMeta[],
|
|
820
|
-
options: Pick<BuildOptions, 'importTypes' | 'commandText'>,
|
|
821
|
-
): Promise<string> {
|
|
822
|
-
const processedRoutes: ProcessedRouteMeta[] = routes.map((route) => ({
|
|
823
|
-
...route,
|
|
824
|
-
typeBase: routeTypeBaseName(route),
|
|
825
|
-
pathParams: getPathParamNames(route.path),
|
|
826
|
-
}))
|
|
827
|
-
const needsSinglePathHelper = processedRoutes.some((route) => route.pathParams.length === 1)
|
|
828
|
-
const needsQueryHelper = processedRoutes.some((route) => route.requestSchema?.query)
|
|
829
|
-
const generatedTypes = await Promise.all(processedRoutes.map(generateRouteTypes))
|
|
830
|
-
|
|
831
|
-
const lines: string[] = []
|
|
832
|
-
lines.push(`// Do not modify this file. It was auto-generated with the following command:`)
|
|
833
|
-
lines.push(`// $ ${options.commandText}`)
|
|
834
|
-
lines.push(``)
|
|
835
|
-
|
|
836
|
-
for (const importType of options.importTypes) {
|
|
837
|
-
lines.push(`import type { ${importType.names.join(', ')} } from '${importType.module}'`)
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
lines.push(`import { mutationOptions, queryOptions } from '@tanstack/react-query'`)
|
|
841
|
-
|
|
842
|
-
const clientImports = [
|
|
843
|
-
'FetchTransport',
|
|
844
|
-
'resolveRoutekitProblemData',
|
|
845
|
-
needsQueryHelper ? 'withQuery' : undefined,
|
|
846
|
-
'type ClientTransport',
|
|
847
|
-
'type RoutekitProblemError',
|
|
848
|
-
'type RoutekitProblemSuccessData',
|
|
849
|
-
needsSinglePathHelper ? 'type SinglePathParam' : undefined,
|
|
850
|
-
].filter(Boolean)
|
|
851
|
-
lines.push(`import { ${clientImports.join(', ')} } from '@mpen/routekit/client'`)
|
|
852
|
-
lines.push(``)
|
|
853
|
-
|
|
854
|
-
emitGeneratedRouteTypeSources(generatedTypes, lines)
|
|
855
|
-
emitTsQueryRouteTypeAliases(generatedTypes, lines)
|
|
856
|
-
|
|
857
|
-
const tree = buildRouteTree(processedRoutes)
|
|
858
|
-
lines.push(
|
|
859
|
-
`export function createApiQueryHelpers(transport: ClientTransport = new FetchTransport()) {`,
|
|
860
|
-
)
|
|
861
|
-
lines.push(` return {`)
|
|
862
|
-
emitTsQueryNodeProperties(tree, lines, ` `)
|
|
863
|
-
lines.push(` }`)
|
|
864
|
-
lines.push(`}`)
|
|
865
|
-
lines.push(``)
|
|
866
|
-
|
|
867
|
-
return lines.join('\n')
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
export async function main() {
|
|
871
|
-
const { positionals, values } = parseArgs({
|
|
872
|
-
allowPositionals: true,
|
|
873
|
-
strict: true,
|
|
874
|
-
options: {
|
|
875
|
-
output: {
|
|
876
|
-
type: 'string',
|
|
877
|
-
short: 'o',
|
|
878
|
-
},
|
|
879
|
-
write: {
|
|
880
|
-
type: 'boolean',
|
|
881
|
-
short: 'w',
|
|
882
|
-
},
|
|
883
|
-
pretty: {
|
|
884
|
-
type: 'boolean',
|
|
885
|
-
short: 'p',
|
|
886
|
-
},
|
|
887
|
-
format: {
|
|
888
|
-
type: 'string',
|
|
889
|
-
short: 'f',
|
|
890
|
-
},
|
|
891
|
-
'client-name': {
|
|
892
|
-
type: 'string',
|
|
893
|
-
},
|
|
894
|
-
'import-type': {
|
|
895
|
-
type: 'string',
|
|
896
|
-
multiple: true,
|
|
897
|
-
},
|
|
898
|
-
'response-type': {
|
|
899
|
-
type: 'string',
|
|
900
|
-
},
|
|
901
|
-
help: {
|
|
902
|
-
type: 'boolean',
|
|
903
|
-
},
|
|
904
|
-
},
|
|
905
|
-
})
|
|
906
|
-
|
|
907
|
-
if (values.help) {
|
|
908
|
-
printHelp()
|
|
909
|
-
return
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const [routerPathArg] = positionals
|
|
913
|
-
if (!routerPathArg) {
|
|
914
|
-
printHelp()
|
|
915
|
-
process.exit(1)
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const format = parseOutputFormat(
|
|
919
|
-
(values as Record<string, string | string[] | undefined>).format as string | undefined,
|
|
920
|
-
)
|
|
921
|
-
const clientName = normalizeClientName(
|
|
922
|
-
(values as Record<string, string | string[] | undefined>)['client-name'] as
|
|
923
|
-
| string
|
|
924
|
-
| undefined,
|
|
925
|
-
)
|
|
926
|
-
const responseType =
|
|
927
|
-
(
|
|
928
|
-
(values as Record<string, string | string[] | undefined>)['response-type'] as
|
|
929
|
-
| string
|
|
930
|
-
| undefined
|
|
931
|
-
)?.trim() || DEFAULT_RESPONSE_TYPE
|
|
932
|
-
const importTypes =
|
|
933
|
-
(
|
|
934
|
-
(values as Record<string, string | string[] | undefined>)['import-type'] as
|
|
935
|
-
| string[]
|
|
936
|
-
| undefined
|
|
937
|
-
)?.map(parseImportTypeOption) ?? []
|
|
938
|
-
|
|
939
|
-
const routerPath = path.resolve(routerPathArg)
|
|
940
|
-
let outputPath: string | undefined
|
|
941
|
-
if (values.output) {
|
|
942
|
-
outputPath = path.resolve(values.output as string)
|
|
943
|
-
} else if (values.write) {
|
|
944
|
-
outputPath = path.join(
|
|
945
|
-
path.dirname(routerPath),
|
|
946
|
-
`${path.basename(routerPath, path.extname(routerPath))}.gen.ts`,
|
|
947
|
-
)
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const routes = extractRoutes(await loadRuntimeRoutes(routerPath))
|
|
951
|
-
const rawArgs = process.argv.slice(1)
|
|
952
|
-
if (rawArgs[0] && path.isAbsolute(rawArgs[0])) {
|
|
953
|
-
rawArgs[0] = path.relative(process.cwd(), rawArgs[0]).replace(/\\/g, '/')
|
|
954
|
-
}
|
|
955
|
-
const commandText = ['bun', ...rawArgs.map((arg) => $.escape(arg))].join(' ')
|
|
956
|
-
|
|
957
|
-
let client =
|
|
958
|
-
format === 'rk-api-client'
|
|
959
|
-
? await buildApiClientSource(routes, {
|
|
960
|
-
clientName,
|
|
961
|
-
responseType,
|
|
962
|
-
importTypes,
|
|
963
|
-
commandText,
|
|
964
|
-
})
|
|
965
|
-
: await buildTsQueryRkProblemSource(routes, {
|
|
966
|
-
importTypes,
|
|
967
|
-
commandText,
|
|
968
|
-
})
|
|
969
|
-
|
|
970
|
-
if (outputPath) {
|
|
971
|
-
if (values.pretty) {
|
|
972
|
-
client = await formatWithPrettier(client, outputPath)
|
|
973
|
-
}
|
|
974
|
-
fs.writeFileSync(outputPath, client, 'utf8')
|
|
975
|
-
console.log(`Wrote API client to ${path.relative(process.cwd(), outputPath)}`)
|
|
976
|
-
} else {
|
|
977
|
-
console.log(client)
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (import.meta.main) {
|
|
982
|
-
main().catch((err) => {
|
|
983
|
-
console.error(err)
|
|
984
|
-
process.exit(1)
|
|
985
|
-
})
|
|
986
|
-
}
|