galbe 0.15.5 → 0.16.0
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/README.md +3 -0
- package/bin/commands/build.ts +30 -19
- package/bin/commands/dev.ts +53 -5
- package/bin/commands/generate/cli/index.ts +4 -1
- package/bin/commands/generate/client.ts +61 -30
- package/bin/commands/generate/code/openapi.parser.ts +440 -163
- package/bin/commands/generate/code/route-merge.ts +26 -21
- package/bin/commands/generate/code.ts +15 -1
- package/bin/commands/generate/model.ts +4 -1
- package/bin/commands/generate/spec.ts +3 -1
- package/bin/res/client.runtime.ts +5 -0
- package/bin/util.ts +36 -90
- package/package.json +34 -9
- package/src/cookies.ts +29 -8
- package/src/extras/spec/openapi.serializer.ts +287 -99
- package/src/extras.ts +1 -1
- package/src/index.ts +378 -73
- package/src/middlewares/_auth.ts +178 -0
- package/src/middlewares/apiKey.ts +139 -0
- package/src/middlewares/basicAuth.ts +151 -0
- package/src/middlewares/bearer.ts +136 -0
- package/src/middlewares/jwt.ts +455 -0
- package/src/middlewares/logger.ts +120 -0
- package/src/middlewares/rateLimit.ts +153 -0
- package/src/middlewares/requestId.ts +94 -0
- package/src/middlewares/timing.ts +86 -0
- package/src/middlewares.ts +53 -0
- package/src/parser.ts +279 -133
- package/src/router.ts +74 -51
- package/src/routes.ts +220 -136
- package/src/schema.ts +123 -31
- package/src/server.ts +130 -70
- package/src/types.ts +368 -92
- package/src/util.ts +271 -5
- package/src/validator.compile.ts +343 -0
- package/src/validator.ts +64 -18
- package/bin/res/client.template.ts +0 -200
- package/scripts/release.ts +0 -196
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
Galbe is a fast, lightweight and highly customizable JavaScript web framework based on [Bun](https://bun.sh).
|
|
11
11
|
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> Galbe is Bun-only (Bun v1.2.21+): it relies on Bun-native APIs and ships TypeScript sources directly. Node.js and Deno are not supported.
|
|
14
|
+
|
|
12
15
|
> [!IMPORTANT]
|
|
13
16
|
> Galbe is currently under active development and not guaranteed to be stable. Future releases may potentially introduce breaking changes.
|
|
14
17
|
|
package/bin/commands/build.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { mkdir, rm } from 'fs/promises'
|
|
|
7
7
|
|
|
8
8
|
import { CWD, fmtVal, silentExec } from '../util'
|
|
9
9
|
import { Galbe } from '../../src'
|
|
10
|
-
import { defineRoutes
|
|
10
|
+
import { defineRoutes } from '../../src/routes'
|
|
11
11
|
import type { BuildConfig } from 'bun'
|
|
12
12
|
import { cpSync, existsSync } from 'fs'
|
|
13
13
|
import { softMerge } from '../../src/util'
|
|
@@ -16,45 +16,54 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string, ou
|
|
|
16
16
|
const buildPath = resolve(tmpdir(), buildId)
|
|
17
17
|
const indexDir = dirname(indexPath)
|
|
18
18
|
|
|
19
|
+
// Locate galbe's own modules from the CLI's install location rather than
|
|
20
|
+
// assuming `<app>/node_modules/galbe/src`, which breaks under pnpm,
|
|
21
|
+
// hoisted or global CLI installs.
|
|
22
|
+
const galbeUtilPath = resolve(import.meta.dir, '..', '..', 'src', 'util')
|
|
23
|
+
const galbeIndexPath = resolve(import.meta.dir, '..', '..', 'src', 'index')
|
|
24
|
+
|
|
19
25
|
let configPath = ''
|
|
20
26
|
if (existsSync(`${indexDir}/galbe.config.ts`)) configPath = `${indexDir}/galbe.config.ts`
|
|
21
27
|
else if (existsSync(`${indexDir}/galbe.config.js`)) configPath = `${indexDir}/galbe.config.js`
|
|
22
28
|
|
|
23
|
-
const routes = new Map<string, { filepath: string; static?: { path: string; root: string } }>()
|
|
24
29
|
let errors: any[] = []
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await proxy.init()
|
|
30
|
+
const { routeFiles, middlewareFiles } = await defineRoutes(
|
|
31
|
+
{ routes: g?.config?.routes, middleware: g?.config?.middleware },
|
|
32
|
+
g,
|
|
33
|
+
({ type, error }) => {
|
|
34
|
+
if (type === 'error') errors.push(error)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
// plugin initialization
|
|
38
|
+
await g.init()
|
|
35
39
|
|
|
36
40
|
if (errors.length) throw errors
|
|
37
41
|
|
|
38
42
|
// Copy static assets next to the bundle so the runtime can resolve them
|
|
39
43
|
// via static-${BUILD_ID}/<target> at request time.
|
|
40
|
-
for (const { target } of
|
|
44
|
+
for (const { target } of g.staticTargets) {
|
|
41
45
|
cpSync(target, `${outPath}/static-${buildId}/${target}`, { recursive: true, dereference: true })
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
await mkdir(buildPath, { recursive: true })
|
|
45
49
|
|
|
50
|
+
const usesGroup = routeFiles.some(r => r.prefix)
|
|
46
51
|
let buildIndex =
|
|
47
52
|
`import galbe from '${relative(buildPath, indexPath)}';\n` +
|
|
53
|
+
(usesGroup ? `import {GalbeGroup} from '${relative(buildPath, galbeIndexPath)}';\n` : '') +
|
|
48
54
|
(configPath ? `import config from '${relative(buildPath, configPath)}';\n` : '') +
|
|
49
|
-
(configPath
|
|
50
|
-
? `import {softMerge} from '${relative(buildPath, `${indexDir}/node_modules/galbe/src/util`)}';\n`
|
|
51
|
-
: '') +
|
|
55
|
+
(configPath ? `import {softMerge} from '${relative(buildPath, galbeUtilPath)}';\n` : '') +
|
|
52
56
|
(configPath ? `let conf = galbe.config;\ngalbe.config = softMerge(config, conf)\n` : '') +
|
|
53
|
-
`${
|
|
57
|
+
`${middlewareFiles.map((m, idx) => `import mw_${idx} from '${relative(buildPath, m.file)}'`).join(';\n')}\n` +
|
|
58
|
+
`${routeFiles.map((r, idx) => `import _${idx} from '${relative(buildPath, r.file)}'`).join(';\n')}\n` +
|
|
54
59
|
`Bun.env.BUN_ENV = 'production';\n` +
|
|
55
60
|
`Bun.env.GALBE_BUILD = '${buildId}';\n` +
|
|
56
61
|
`galbe.meta = ${JSON.stringify(g.meta)};\n` +
|
|
57
|
-
|
|
62
|
+
`galbe.metaMiddleware = ${JSON.stringify(g.metaMiddleware)};\n` +
|
|
63
|
+
`${middlewareFiles.map((m, idx) => `galbe.middleware(${JSON.stringify(m.scope)}, mw_${idx})`).join(';\n')};\n` +
|
|
64
|
+
`${routeFiles
|
|
65
|
+
.map((r, idx) => `_${idx}(${r.prefix ? `new GalbeGroup(galbe, ${JSON.stringify(r.prefix)})` : 'galbe'})`)
|
|
66
|
+
.join(';\n')};\n` +
|
|
58
67
|
`galbe.listen();\n` +
|
|
59
68
|
`process.on('SIGTERM', () => galbe.stop());\n` +
|
|
60
69
|
`process.on('SIGINT', () => galbe.stop());\n`
|
|
@@ -114,7 +123,9 @@ export default (cmd: Command) => {
|
|
|
114
123
|
buildIndex = await createBuildIndex(index, g, buildID, outPath)
|
|
115
124
|
} catch (errors) {
|
|
116
125
|
console.log(`\nerror: build errors`)
|
|
117
|
-
|
|
126
|
+
// route-file errors come as an array; anything else (a boot error, a
|
|
127
|
+
// failed import) throws a single value — don't lose it
|
|
128
|
+
for (let error of [errors].flat()) console.log(error)
|
|
118
129
|
return process.exit(1)
|
|
119
130
|
}
|
|
120
131
|
if (!buildIndex) {
|
package/bin/commands/dev.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { $ } from 'bun'
|
|
|
2
2
|
import { Command, Option } from 'commander'
|
|
3
3
|
import { resolve, dirname } from 'path'
|
|
4
4
|
|
|
5
|
-
import { CWD, fmtInterval, fmtVal, instanciateRoutes, watchDir } from '../util'
|
|
5
|
+
import { CWD, fmtInterval, fmtVal, instanciateRoutes, resolveReloadStrategy, watchDir } from '../util'
|
|
6
6
|
import { Galbe } from '../../src'
|
|
7
7
|
import { softMerge } from '../../src/util'
|
|
8
8
|
import { existsSync } from 'fs'
|
|
@@ -42,22 +42,70 @@ export default (cmd: Command) => {
|
|
|
42
42
|
|
|
43
43
|
if (!Bun.env.BUN_ENV) Bun.env.BUN_ENV = 'development'
|
|
44
44
|
|
|
45
|
+
if (!!watch_dir && resolveReloadStrategy() === 'respawn') {
|
|
46
|
+
// Reload by respawning the app process: a syntax error in an edited file
|
|
47
|
+
// kills the child, not the watcher — the next save reloads.
|
|
48
|
+
const spawnApp = () =>
|
|
49
|
+
Bun.spawn([process.execPath, process.argv[1], 'dev', index, '-p', `${port || DEFAULT_PORT}`], {
|
|
50
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
51
|
+
cwd: CWD,
|
|
52
|
+
})
|
|
53
|
+
const killChild = () => {
|
|
54
|
+
try {
|
|
55
|
+
child?.kill()
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
process.on('SIGINT', () => {
|
|
59
|
+
killChild()
|
|
60
|
+
process.exit(0)
|
|
61
|
+
})
|
|
62
|
+
process.on('SIGTERM', () => {
|
|
63
|
+
killChild()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
})
|
|
66
|
+
if (clear) await $`clear`.nothrow()
|
|
67
|
+
let child = spawnApp()
|
|
68
|
+
let reloading: Promise<void> = Promise.resolve()
|
|
69
|
+
await watchDir(
|
|
70
|
+
watch_dir,
|
|
71
|
+
() => {
|
|
72
|
+
reloading = reloading.then(async () => {
|
|
73
|
+
killChild()
|
|
74
|
+
await child.exited
|
|
75
|
+
if (clear) await $`clear`.nothrow()
|
|
76
|
+
child = spawnApp()
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
{ ignore: watchignore ? new RegExp(watchignore) : undefined }
|
|
80
|
+
)
|
|
81
|
+
// The watcher is non-persistent and a pending promise alone does not
|
|
82
|
+
// keep the event loop alive — hold it open with an interval.
|
|
83
|
+
setInterval(() => {}, 2 ** 31 - 1)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
if (!!watch_dir) {
|
|
46
88
|
await watchDir(
|
|
47
89
|
watch_dir,
|
|
48
90
|
async () => {
|
|
49
91
|
g.stop()
|
|
50
|
-
if (clear) await $`clear
|
|
92
|
+
if (clear) await $`clear`.nothrow()
|
|
51
93
|
Loader.registry.clear()
|
|
52
|
-
|
|
94
|
+
try {
|
|
95
|
+
g = (await import(indexPath)).default
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('\x1b[0;31mReload failed:\x1b[0m', e)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
g.config = softMerge(galbeConfig, g.config)
|
|
53
101
|
await instanciateRoutes(g)
|
|
54
102
|
await g.listen(port)
|
|
55
103
|
},
|
|
56
|
-
{ ignore: watchignore ? new RegExp(watchignore) :
|
|
104
|
+
{ ignore: watchignore ? new RegExp(watchignore) : undefined }
|
|
57
105
|
)
|
|
58
106
|
}
|
|
59
107
|
|
|
60
|
-
if (!!watch_dir && clear) await $`clear
|
|
108
|
+
if (!!watch_dir && clear) await $`clear`.nothrow()
|
|
61
109
|
g = (await import(indexPath)).default
|
|
62
110
|
let conf = g.config
|
|
63
111
|
g.config = softMerge(galbeConfig, conf)
|
|
@@ -63,8 +63,11 @@ export default (cmd: Command) => {
|
|
|
63
63
|
|
|
64
64
|
let commands: GalbeCLICommand[] = []
|
|
65
65
|
|
|
66
|
+
// meta keys are relative to basePath; route paths carry it
|
|
67
|
+
const prefix = g.router.prefix || ''
|
|
66
68
|
walkRoutes(g.router.routes, r => {
|
|
67
|
-
|
|
69
|
+
const rPath = prefix && r.path.startsWith(prefix) ? r.path.slice(prefix.length) || '/' : r.path
|
|
70
|
+
let meta = metaRoutes?.[rPath]?.[r.method]
|
|
68
71
|
let [_, summary, description] = meta?.head?.match(/^([^\n]*)\n\n(.*)/) || []
|
|
69
72
|
if (!summary) description = meta?.head
|
|
70
73
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Option } from 'commander'
|
|
2
2
|
import { resolve } from 'path'
|
|
3
|
-
import { transformSync } from '@swc/
|
|
3
|
+
import { transformSync } from '@swc/wasm'
|
|
4
4
|
import { CWD, fmtList, instanciateRoutes, silentExec } from '../../util'
|
|
5
5
|
import { Galbe, type GalbeClientRoute, type GalbeClientOptions } from '../../../src'
|
|
6
6
|
import { walkRoutes } from '../../../src/util'
|
|
@@ -32,12 +32,7 @@ const mimeToShort = (mime: string) => MIME_SHORT[mime] ?? (mime.startsWith('text
|
|
|
32
32
|
// ─── Operaion-id derivation ───────────────────────────────────────────────────
|
|
33
33
|
|
|
34
34
|
const deriveOperationId = (method: string, path: string): string =>
|
|
35
|
-
`${method}-${path
|
|
36
|
-
.replace(/\//g, '-')
|
|
37
|
-
.replace(/:/g, '')
|
|
38
|
-
.replace(/^-/, '')
|
|
39
|
-
.replace(/-+/g, '-')
|
|
40
|
-
.replace(/-$/, '')}`
|
|
35
|
+
`${method}-${path.replace(/\//g, '-').replace(/:/g, '').replace(/^-/, '').replace(/-+/g, '-').replace(/-$/, '')}`
|
|
41
36
|
|
|
42
37
|
// ─── Response-entry helpers ───────────────────────────────────────────────────
|
|
43
38
|
|
|
@@ -64,7 +59,10 @@ const responseBodyInfo = (entry: STResponseEntry): BodyInfo => {
|
|
|
64
59
|
for (const [mime, s] of Object.entries(content)) {
|
|
65
60
|
if (!mime.includes('/') || !s) continue
|
|
66
61
|
const schema = s as STSchema
|
|
67
|
-
if (Stream in schema && (schema as any)[Stream]) {
|
|
62
|
+
if (Stream in schema && (schema as any)[Stream]) {
|
|
63
|
+
methods.push('stream')
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
68
66
|
const short = mimeToShort(mime)
|
|
69
67
|
methods.push(short === 'byteArray' ? 'byteArray' : short === 'text' ? 'text' : 'json')
|
|
70
68
|
typeStr = schemaToTypeStr(schema)
|
|
@@ -77,7 +75,9 @@ const responseHeadersType = (entry: STResponseEntry): string => {
|
|
|
77
75
|
? (entry as any).responseHeaders
|
|
78
76
|
: (entry as STResponseContent).responseHeaders
|
|
79
77
|
if (!rh || !Object.keys(rh).length) return 'Headers'
|
|
80
|
-
const keys = Object.keys(rh)
|
|
78
|
+
const keys = Object.keys(rh)
|
|
79
|
+
.map(k => `'${k}'`)
|
|
80
|
+
.join('|')
|
|
81
81
|
return `{get<K extends string>(name:K):K extends ${keys}?string:string|null}&Omit<Headers,'get'>`
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -94,10 +94,32 @@ type _OKStatus = ${[...OKS].join('|')}
|
|
|
94
94
|
type _HttpStatus = ${HTTP_CODES.join('|')}
|
|
95
95
|
`.trim()
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Expand `1XX`…`5XX` range keys into the concrete statuses they cover, so the
|
|
99
|
+
* generated types stay per-status. An exact declaration always wins over the
|
|
100
|
+
* range containing it; `default` is left alone as the fallback arm.
|
|
101
|
+
*/
|
|
102
|
+
const expandRanges = (response: STResponse): Array<[string, STResponseEntry]> => {
|
|
103
|
+
const exact = new Set(Object.keys(response).filter(k => /^\d+$/.test(k)))
|
|
104
|
+
const out: Array<[string, STResponseEntry]> = []
|
|
105
|
+
for (const [key, entry] of Object.entries(response)) {
|
|
106
|
+
if (!entry) continue
|
|
107
|
+
const range = key.match(/^([1-5])XX$/)
|
|
108
|
+
if (!range) {
|
|
109
|
+
out.push([key, entry])
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
const hundred = Number(range[1]) * 100
|
|
113
|
+
for (const code of HTTP_CODES)
|
|
114
|
+
if (code >= hundred && code < hundred + 100 && !exact.has(String(code))) out.push([String(code), entry])
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
// Convert operationId to a safe TypeScript identifier (for type names)
|
|
98
120
|
const safeTypeId = (id: string) => id.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
99
121
|
// Quote a property key if it is not a valid bare identifier
|
|
100
|
-
const safePropKey = (id: string) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(id) ? id : `'${id}'`
|
|
122
|
+
const safePropKey = (id: string) => (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(id) ? id : `'${id}'`)
|
|
101
123
|
|
|
102
124
|
const buildBodyObjType = (methods: string[], typeStr: string): string => {
|
|
103
125
|
const parts: string[] = []
|
|
@@ -121,7 +143,7 @@ const buildRawResponseType = (operationId: string, response: STResponse | null):
|
|
|
121
143
|
const declared: string[] = []
|
|
122
144
|
const arms: string[] = []
|
|
123
145
|
|
|
124
|
-
for (const [rawKey, entry] of
|
|
146
|
+
for (const [rawKey, entry] of expandRanges(response)) {
|
|
125
147
|
if (!entry) continue
|
|
126
148
|
const status = rawKey === 'default' ? null : Number(rawKey)
|
|
127
149
|
if (status === null) continue // handled as fallback
|
|
@@ -129,14 +151,14 @@ const buildRawResponseType = (operationId: string, response: STResponse | null):
|
|
|
129
151
|
const ok = OKS.has(status)
|
|
130
152
|
const { methods, typeStr } = responseBodyInfo(entry)
|
|
131
153
|
const headersT = responseHeadersType(entry)
|
|
132
|
-
arms.push(
|
|
133
|
-
`{status:${status};ok:${ok};headers:${headersT};body:${buildBodyObjType(methods, typeStr)}}`
|
|
134
|
-
)
|
|
154
|
+
arms.push(`{status:${status};ok:${ok};headers:${headersT};body:${buildBodyObjType(methods, typeStr)}}`)
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
// fallback arm
|
|
138
158
|
const defaultEntry = (response as any)['default'] as STResponseEntry | undefined
|
|
139
|
-
const fallbackBody = defaultEntry
|
|
159
|
+
const fallbackBody = defaultEntry
|
|
160
|
+
? buildBodyObjType(...(Object.values(responseBodyInfo(defaultEntry)) as [string[], string]))
|
|
161
|
+
: FALLBACK_BODY
|
|
140
162
|
const excludeStr = declared.length ? `Exclude<_HttpStatus,${declared.join('|')}>` : '_HttpStatus'
|
|
141
163
|
arms.push(`{status:${excludeStr};ok:boolean;headers:Headers;body:${fallbackBody}}`)
|
|
142
164
|
|
|
@@ -147,7 +169,7 @@ const buildRawResponseType = (operationId: string, response: STResponse | null):
|
|
|
147
169
|
const buildSuccessType = (response: STResponse | null): string => {
|
|
148
170
|
if (!response) return 'unknown'
|
|
149
171
|
const types: string[] = []
|
|
150
|
-
for (const [rawKey, entry] of
|
|
172
|
+
for (const [rawKey, entry] of expandRanges(response)) {
|
|
151
173
|
if (!entry) continue
|
|
152
174
|
const status = rawKey === 'default' ? null : Number(rawKey)
|
|
153
175
|
if (status === null || !OKS.has(status)) continue
|
|
@@ -167,7 +189,7 @@ const buildErrorType = (operationId: string, response: STResponse | null): strin
|
|
|
167
189
|
const declared: string[] = []
|
|
168
190
|
const arms: string[] = []
|
|
169
191
|
|
|
170
|
-
for (const [rawKey, entry] of
|
|
192
|
+
for (const [rawKey, entry] of expandRanges(response)) {
|
|
171
193
|
if (!entry) continue
|
|
172
194
|
const status = rawKey === 'default' ? null : Number(rawKey)
|
|
173
195
|
if (status === null || OKS.has(status)) continue
|
|
@@ -180,9 +202,9 @@ const buildErrorType = (operationId: string, response: STResponse | null): strin
|
|
|
180
202
|
const defaultEntry = (response as any)['default'] as STResponseEntry | undefined
|
|
181
203
|
const fallbackBody = defaultEntry ? responseBodyInfo(defaultEntry).typeStr : 'any'
|
|
182
204
|
// also exclude all declared 2xx
|
|
183
|
-
const declared2xx =
|
|
205
|
+
const declared2xx = expandRanges(response)
|
|
206
|
+
.map(([k]) => k)
|
|
184
207
|
.filter(k => k !== 'default' && OKS.has(Number(k)))
|
|
185
|
-
.map(String)
|
|
186
208
|
const allDeclared = [...declared, ...declared2xx]
|
|
187
209
|
const errExcludeStr = allDeclared.length ? `Exclude<_HttpStatus,${allDeclared.join('|')}>` : '_HttpStatus'
|
|
188
210
|
arms.push(`{status:${errExcludeStr};headers:Headers;body:${fallbackBody}}`)
|
|
@@ -217,7 +239,10 @@ const expandRoute = (r: GalbeClientRoute): RouteVariant[] => {
|
|
|
217
239
|
Object.entries(r.query).map(([k, v]) => [k, { typeStr: v.type, optional: v.optional, description: v.description }])
|
|
218
240
|
)
|
|
219
241
|
const reqHeaders = Object.fromEntries(
|
|
220
|
-
Object.entries(r.headers).map(([k, v]) => [
|
|
242
|
+
Object.entries(r.headers).map(([k, v]) => [
|
|
243
|
+
k,
|
|
244
|
+
{ typeStr: v.type, optional: v.optional, description: v.description },
|
|
245
|
+
])
|
|
221
246
|
)
|
|
222
247
|
|
|
223
248
|
const bodyEntries = r.body ? Object.entries(r.body) : []
|
|
@@ -289,9 +314,7 @@ const buildParamList = (v: RouteVariant): string => {
|
|
|
289
314
|
const buildFetchCall = (v: RouteVariant, fnName: string): string => {
|
|
290
315
|
const pathExpr = v.params.length ? `\`${v.pathTemplate}\`` : `'${v.path}'`
|
|
291
316
|
const bodyArg = v.bodyShortName !== null ? 'body' : 'undefined'
|
|
292
|
-
const optionsArg = v.bodyShortName !== null
|
|
293
|
-
? `{...(options??{}),contentType:'${v.bodyShortName}'}`
|
|
294
|
-
: 'options'
|
|
317
|
+
const optionsArg = v.bodyShortName !== null ? `{...(options??{}),contentType:'${v.bodyShortName}'}` : 'options'
|
|
295
318
|
return `${fnName}(this.#config,'${v.method.toUpperCase()}',${pathExpr},${bodyArg},${optionsArg})`
|
|
296
319
|
}
|
|
297
320
|
|
|
@@ -413,7 +436,9 @@ export default (cmd: Command) => {
|
|
|
413
436
|
if (!out) out = { ts: 'dist/client.ts', js: 'dist/client.js' }[target as 'ts' | 'js']
|
|
414
437
|
|
|
415
438
|
let pckg: any = {}
|
|
416
|
-
try {
|
|
439
|
+
try {
|
|
440
|
+
pckg = await Bun.file(resolve(CWD, 'package.json')).json()
|
|
441
|
+
} catch {}
|
|
417
442
|
|
|
418
443
|
let error: any = null
|
|
419
444
|
Bun.write(Bun.stdout, '💻 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m client\x1b[0m')
|
|
@@ -424,7 +449,9 @@ export default (cmd: Command) => {
|
|
|
424
449
|
await instanciateRoutes(mod)
|
|
425
450
|
await mod.init()
|
|
426
451
|
return mod
|
|
427
|
-
} catch (err) {
|
|
452
|
+
} catch (err) {
|
|
453
|
+
error = err
|
|
454
|
+
}
|
|
428
455
|
})
|
|
429
456
|
|
|
430
457
|
if (error) {
|
|
@@ -444,8 +471,11 @@ export default (cmd: Command) => {
|
|
|
444
471
|
const routes: GalbeClientRoute[] = []
|
|
445
472
|
const autoDerivedIds: string[] = []
|
|
446
473
|
|
|
474
|
+
// meta keys are relative to basePath; route paths carry it
|
|
475
|
+
const prefix = g.router.prefix || ''
|
|
447
476
|
walkRoutes(g.router.routes, r => {
|
|
448
|
-
const
|
|
477
|
+
const rPath = prefix && r.path.startsWith(prefix) ? r.path.slice(prefix.length) || '/' : r.path
|
|
478
|
+
const meta = metaRoutes?.[rPath]?.[r.method]
|
|
449
479
|
const [, summary, description] = meta?.head?.match(/^([^\n]*)\n\n(.*)/) ?? []
|
|
450
480
|
const explicitId: string | undefined = meta?.operationId
|
|
451
481
|
const autoDerived = !explicitId
|
|
@@ -455,7 +485,7 @@ export default (cmd: Command) => {
|
|
|
455
485
|
// Collect named types
|
|
456
486
|
Object.values(r.schema.response ?? {}).forEach(entry => {
|
|
457
487
|
if (!entry) return
|
|
458
|
-
const s = isResponseValue(entry as STResponseEntry) ? entry as STSchema : null
|
|
488
|
+
const s = isResponseValue(entry as STResponseEntry) ? (entry as STSchema) : null
|
|
459
489
|
if (s?.id) namedTypes[s.id] = schemaToTypeStr(s)
|
|
460
490
|
})
|
|
461
491
|
|
|
@@ -533,8 +563,8 @@ export default (cmd: Command) => {
|
|
|
533
563
|
const allVariants = finalRoutes.flatMap(expandRoute)
|
|
534
564
|
Bun.write(Bun.stdout, '\n')
|
|
535
565
|
for (const v of allVariants) {
|
|
536
|
-
const sourceRoute = finalRoutes.find(
|
|
537
|
-
r.operationId === v.operationId || v.operationId.startsWith(r.operationId)
|
|
566
|
+
const sourceRoute = finalRoutes.find(
|
|
567
|
+
r => r.operationId === v.operationId || v.operationId.startsWith(r.operationId)
|
|
538
568
|
)
|
|
539
569
|
const isAuto = sourceRoute?.autoDerived
|
|
540
570
|
Bun.write(
|
|
@@ -561,7 +591,8 @@ export default (cmd: Command) => {
|
|
|
561
591
|
|
|
562
592
|
if (target === 'js') {
|
|
563
593
|
code = transformSync(code, {
|
|
564
|
-
|
|
594
|
+
// @swc/wasm's JscTarget typing lags @swc/core's; 'esnext' is supported at runtime
|
|
595
|
+
jsc: { parser: { syntax: 'typescript' }, preserveAllComments: true, target: 'esnext' as any },
|
|
565
596
|
}).code
|
|
566
597
|
}
|
|
567
598
|
|