galbe 0.3.0 → 0.4.1

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.
@@ -1,10 +1,10 @@
1
- name: Build & Test
1
+ name: Install & Test
2
2
  on:
3
3
  push:
4
4
  pull_request:
5
5
  jobs:
6
6
  build:
7
- name: Build & Test
7
+ name: Install & Test
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
10
  - uses: actions/checkout@v4
@@ -13,7 +13,5 @@ jobs:
13
13
  bun-version: latest
14
14
  - name: Install
15
15
  run: bun install
16
- - name: Build
17
- run: bun run build
18
16
  - name: Test
19
17
  run: bun test
@@ -30,9 +30,9 @@ jobs:
30
30
  run: |
31
31
  git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
32
32
  git config user.name "${GITHUB_ACTOR}"
33
- - name: Install dependencies & build
33
+ - name: Install dependencies
34
34
  run: |
35
- bun install && bun run build
35
+ bun install
36
36
  - name: Release
37
37
  run: |
38
38
  npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
package/bin/cli.ts CHANGED
@@ -1,113 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { $, Glob } from 'bun'
4
- import type { RouteMeta } from '../src/routes'
5
3
  import { program } from 'commander'
6
- import { relative, resolve } from 'path'
7
- import { mkdir, readdir, lstat, rm } from 'fs/promises'
8
- import { DEFAULT_ROUTE_PATTERN, metaAnalysis } from '../src/routes'
9
- import { randomUUID } from 'crypto'
10
- import { Galbe } from '../src'
11
4
 
12
- const ROOT = process.cwd()
13
- const BUILD_ID = randomUUID()
5
+ import { pckg } from './util'
14
6
 
15
- Bun.env.FORCE_COLOR = '1'
7
+ import dev from './commands/dev'
8
+ import build from './commands/build'
9
+ import generate from './commands/generate'
16
10
 
17
- const parseRoutes = async (routes?: boolean | string | string[]): Promise<{ path: string; meta: RouteMeta }[]> => {
18
- routes = routes === true ? DEFAULT_ROUTE_PATTERN : routes
19
- if (!routes) return []
20
- let files: { path: string; meta: RouteMeta }[] = []
21
- if (typeof routes === 'string') {
22
- for await (const path of new Glob(routes).scan({ cwd: ROOT, absolute: true, onlyFiles: false })) {
23
- const isDir = (await lstat(path)).isDirectory()
24
- if (isDir) {
25
- files = files.concat(
26
- await Promise.all(
27
- (
28
- await readdir(path)
29
- ).map(async f => ({
30
- path: f,
31
- meta: await metaAnalysis(path)
32
- }))
33
- )
34
- )
35
- } else files.push({ path, meta: await metaAnalysis(path) })
36
- }
37
- }
38
- if (Array.isArray(routes)) for (const r of routes) files = files.concat(await parseRoutes(r))
39
- return files
40
- }
11
+ program.name('galbe').description(pckg.description).version(pckg.version)
41
12
 
