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.
@@ -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. For example, Galbe's official plugins names will always start with `dev.galbe.*`.
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 { Galbe, type Context, type Route } from 'galbe'
67
-
68
- class MyPlugin {
69
- name = 'dev.galbe.example'
70
- deprecated: Record<string, string[]> = {}
71
- // Retrieve and store all route with a @deprecated flag metadata
72
- init(config: any, galbe: Galbe) {
73
- if (config?.enabled && galbe.meta) {
74
- for (const f of galbe.meta) {
75
- for (const [path, methods] of Object.entries(f.routes)) {
76
- for (const [method, meta] of Object.entries(methods)) {
77
- if (meta.deprecated) {
78
- if (!this.deprecated?.[method]) this.deprecated[method] = []
79
- this.deprecated[method].push(path)
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
- * @tag tag1
118
- * @tag tag2
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.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": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
8
+ "main": "./src/index.ts",
10
9
  "exports": {
11
- ".": "./dist/index.js",
12
- "./*": "./dist/*.js"
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
- "commander": "^11.1.0"
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": {
@@ -1,5 +1,5 @@
1
1
  import { $ } from 'bun'
2
- import { existsSync } from 'node:fs'
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`