galbe 0.3.0 → 0.5.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/.github/workflows/build_test.yml +2 -4
- package/.github/workflows/release.yml +2 -2
- package/bin/cli.ts +8 -104
- package/bin/commands/build.ts +95 -0
- package/bin/commands/dev.ts +53 -0
- package/bin/commands/generate/client.ts +192 -0
- package/bin/commands/generate/code/openapi.parser.ts +479 -0
- package/bin/commands/generate/code.ts +75 -0
- package/bin/commands/generate/index.ts +12 -0
- package/bin/commands/generate/spec.ts +94 -0
- package/bin/res/cli.template.js +120 -0
- package/bin/res/client.template.ts +161 -0
- package/bin/util.ts +164 -0
- package/bun.lockb +0 -0
- package/docs/plugins.md +36 -36
- package/docs/routes.md +3 -3
- package/package.json +10 -8
- package/scripts/postinstall.ts +1 -3
- package/src/extras/spec/openapi.serializer.ts +264 -0
- package/src/extras.ts +1 -0
- package/src/index.ts +103 -73
- package/src/parser.ts +27 -5
- package/src/router.ts +27 -29
- package/src/routes.ts +138 -39
- package/src/schema.ts +69 -8
- package/src/server.ts +9 -10
- package/src/types.ts +73 -41
- package/src/util.ts +95 -10
- package/src/validator.ts +2 -0
- package/test/parser.test.ts +34 -0
- package/test/requests.test.ts +5 -12
- package/test/resources/test.route.comment.ts +20 -0
- package/test/responses.test.ts +109 -11
- package/test/routeFiles.test.ts +44 -27
- package/test/router.test.ts +67 -42
- package/scripts/build.ts +0 -14
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { program, Option } from 'commander'
|
|
4
|
+
import { resolve } from 'path'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HEADERS = {
|
|
7
|
+
'user-agent': 'Galbe//*%(()=>version)()%*//cli'
|
|
8
|
+
}
|
|
9
|
+
const ansi = (p, c, str) => (p ? `\x1b[${c}m${str}\x1b[0m` : str)
|
|
10
|
+
|
|
11
|
+
const fmtObject = (o, p = false, idt = 2, iidt = 0) => {
|
|
12
|
+
let _ = ' '.repeat(iidt)
|
|
13
|
+
let __ = ' '.repeat(iidt + idt)
|
|
14
|
+
let lr = idt === 0 ? '' : '\n'
|
|
15
|
+
if (o === null) return ansi(p, '38;2;255;128;0', 'null')
|
|
16
|
+
if (typeof o === 'boolean') return o ? ansi(p, '38;2;255;128;0', 'true') : ansi(p, '38;2;255;128;0', 'false')
|
|
17
|
+
if (typeof o === 'number') return ansi(p, '38;2;10;180;220', o)
|
|
18
|
+
if (typeof o === 'string') return ansi(p, '38;2;125;170;0', `"${o}"`)
|
|
19
|
+
if (typeof o === 'object') {
|
|
20
|
+
if (Array.isArray(o))
|
|
21
|
+
return `[${lr}${o.map(e => `${__}${fmtObject(e, p, idt, iidt + idt)}`).join(`,${lr}`)}${lr}${_}]`
|
|
22
|
+
return `{${lr}${Object.entries(o)
|
|
23
|
+
.map(([k, v]) =>
|
|
24
|
+
v === undefined ? '' : `${__}${ansi(p, '38;2;170;120;200', `"${k}"`)}: ${fmtObject(v, p, idt, iidt + idt)}`
|
|
25
|
+
)
|
|
26
|
+
.filter(l => l)
|
|
27
|
+
.join(`,${lr}`)}${lr}${_}}`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const fmtRes = (r, p = false) => {
|
|
31
|
+
if (typeof r === 'object') {
|
|
32
|
+
try {
|
|
33
|
+
let resp = fmtObject(r, p, 2)
|
|
34
|
+
return process.stdout.write(`${resp}\n`)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(err)
|
|
37
|
+
return process.stdout.write(`${r}\n`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!p) return process.stdout.write(`${r}\n`)
|
|
41
|
+
if (typeof r === 'boolean') return process.stdout.write(`${ansi(p, '38;2;255;128;0', r)}\n`)
|
|
42
|
+
if (typeof r === 'number') return process.stdout.write(`${ansi(p, '38;2;10;180;220', r)}\n`)
|
|
43
|
+
if (typeof r === 'string') return process.stdout.write(`${ansi(p, '38;2;125;170;0', r)}\n`)
|
|
44
|
+
return process.stdout.write(`${r}\n`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fetchApi = async (method, path, props) => {
|
|
48
|
+
let headers = Object.fromEntries(props?.['%header'].map(s => s.split('=')))
|
|
49
|
+
let queryParam = Object.fromEntries(props?.['%query'].map(s => s.split('=')))
|
|
50
|
+
let body = props?.['%body']
|
|
51
|
+
let bodyFile = props?.['%bodyFile']
|
|
52
|
+
let format = new Set(props?.['%format'])
|
|
53
|
+
|
|
54
|
+
delete props?.['%header']
|
|
55
|
+
delete props?.['%query']
|
|
56
|
+
delete props?.['%body']
|
|
57
|
+
delete props?.['%bodyFile']
|
|
58
|
+
delete props?.['%format']
|
|
59
|
+
|
|
60
|
+
const queryString = Object.entries({ ...queryParam, ...props })
|
|
61
|
+
.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v))
|
|
62
|
+
.join('&')
|
|
63
|
+
|
|
64
|
+
if (!Bun.env.GCLI_SERVER_URL) {
|
|
65
|
+
console.error('error: Missing GCLI_SERVER_URL env')
|
|
66
|
+
return process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
let url = `${Bun.env.GCLI_SERVER_URL}${path}?${queryString}`
|
|
69
|
+
if (bodyFile) body = await Bun.file(resolve(process.cwd(), bodyFile)).arrayBuffer()
|
|
70
|
+
let startTime = Bun.nanoseconds()
|
|
71
|
+
let res = await fetch(url, {
|
|
72
|
+
method,
|
|
73
|
+
headers: {
|
|
74
|
+
...DEFAULT_HEADERS,
|
|
75
|
+
...(bodyFile ? { 'content-type': 'application/octet-stream' } : {}),
|
|
76
|
+
...(headers || {})
|
|
77
|
+
},
|
|
78
|
+
...(body ? { body } : {})
|
|
79
|
+
})
|
|
80
|
+
let endTime = Bun.nanoseconds() - startTime
|
|
81
|
+
if (format.has('s')) fmtRes(res.status, format.has('p'))
|
|
82
|
+
if (format.has('h')) fmtRes(Object.fromEntries(res.headers.entries()), format.has('p'))
|
|
83
|
+
if (format.has('b')) {
|
|
84
|
+
let isJson = res.headers.get('content-type') === 'application/json'
|
|
85
|
+
if (isJson) fmtRes(await res.json(), format.has('p'))
|
|
86
|
+
else if (res.headers.get('content-type')?.match(/^text\//)) fmtRes(await res.text(), format.has('p'))
|
|
87
|
+
else process.stdout.write(await res.arrayBuffer())
|
|
88
|
+
}
|
|
89
|
+
if (format.has('t')) process.stdout.write(`${(endTime / 1_000_000).toFixed(2)}ms\n`)
|
|
90
|
+
process.exit(res.ok ? 0 : 1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
program.name('/*%(()=>name)()%*/').description('/*%(()=>description)()%*/').version('/*%(()=>version)()%*/')
|
|
94
|
+
/*%
|
|
95
|
+
const formatDefault = def =>
|
|
96
|
+
typeof def === 'string'
|
|
97
|
+
? `\`${def}\``
|
|
98
|
+
: Array.isArray(def)
|
|
99
|
+
? `[${def.map(d => formatDefault(d)).join(',')}]`
|
|
100
|
+
: def ?? 'undefined';
|
|
101
|
+
|
|
102
|
+
result = commands.map(c=>{
|
|
103
|
+
let args = c.arguments.map(a=>`.argument("${a.type}", "${a.description}")`)
|
|
104
|
+
let optionsBase = [
|
|
105
|
+
{name: '%format', short:'%f', type: '[string]', description: 'response format [\'s\',\'h\',\'b\',\'t\',\'p\']', default:["s","b","p"]},
|
|
106
|
+
{name: '%header', short:'%h', type: '<string...>', description: 'request header formated as headerName=headerValue', default:[]},
|
|
107
|
+
{name: '%query', short:'%q', type: '<string...>', description: 'query param formated as paramName=paramValue', default:[]},
|
|
108
|
+
{name: '%body', short:'%b', type: '<string>', description: 'request body', default:''},
|
|
109
|
+
{name: '%bodyFile', short:'%bf', type: '<path>', description: 'request body file', default:''}
|
|
110
|
+
]
|
|
111
|
+
let options = [...optionsBase,...(c.options||[])].map(o=>`.addOption(new Option("-${o.short}, --${o.name} ${o.type}", "${o.description}").default(${formatDefault(o.default)}))`)
|
|
112
|
+
let action = `.action(async (${c.arguments.map(a=>`${a.name},`)} props) => {
|
|
113
|
+
${c.action ? ';('+c.action.toString()+')(props)' : ''}
|
|
114
|
+
return await fetchApi("${c.route.method.toUpperCase()}",\`${c.route.pathT}\`, props)
|
|
115
|
+
})`
|
|
116
|
+
return `program.command("${c.name}").description("${c.description}")${args.join('')}${options.join('')}${action}`
|
|
117
|
+
})
|
|
118
|
+
%*/
|
|
119
|
+
|
|
120
|
+
program.parse()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export type GalbeClientMode = 'response' | 'direct'
|
|
2
|
+
export type GalbeClientConfig = {
|
|
3
|
+
server?: { url?: string }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const Kind = Symbol.for('json.string')
|
|
7
|
+
type Json<T> = { T: T }
|
|
8
|
+
|
|
9
|
+
interface GR<S extends number = number, B = any, OKS extends number = OKStatusCode> {
|
|
10
|
+
status: S
|
|
11
|
+
ok: S extends OKS ? true : false
|
|
12
|
+
redirected: boolean
|
|
13
|
+
statusText: string
|
|
14
|
+
type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect'
|
|
15
|
+
url: string
|
|
16
|
+
headers: Headers
|
|
17
|
+
body: <ST extends boolean = false>(
|
|
18
|
+
stream?: ST
|
|
19
|
+
) => B extends Uint8Array
|
|
20
|
+
? ST extends true
|
|
21
|
+
? Promise<AsyncGenerator<Uint8Array, void, unknown>>
|
|
22
|
+
: B extends Json<infer T>
|
|
23
|
+
? Promise<T>
|
|
24
|
+
: Promise<B>
|
|
25
|
+
: B extends string
|
|
26
|
+
? ST extends true
|
|
27
|
+
? Promise<AsyncGenerator<string, void, unknown>>
|
|
28
|
+
: B extends Json<infer T>
|
|
29
|
+
? Promise<T>
|
|
30
|
+
: Promise<B>
|
|
31
|
+
: B extends Json<infer T>
|
|
32
|
+
? Promise<T>
|
|
33
|
+
: Promise<B>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type OKStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
|
|
37
|
+
// prettier-ignore
|
|
38
|
+
export type HttpStatusCode = 100|101|102|103|OKStatusCode|300|301|302|303|304|305|307|308|400|401|402|403|404|405|406|407|408|409|410|411|412|413|414|415|416|417|418|421|422|423|424|426|428|429|431|451|500|501|502|503|504|505|506|507|508|510|511
|
|
39
|
+
type PGR<
|
|
40
|
+
S extends number = number,
|
|
41
|
+
B = any,
|
|
42
|
+
O extends number = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
|
|
43
|
+
> = Promise<GR<S, B, O>>
|
|
44
|
+
|
|
45
|
+
type RequestOptions<H = any, B = any> = {
|
|
46
|
+
headers?: H
|
|
47
|
+
body?: B
|
|
48
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const decoder = new TextDecoder()
|
|
52
|
+
const DEFAULT_HEADERS = {
|
|
53
|
+
'user-agent': 'Galbe//*%(()=>version)()%*/'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default class GalbeClient {
|
|
57
|
+
config?: GalbeClientConfig
|
|
58
|
+
|
|
59
|
+
/*%
|
|
60
|
+
Object.entries(routes).map(([method, list])=>{
|
|
61
|
+
return`${method} = {\n${list.map( r => {
|
|
62
|
+
let p = Object.entries(r.params)
|
|
63
|
+
let schemas = Object.keys(r.schemas).length ?
|
|
64
|
+
`<${r.schemas.headers??'any'},${r.schemas.body??'any'}>`:
|
|
65
|
+
''
|
|
66
|
+
let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
|
|
67
|
+
let responses = Object.keys(r.schemas?.response||{}).length ?
|
|
68
|
+
`${Object.entries(r.schemas.response).map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).join('|')}>,any${oks?.length?`,${oks.join('|')}`:''}>`:
|
|
69
|
+
`PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
|
|
70
|
+
return ` '${r.path}':(${p.length?p.map(([k,v])=>`${k}:${v.type}`).join(',')+', ':''}options:RequestOptions${schemas}={})=>this.fetch(\`${r.pathT}\`,{...options,method:'${r.method.toUpperCase()}'}) as ${responses}`
|
|
71
|
+
}).join(',\n')}\n}`
|
|
72
|
+
}).join('\n')
|
|
73
|
+
%*/
|
|
74
|
+
|
|
75
|
+
constructor(config?: GalbeClientConfig) {
|
|
76
|
+
//@ts-ignore
|
|
77
|
+
this.config = { mode: 'response', ...config }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async fetch(path: string, options: RequestOptions) {
|
|
81
|
+
let url = `${this?.config?.server?.url ?? ''}${path}`
|
|
82
|
+
let res = await fetch(url, {
|
|
83
|
+
method: options?.method || 'GET',
|
|
84
|
+
headers: { ...DEFAULT_HEADERS, ...(options?.headers || {}) },
|
|
85
|
+
...(options?.body ? { body: JSON.stringify(options.body) } : {})
|
|
86
|
+
})
|
|
87
|
+
return {
|
|
88
|
+
headers: res.headers,
|
|
89
|
+
ok: res.ok,
|
|
90
|
+
redirected: res.redirected,
|
|
91
|
+
status: res.status,
|
|
92
|
+
statusText: res.statusText,
|
|
93
|
+
type: res.type,
|
|
94
|
+
url: res.url,
|
|
95
|
+
body: async (stream = false) => {
|
|
96
|
+
if (res.headers.get('content-type') === 'application/json') return res.json()
|
|
97
|
+
else if (res.headers.get('content-type') === 'text/event-stream') {
|
|
98
|
+
const reader = res.body?.getReader()
|
|
99
|
+
return async function* () {
|
|
100
|
+
while (reader) {
|
|
101
|
+
const { value, done } = await reader.read()
|
|
102
|
+
if (done) break
|
|
103
|
+
yield decoder.decode(value)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (res.headers.get('content-type')?.match(/^text\//)) {
|
|
107
|
+
if (stream) {
|
|
108
|
+
const reader = res.body?.getReader()
|
|
109
|
+
return (async function* () {
|
|
110
|
+
while (reader) {
|
|
111
|
+
const { value, done } = await reader.read()
|
|
112
|
+
if (done) break
|
|
113
|
+
yield decoder.decode(value)
|
|
114
|
+
}
|
|
115
|
+
})()
|
|
116
|
+
} else return res.text()
|
|
117
|
+
} else if (res.headers.get('content-type') === 'application/octet-stream') {
|
|
118
|
+
const reader = res.body?.getReader()
|
|
119
|
+
if (stream) {
|
|
120
|
+
return (async function* () {
|
|
121
|
+
while (reader) {
|
|
122
|
+
const { value, done } = await reader.read()
|
|
123
|
+
if (done) break
|
|
124
|
+
yield value
|
|
125
|
+
}
|
|
126
|
+
})()
|
|
127
|
+
} else {
|
|
128
|
+
let body = new Uint8Array()
|
|
129
|
+
while (reader) {
|
|
130
|
+
const { value, done } = await reader.read()
|
|
131
|
+
if (done) break
|
|
132
|
+
let buff = new Uint8Array(body.length + value.length)
|
|
133
|
+
buff.set(body)
|
|
134
|
+
buff.set(value, body.length)
|
|
135
|
+
body = buff
|
|
136
|
+
}
|
|
137
|
+
return body
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return res.body
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Aliases
|
|
146
|
+
/*%
|
|
147
|
+
Object.entries(routes).map(([method, list])=>{
|
|
148
|
+
return list.filter(r=>r.alias).map(r => {
|
|
149
|
+
let p = Object.entries(r.params)
|
|
150
|
+
let schemas = Object.keys(r.schemas).length ?
|
|
151
|
+
`<${r.schemas.headers??'any'},${r.schemas.body??'any'}>`:
|
|
152
|
+
''
|
|
153
|
+
let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
|
|
154
|
+
let responses = Object.keys(r.schemas?.response||{}).length ?
|
|
155
|
+
`${Object.entries(r.schemas.response).map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).join('|')}>,any${oks?.length?`,${oks.join('|')}`:''}>`:
|
|
156
|
+
`PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
|
|
157
|
+
return `${r.alias}(${p.length ? p.map(([k,v])=>`${k}: ${v.type}`).join(', ')+', ':''}options: RequestOptions${schemas} = {}){return this.fetch(\`${r.pathT}\`, {...options, method: '${r.method.toUpperCase()}'}) as ${responses}}\n`
|
|
158
|
+
}).join(' ')
|
|
159
|
+
})
|
|
160
|
+
%*/
|
|
161
|
+
}
|
package/bin/util.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { relative } from 'path'
|
|
2
|
+
import { watch } from 'chokidar'
|
|
3
|
+
import { Galbe, Route } from '../src'
|
|
4
|
+
import { logRoute, walkRoutes } from '../src/util'
|
|
5
|
+
import { RouteMeta, defineRoutes } from '../src/routes'
|
|
6
|
+
|
|
7
|
+
export { default as pckg } from '../package.json'
|
|
8
|
+
|
|
9
|
+
export const CWD = process.cwd()
|
|
10
|
+
export const WATCH_IGNORE = /\.galbe/
|
|
11
|
+
|
|
12
|
+
export const fmtVal = (v: any) => {
|
|
13
|
+
if (typeof v === 'boolean') return `\x1b[3${v ? '2' : '1'}m${v}\x1b[0m`
|
|
14
|
+
if (typeof v === 'string') return `\x1b[33m${v}\x1b[0m`
|
|
15
|
+
if (typeof v === 'number') return `\x1b[36m${v}\x1b[0m`
|
|
16
|
+
return v
|
|
17
|
+
}
|
|
18
|
+
export const fmtList = (l: any) => `[${l.map(v => fmtVal(v)).join(', ')}]`
|
|
19
|
+
export const fmtInterval = (a: any, b: any) => `[${fmtVal(a)}-${fmtVal(b)}]`
|
|
20
|
+
|
|
21
|
+
export const silentExec = async (fn: () => any) => {
|
|
22
|
+
let consoleMock = Object.fromEntries(
|
|
23
|
+
Object.entries(console)
|
|
24
|
+
.filter(([_, v]) => typeof v === 'function')
|
|
25
|
+
.map(([k, _]) => [k, () => {}])
|
|
26
|
+
)
|
|
27
|
+
const _console = console
|
|
28
|
+
const _processStdoutWrite = process.stdout.write
|
|
29
|
+
const _processStderrWrite = process.stderr.write
|
|
30
|
+
//@ts-ignore
|
|
31
|
+
global.console = consoleMock
|
|
32
|
+
//@ts-ignore
|
|
33
|
+
process.stdout.write = function () {}
|
|
34
|
+
//@ts-ignore
|
|
35
|
+
process.stderr.write = function () {}
|
|
36
|
+
const r = await fn()
|
|
37
|
+
global.console = _console
|
|
38
|
+
process.stdout.write = _processStdoutWrite
|
|
39
|
+
process.stderr.write = _processStderrWrite
|
|
40
|
+
return r
|
|
41
|
+
}
|
|
42
|
+
export const watchDir = async (
|
|
43
|
+
path: string,
|
|
44
|
+
callback: (event: {
|
|
45
|
+
path: string | null
|
|
46
|
+
eventType: 'change' | 'add' | 'addDir' | 'unlink' | 'unlinkDir'
|
|
47
|
+
}) => any | Promise<any>,
|
|
48
|
+
options?: { ignore?: RegExp }
|
|
49
|
+
) => {
|
|
50
|
+
let watcher = watch(path, {
|
|
51
|
+
persistent: false,
|
|
52
|
+
ignored: options?.ignore,
|
|
53
|
+
ignoreInitial: true
|
|
54
|
+
})
|
|
55
|
+
watcher.on('all', async (eventType, filename) => {
|
|
56
|
+
if (filename.match(WATCH_IGNORE)) return
|
|
57
|
+
await callback({ path: filename.toString(), eventType })
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
export const instanciateRoutes = async (g: Galbe) => {
|
|
61
|
+
console.log('🏗️ \x1b[1;30mConstructing routes\x1b[0m\n')
|
|
62
|
+
// Main thread routes definitions
|
|
63
|
+
let hasMainRoutes = false
|
|
64
|
+
walkRoutes(g.router.routes, r => {
|
|
65
|
+
hasMainRoutes = true
|
|
66
|
+
logRoute(r)
|
|
67
|
+
})
|
|
68
|
+
if (hasMainRoutes) process.stdout.write('\n')
|
|
69
|
+
// Route Files Analysis
|
|
70
|
+
let routes: Record<string, { route?: Route; meta?: RouteMeta; error?: any }[]> = {}
|
|
71
|
+
let errors: Record<string, any> = {}
|
|
72
|
+
await defineRoutes({ routes: g?.config?.routes }, g, ({ type, route, error, filepath, meta }) => {
|
|
73
|
+
if (!filepath) return
|
|
74
|
+
if (!(filepath in routes)) routes[filepath] = []
|
|
75
|
+
if (type === 'add' && route && filepath) routes[filepath].push({ route, meta })
|
|
76
|
+
if (type === 'error') errors[filepath] = error
|
|
77
|
+
})
|
|
78
|
+
for (let [fp, e] of Object.entries(routes)) {
|
|
79
|
+
console.log(`\x1b\[0;36m ${relative(CWD, fp)}\x1b[0m`)
|
|
80
|
+
let maxPathLength = e.reduce((p, c) => {
|
|
81
|
+
return Math.max(p, c.route?.path.length || 0)
|
|
82
|
+
}, 0)
|
|
83
|
+
for (let r of e) {
|
|
84
|
+
if (r.route) logRoute(r.route, r.meta, { maxPathLength })
|
|
85
|
+
}
|
|
86
|
+
if (errors?.[fp]) {
|
|
87
|
+
console.log(`\x1b\[0;31m Error:\x1b[0m`)
|
|
88
|
+
console.log(errors?.[fp])
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write('\n')
|
|
91
|
+
}
|
|
92
|
+
console.log('\x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const softMerge = (base, override) => {
|
|
96
|
+
for (const key in override) {
|
|
97
|
+
if (override[key] instanceof Object && !(override[key] instanceof Array)) {
|
|
98
|
+
if (!base[key]) Object.assign(base, { [key]: {} })
|
|
99
|
+
softMerge(base[key], override[key])
|
|
100
|
+
} else Object.assign(base, { [key]: override[key] })
|
|
101
|
+
}
|
|
102
|
+
return base
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const HttpStatus = {
|
|
106
|
+
100: 'Continue',
|
|
107
|
+
101: 'Switching Protocols',
|
|
108
|
+
102: 'Processing',
|
|
109
|
+
103: 'Early Hints',
|
|
110
|
+
200: 'OK',
|
|
111
|
+
201: 'Created',
|
|
112
|
+
202: 'Accepted',
|
|
113
|
+
203: 'Non Authoritative Information',
|
|
114
|
+
204: 'No Content',
|
|
115
|
+
205: 'Reset Content',
|
|
116
|
+
206: 'Partial Content',
|
|
117
|
+
207: 'Multi-Status',
|
|
118
|
+
300: 'Multiple Choices',
|
|
119
|
+
301: 'Moved Permanently',
|
|
120
|
+
302: 'Moved Temporarily',
|
|
121
|
+
303: 'See Other',
|
|
122
|
+
304: 'Not Modified',
|
|
123
|
+
305: 'Use Proxy',
|
|
124
|
+
307: 'Temporary Redirect',
|
|
125
|
+
308: 'Permanent Redirect',
|
|
126
|
+
400: 'Bad Request',
|
|
127
|
+
401: 'Unauthorized',
|
|
128
|
+
402: 'Payment Required',
|
|
129
|
+
403: 'Forbidden',
|
|
130
|
+
404: 'Not Found',
|
|
131
|
+
405: 'Method Not Allowed',
|
|
132
|
+
406: 'Not Acceptable',
|
|
133
|
+
407: 'Proxy Authentication Required',
|
|
134
|
+
408: 'Request Timeout',
|
|
135
|
+
409: 'Conflict',
|
|
136
|
+
410: 'Gone',
|
|
137
|
+
411: 'Length Required',
|
|
138
|
+
412: 'Precondition Failed',
|
|
139
|
+
413: 'Request Entity Too Large',
|
|
140
|
+
414: 'Request-URI Too Long',
|
|
141
|
+
415: 'Unsupported Media Type',
|
|
142
|
+
416: 'Requested Range Not Satisfiable',
|
|
143
|
+
417: 'Expectation Failed',
|
|
144
|
+
418: "I'm a teapot",
|
|
145
|
+
419: 'Insufficient Space on Resource',
|
|
146
|
+
420: 'Method Failure',
|
|
147
|
+
421: 'Misdirected Request',
|
|
148
|
+
422: 'Unprocessable Entity',
|
|
149
|
+
423: 'Locked',
|
|
150
|
+
424: 'Failed Dependency',
|
|
151
|
+
426: 'Upgrade Required',
|
|
152
|
+
428: 'Precondition Required',
|
|
153
|
+
429: 'Too Many Requests',
|
|
154
|
+
431: 'Request Header Fields Too Large',
|
|
155
|
+
451: 'Unavailable For Legal Reasons',
|
|
156
|
+
500: 'Internal Server Error',
|
|
157
|
+
501: 'Not Implemented',
|
|
158
|
+
502: 'Bad Gateway',
|
|
159
|
+
503: 'Service Unavailable',
|
|
160
|
+
504: 'Gateway Timeout',
|
|
161
|
+
505: 'HTTP Version Not Supported',
|
|
162
|
+
507: 'Insufficient Storage',
|
|
163
|
+
511: 'Network Authentication Required'
|
|
164
|
+
}
|
package/bun.lockb
CHANGED
|
Binary file
|
package/docs/plugins.md
CHANGED
|
@@ -17,7 +17,7 @@ type GalbePlugin = {
|
|
|
17
17
|
|
|
18
18
|
**name**
|
|
19
19
|
|
|
20
|
-
The name should be a Unique Plugin Identifier. It should be chosen to be unique to avoid conflicts with other potential plugins.
|
|
20
|
+
The name should be a Unique Plugin Identifier. It should be chosen to be unique to avoid conflicts with other potential plugins. Ideally, it will have the form of `com.example.myplugin`.
|
|
21
21
|
|
|
22
22
|
**init**
|
|
23
23
|
|
|
@@ -62,44 +62,44 @@ Here is an example of a plugin implementation that handles routes tagged with `@
|
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
64
|
// myPlugin.ts
|
|
65
|
-
|
|
66
|
-
import {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
import type { GalbePlugin } from 'galbe'
|
|
66
|
+
import { walkMetaRoutes } from 'galbe/utils'
|
|
67
|
+
|
|
68
|
+
const PLUGIN_ID = 'dev.galbe.deprecated'
|
|
69
|
+
|
|
70
|
+
export default () => {
|
|
71
|
+
let deprecateds = new Set<string>()
|
|
72
|
+
return {
|
|
73
|
+
name: PLUGIN_ID,
|
|
74
|
+
// Init the plugin, check for deprecated metadata tags
|
|
75
|
+
init(_config, galbe) {
|
|
76
|
+
if (galbe.meta) {
|
|
77
|
+
walkMetaRoutes(galbe.meta, (method, path, meta) => {
|
|
78
|
+
if (meta.deprecated) deprecateds.add(JSON.stringify({ method, path }))
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
// Check if the current route is deprecated; if so, flag it as such and log it
|
|
83
|
+
onRoute(context) {
|
|
84
|
+
let r = context.route
|
|
85
|
+
if (!r) return
|
|
86
|
+
if (deprecateds.has(JSON.stringify({ method: r.method, path: r.path }))) {
|
|
87
|
+
context.state[PLUGIN_ID] = { deprecated: true }
|
|
88
|
+
console.warn(`Call to deprecated route "${r.method} ${r.path}"`)
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
// Add a header to the response if the route has been flagged as deprecated
|
|
92
|
+
afterHandle(response, context) {
|
|
93
|
+
let r = context.route
|
|
94
|
+
if (!r) return
|
|
95
|
+
console.log('yooo', r.method, r.path)
|
|
96
|
+
if (deprecateds.has(JSON.stringify({ method: r.method, path: r.path }))) {
|
|
97
|
+
console.log('OK...')
|
|
98
|
+
response.headers.set('x-deprecated', 'true')
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
|
-
}
|
|
86
|
-
// Check if the current route is deprecated; if so, flag it as such and log it
|
|
87
|
-
onRoute(context: Context) {
|
|
88
|
-
let route = context.route
|
|
89
|
-
if (this.deprecated?.[route.method]?.includes(route.path)) {
|
|
90
|
-
context.state[this.name] = { deprecated: true }
|
|
91
|
-
console.warn(`Call to deprecated route [${route.method}]${route.path}`)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Add a header if the request has previously been flagged as deprecated
|
|
95
|
-
afterHandle(response: Response, context: Context) {
|
|
96
|
-
if (context.state?.[this.name]?.deprecated) {
|
|
97
|
-
response.headers.set('x-deprecated', 'true')
|
|
98
|
-
}
|
|
99
|
-
}
|
|
101
|
+
} as GalbePlugin
|
|
100
102
|
}
|
|
101
|
-
|
|
102
|
-
export default new MyPlugin()
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
```ts
|
package/docs/routes.md
CHANGED
|
@@ -10,7 +10,7 @@ Here is how to define routes in Galbe.
|
|
|
10
10
|
galbe.[method](path: string, schema?: Schema, hooks?: Hooks[], handler: Handler)
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
**method** ( get | post | put | delete | patch | options )
|
|
13
|
+
**method** ( get | post | put | delete | patch | options | head )
|
|
14
14
|
|
|
15
15
|
The [HTTP Request Method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) for the defined route.
|
|
16
16
|
|
|
@@ -114,8 +114,8 @@ export default g => {
|
|
|
114
114
|
/**
|
|
115
115
|
* This is a route head comment
|
|
116
116
|
* @deprecated
|
|
117
|
-
* @
|
|
118
|
-
* @
|
|
117
|
+
* @operationId fooBar
|
|
118
|
+
* @tags tag1 tag2
|
|
119
119
|
*/
|
|
120
120
|
g.get('/foo/:bar', ctx => ctx.params.bar)
|
|
121
121
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "galbe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
|
|
5
5
|
"author": "Pierre Caillaud M (https://github.com/pierre-cm)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": "./bin/cli.ts",
|
|
8
|
-
"main": "./
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
8
|
+
"main": "./src/index.ts",
|
|
10
9
|
"exports": {
|
|
11
|
-
".": "./
|
|
12
|
-
"
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./schema": "./src/schema.ts",
|
|
12
|
+
"./extras": "./src/extras.ts",
|
|
13
|
+
"./utils": "./src/util.ts"
|
|
13
14
|
},
|
|
14
15
|
"repository": {
|
|
15
16
|
"type": "git",
|
|
@@ -27,14 +28,13 @@
|
|
|
27
28
|
],
|
|
28
29
|
"license": "MIT",
|
|
29
30
|
"scripts": {
|
|
30
|
-
"build": "bun ./scripts/build.ts",
|
|
31
|
-
"clean": "rm -rf dist",
|
|
32
31
|
"test": "bun test",
|
|
33
32
|
"postinstall": "bun run ./scripts/postinstall.ts",
|
|
34
33
|
"release": "release-it"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
36
|
"@types/bun": "^1.0.4",
|
|
37
|
+
"openapi-types": "^12.1.3",
|
|
38
38
|
"release-it": "^17.1.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"@swc/wasm": "^1.4.0",
|
|
46
46
|
"acorn": "^8.11.2",
|
|
47
47
|
"acorn-walk": "^8.3.0",
|
|
48
|
-
"
|
|
48
|
+
"chokidar": "^3.6.0",
|
|
49
|
+
"commander": "^11.1.0",
|
|
50
|
+
"js-yaml": "^4.1.0"
|
|
49
51
|
},
|
|
50
52
|
"release-it": {
|
|
51
53
|
"git": {
|
package/scripts/postinstall.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { $ } from 'bun'
|
|
2
|
-
import { existsSync } from '
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
3
|
|
|
4
4
|
if (existsSync('.git')) {
|
|
5
5
|
console.log('Setting up dev environment')
|
|
@@ -7,5 +7,3 @@ if (existsSync('.git')) {
|
|
|
7
7
|
await $`chmod +x .git/hooks/prepare-commit-msg`
|
|
8
8
|
console.log('done')
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
await $`bun run build`
|