42
- const createBuildIndex = async (indexPath: string, routes: { path: string; meta: RouteMeta }[]) => {
43
- const buildPath = resolve(ROOT, '.galbe', 'build', BUILD_ID)
44
- await mkdir(buildPath, { recursive: true })
45
- await Bun.write(
46
- resolve(buildPath, 'index.ts'),
47
- `import galbe from '${relative(buildPath, indexPath)}';
48
- ${routes.map((r, idx) => `import _${idx} from '${relative(buildPath, r.path)}'`).join(';\n')}
49
- ${routes
50
- .map(
51
- (r, idx) => `galbe.routesMetadata = {...${JSON.stringify(r.meta)}}
52
- _${idx}(galbe)`
53
- )
54
- .join(';\n')}
55
- galbe.listen();
56
- `
57
- )
58
- return resolve(buildPath, 'index.ts')
59
- }
60
-
61
- program.name('galbe').description('CLI to execute galbe utilities').version('0.1.0')
62
-
63
- program
64
- .command('dev')
65
- .description('Start a dev server running your Galbe application')
66
- .argument('<string>', 'filename')
67
- .option('-p, --port <number>', 'port number', '')
68
- .option('-w, --watch', 'watch file changes', 'true')
69
- .action(async (fileName, props) => {
70
- const { port, watch } = props
71
- const devRoot = resolve(ROOT, '.galbe', 'dev')
72
- await mkdir(devRoot, { recursive: true })
73
- await Bun.write(
74
- resolve(devRoot, 'index.ts'),
75
- `import galbe from '${relative(devRoot, fileName)}';galbe.listen(${port});`
76
- )
77
- process.on('SIGINT', async () => {
78
- await rm(resolve(ROOT, '.galbe', 'dev'), { recursive: true })
79
- })
80
-
81
- await $`BUN_ENV=development bun run ${watch ? '--watch' : ''} ${resolve(devRoot, 'index.ts')}`.cwd(ROOT)
82
- })
83
-
84
- program
85
- .command('build')
86
- .description('undle your Galbe application')
87
- .argument('<string>', 'filename')
88
- .option('-o, --out <string>', 'output file/directory', '')
89
- .option('-c, --compile', 'create a standalone executable', false)
90
- .action(async (fileName, props) => {
91
- const { out, compile } = props
92
- const g: Galbe = (await import(resolve(ROOT, fileName))).default
93
- const routes = await parseRoutes(g?.config?.routes)
94
- const buildIndex = await createBuildIndex(fileName, routes)
95
-
96
- const cmds = [
97
- 'bun',
98
- 'build',
99
- buildIndex,
100
- '--target',
101
- 'bun',
102
- ...(compile ? ['--compile', '--outfile', out ? out : 'app'] : ['--outdir', out ? out : 'dist'])
103
- ].filter(c => c)
104
- Bun.spawn(cmds, {
105
- cwd: ROOT,
106
- stdout: 'inherit',
107
- async onExit() {
108
- await rm(resolve(ROOT, '.galbe', 'build', BUILD_ID), { recursive: true })
109
- }
110
- })
111
- })
13
+ dev(program.command('dev'))
14
+ build(program.command('build'))
15
+ generate(program.command('generate'))
112
16
 
113
17
  program.parse()
@@ -0,0 +1,95 @@
1
+ import { $ } from 'bun'
2
+
3
+ import { Command, Option } from 'commander'
4
+ import { resolve, relative, dirname } from 'path'
5
+ import { tmpdir } from 'os'
6
+ import { mkdir, rm } from 'fs/promises'
7
+
8
+ import { CWD, fmtVal, silentExec } from '../util'
9
+ import { Galbe } from '../../src'
10
+ import { defineRoutes } from '../../src/routes'
11
+ import { BuildConfig } from 'bun'
12
+
13
+ const createBuildIndex = async (indexPath: string, g: Galbe) => {
14
+ const buildId = crypto.randomUUID()
15
+ const buildPath = resolve(tmpdir(), buildId)
16
+ const routes = new Set<string>()
17
+ let errors: any[] = []
18
+ await defineRoutes({ routes: g?.config?.routes }, g, ({ type, error, filepath }) => {
19
+ if (!filepath) return
20
+ routes.add(filepath)
21
+ if (type === 'error') errors.push(error)
22
+ })
23
+ if (errors.length) throw errors
24
+ await mkdir(buildPath, { recursive: true })
25
+ await Bun.write(
26
+ resolve(buildPath, 'index.ts'),
27
+ `import galbe from '${relative(buildPath, indexPath)}';
28
+ ${[...routes].map((r, idx) => `import _${idx} from '${relative(buildPath, r)}'`).join(';\n')}
29
+ galbe.meta = ${JSON.stringify(g.meta)};
30
+ ${[...routes].map((_, idx) => `_${idx}(galbe)`).join(';\n')}
31
+ galbe.listen();
32
+ `
33
+ )
34
+ return resolve(buildPath, 'index.ts')
35
+ }
36
+
37
+ export default (cmd: Command) => {
38
+ cmd
39
+ .description('bundle your \x1b[1;30m\x1b[36mGalbe\x1b[0m application')
40
+ .argument('<index>', 'index file')
41
+ .addOption(new Option('-o, --out <dir>', 'output directory').default('dist/app', fmtVal('dist/app')))
42
+ .addOption(new Option('-C, --compile', 'create a standalone executable').default(false, fmtVal(false)))
43
+ .option('-c, --config <file>', 'bun js or ts config file')
44
+ .action(async (index, props) => {
45
+ const { out, compile, config } = props
46
+
47
+ const bunfig = config ? (await import(resolve(CWD, config)))?.default || {} : {}
48
+
49
+ let error = null
50
+ process.stdout.write('📦 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m app\x1b[0m')
51
+ let g: Galbe = await silentExec(async () => {
52
+ try {
53
+ const g = (await import(resolve(CWD, index))).default
54
+ return g
55
+ } catch (err) {
56
+ error = err
57
+ }
58
+ })
59
+ if (error) {
60
+ console.log(`\nerror: galbe instance import failed`)
61
+ console.log(error)
62
+ return process.exit(1)
63
+ }
64
+ let buildIndex: string = ''
65
+ try {
66
+ buildIndex = await createBuildIndex(index, g)
67
+ } catch (errors) {
68
+ console.log(`\nerror: build errors`)
69
+ for (let error of errors) console.log(error)
70
+ return process.exit(1)
71
+ }
72
+ if (!buildIndex) {
73
+ console.log(`\nerror: could not create build index`)
74
+ return process.exit(1)
75
+ }
76
+
77
+ const buildConfig: BuildConfig = {
78
+ publicPath: `${resolve(CWD, out)}/`,
79
+ ...Object.fromEntries(Object.entries(bunfig).filter(([k, v]) => v)),
80
+ entrypoints: [buildIndex],
81
+ outdir: resolve(CWD, out),
82
+ target: 'bun'
83
+ }
84
+
85
+ let bo = await Bun.build(buildConfig)
86
+ if (bo.success) process.stdout.write(' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
87
+ else {
88
+ console.log(`\nerror: build errors`)
89
+ console.log(...bo.logs)
90
+ }
91
+ if (compile) await $`bun build --compile ${resolve(CWD, out, 'index.js')} --outfile ${resolve(CWD, out, 'app')}`
92
+
93
+ await rm(dirname(buildIndex), { recursive: true })
94
+ })
95
+ }
@@ -0,0 +1,53 @@
1
+ import { $ } from 'bun'
2
+ import { Command, Option } from 'commander'
3
+ import { resolve } from 'path'
4
+
5
+ import { CWD, fmtInterval, fmtVal, instanciateRoutes, watchDir } from '../util'
6
+ import { Galbe } from '../../src'
7
+
8
+ const defaultPort = 3000
9
+
10
+ export default (cmd: Command) => {
11
+ cmd
12
+ .description('start a dev server running your \x1b[1;30m\x1b[36mGalbe\x1b[0m application')
13
+ .argument('<index>', 'index file')
14
+ .addOption(
15
+ new Option('-p, --port <number>', `port number ${fmtInterval(1, 65535)}`)
16
+ .argParser(v => {
17
+ if (parseInt(v) >= 1 && parseInt(v) <= 65535) return v
18
+ console.log(`error: port range must be between ${fmtInterval(1, 65535)}`)
19
+ process.exit(1)
20
+ })
21
+ .default(null, fmtVal(defaultPort))
22
+ )
23
+ .addOption(new Option('-w, --watch', 'watch file changes').default(false, fmtVal(false)))
24
+ .addOption(new Option('-nc, --noclear', "don't clear on file changes").default(false, fmtVal(false)))
25
+ .action(async (index, props) => {
26
+ const { port, watch, noclear } = props
27
+ const clear = !noclear
28
+ const indexPath = resolve(CWD, index)
29
+ let g: Galbe
30
+
31
+ Bun.env.BUN_ENV = 'development'
32
+
33
+ if (watch) {
34
+ await watchDir(
35
+ CWD,
36
+ async () => {
37
+ g.stop()
38
+ if (clear) await $`clear`
39
+ Loader.registry.clear()
40
+ g = (await import(indexPath)).default
41
+ await instanciateRoutes(g)
42
+ await g.listen(port)
43
+ },
44
+ { ignore: /node_modules/ }
45
+ )
46
+ }
47
+
48
+ if (watch && clear) await $`clear`
49
+ g = (await import(indexPath)).default
50
+ await instanciateRoutes(g)
51
+ await g.listen(port)
52
+ })
53
+ }
@@ -0,0 +1,192 @@
1
+ import { $ } from 'bun'
2
+ import { devNull } from 'os'
3
+ import { Script, createContext } from 'vm'
4
+ import { Command, Option } from 'commander'
5
+ import { resolve, extname } from 'path'
6
+ import { rm } from 'fs/promises'
7
+ import { transformSync } from '@swc/core'
8
+ import { CWD, fmtList, instanciateRoutes, silentExec } from '../../util'
9
+ import { $T, Galbe, GalbeCLICommand, Method, Route } from '../../../src'
10
+ import { walkRoutes } from '../../../src/util'
11
+ import { schemaToTypeStr, Optional, STSchema } from '../../../src/schema'
12
+
13
+ const clientTargets = ['ts', 'js', 'cli']
14
+
15
+ export default (cmd: Command) => {
16
+ cmd
17
+ .description('generate a \x1b[1;30m\x1b[36mGalbe\x1b[0m client')
18
+ .argument('<index>', 'index file')
19
+ .addOption(
20
+ new Option('-o, --out <file>', 'output file').default(
21
+ null,
22
+ fmtList(['dist/client.ts', 'dist/client.js', 'dist/cli'])
23
+ )
24
+ )
25
+ .addOption(
26
+ new Option('-t, --target <target>', `build target ${fmtList(clientTargets)}`).argParser(v => {
27
+ if (clientTargets.includes(v)) return v
28
+ console.log(`error: target must be one of ${fmtList(clientTargets)}`)
29
+ process.exit(1)
30
+ })
31
+ )
32
+ .action(async (index, props) => {
33
+ let { target, out } = props
34
+ if (!target) target = clientTargets.includes(extname(index)?.slice(1)) ? extname(index)?.slice(1) : 'ts'
35
+ if (!out) out = { ts: 'dist/client.ts', js: 'dist/client.js', cli: 'dist/cli' }[target]
36
+ let pckg: any = {}
37
+ try {
38
+ pckg = await Bun.file(resolve(CWD, 'package.json')).json()
39
+ } catch (e) {}
40
+
41
+ let error = null
42
+ process.stdout.write('💻 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m client\x1b[0m')
43
+ let g: Galbe = await silentExec(async () => {
44
+ try {
45
+ const g = (await import(resolve(CWD, index))).default
46
+ await instanciateRoutes(g)
47
+ await g.init()
48
+ return g
49
+ } catch (err) {
50
+ error = err
51
+ }
52
+ })
53
+ if (error) {
54
+ console.log(`\nerror: galbe instance import failed`)
55
+ console.log(error)
56
+ return process.exit(1)
57
+ }
58
+
59
+ const routes: Record<Method, Route[]> = {
60
+ get: [],
61
+ post: [],
62
+ put: [],
63
+ patch: [],
64
+ delete: [],
65
+ options: [],
66
+ head: []
67
+ }
68
+ let commands: GalbeCLICommand[] = []
69
+ const metaRoutes = g.meta?.reduce(
70
+ (routes, c) => ({ ...routes, ...c.routes }),
71
+ {} as Record<string, Record<string, Record<string, any>>>
72
+ )
73
+
74
+ walkRoutes(g.router.routes, r => {
75
+ let meta = metaRoutes?.[r.path]?.[r.method]
76
+ let route = {
77
+ ...r,
78
+ ...(meta?.operationId ? { alias: meta?.operationId } : {}),
79
+ pathT: r.path.replaceAll(/:([^\/]+)/g, '${$1}'),
80
+ params:
81
+ Object.fromEntries(
82
+ [...r.path.matchAll(/:([^\/]+)/g)]?.map(m => [
83
+ m?.[1],
84
+ {
85
+ ...(r.schema?.params?.[m?.[1]] ? { type: schemaToTypeStr(r.schema.params[m[1]]) } : {})
86
+ }
87
+ ])
88
+ ) || {},
89
+ schemas: {
90
+ ...(r.schema.headers ? { headers: schemaToTypeStr($T.object(r.schema.headers)) } : {}),
91
+ ...(r.schema.query ? { query: schemaToTypeStr($T.object(r.schema.query)) } : {}),
92
+ ...(r.schema.body ? { body: schemaToTypeStr(r.schema.body) } : {}),
93
+ ...(r.schema.response
94
+ ? {
95
+ response: Object.fromEntries(
96
+ Object.entries(r.schema.response).map(([k, v]) => [k === 'default' ? 200 : k, schemaToTypeStr(v)])
97
+ )
98
+ }
99
+ : {})
100
+ }
101
+ }
102
+ routes[r.method.toLocaleLowerCase()].push(route)
103
+ if (target === 'cli' && meta?.operationId)
104
+ commands.push({
105
+ name: meta.operationId,
106
+ description: meta.head,
107
+ route,
108
+ arguments:
109
+ Object.entries((r.schema?.params || {}) as Record<string, STSchema>)?.map(([k, p]) => {
110
+ let type = schemaToTypeStr({ ...p, [Optional]: false })
111
+ return {
112
+ name: k,
113
+ type: type === 'boolean' ? '' : `<${type}>`,
114
+ description: p?.description || ''
115
+ }
116
+ }) || [],
117
+ options:
118
+ Object.entries((r.schema?.query || {}) as Record<string, STSchema>)?.map(([k, o]) => {
119
+ let type = schemaToTypeStr({ ...o, [Optional]: false })
120
+ return {
121
+ name: k,
122
+ short: k[0],
123
+ type: type === 'boolean' ? '' : `<${type}>`,
124
+ description: o?.description || '',
125
+ default: o.default
126
+ }
127
+ }) || []
128
+ })
129
+ })
130
+
131
+ if (target === 'js' || target === 'ts') {
132
+ const file = await Bun.file(resolve(import.meta.dir, '..', '..', 'res', 'client.template.ts')).text()
133
+ let filled = file.replaceAll(/\/\*\%([\s\S]*?)\%\*\//g, (_match, p) => {
134
+ let idt = p.match(/^\n*([ \t]*)/, p)?.[1] || ''
135
+ const script = new Script(p)
136
+ const sandbox = {
137
+ console,
138
+ version: pckg?.version || '0.1.0',
139
+ routes
140
+ }
141
+ createContext(sandbox)
142
+ let res = script.runInNewContext(sandbox)
143
+ if (typeof res === 'string') res = res.split('\n')
144
+ if (Array.isArray(res)) return res.map((s, i) => (i === 0 ? s : `${idt}${s}`)).join('\n')
145
+ return res ?? ''
146
+ })
147
+
148
+ if (target === 'js') {
149
+ filled = transformSync(filled, {
150
+ jsc: {
151
+ parser: {
152
+ syntax: 'typescript'
153
+ },
154
+ preserveAllComments: true,
155
+ target: 'esnext'
156
+ }
157
+ }).code
158
+ }
159
+
160
+ await Bun.write(out, filled)
161
+ } else if (target === 'cli') {
162
+ const file = await Bun.file(resolve(import.meta.dir, '..', '..', 'res', 'cli.template.js')).text()
163
+
164
+ // Plugin CLI hook
165
+ if (commands) for (let p of g.plugins) if (p.cli) await p.cli(commands)
166
+
167
+ let filled = file.replaceAll(/\/\*\%([\s\S]*?)\%\*\//g, (_match, p) => {
168
+ let idt = p.match(/^\n*([ \t]*)/, p)?.[1] || ''
169
+ const script = new Script(p)
170
+ const sandbox = {
171
+ console,
172
+ name: pckg?.name || 'Galbe app CLI',
173
+ description: pckg?.description || '',
174
+ version: pckg?.version || '0.1.0',
175
+ commands
176
+ }
177
+ createContext(sandbox)
178
+ let res = script.runInNewContext(sandbox)
179
+ if (typeof res === 'string') res = res.split('\n')
180
+ if (Array.isArray(res)) return res.map((s, i) => (i === 0 ? s : `${idt}${s}`)).join('\n')
181
+ return res ?? ''
182
+ })
183
+ const buildId = crypto.randomUUID()
184
+ const buildPath = resolve(CWD, '.galbe', 'client', `${buildId}.js`)
185
+ await Bun.write(buildPath, filled)
186
+ await $`bun build --compile ${buildPath} --outfile ${resolve(CWD, out)} > ${devNull} && printf "\u200B"`
187
+ await rm(resolve(CWD, '.galbe'), { recursive: true })
188
+ }
189
+
190
+ process.stdout.write(' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
191
+ })
192
+ }