galbe 0.13.0 → 0.14.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 CHANGED
@@ -12,8 +12,21 @@ Galbe is a fast, lightweight and highly customizable JavaScript web framework ba
12
12
  > [!IMPORTANT]
13
13
  > Galbe is currently under active development and not guaranteed to be stable. Future releases may potentially introduce breaking changes.
14
14
 
15
+ ## Features
16
+
17
+ - **Fast**: Built on top of Bun, Galbe is designed for speed.
18
+ - **Lightweight**: Minimal core with a plugin system to add only what you need.
19
+ - **Type-safe**: Built with TypeScript for a great developer experience, with runtime validation using Galbe Schemas.
20
+ - **Plugins**: Extensible architecture to easily add functionality.
21
+
22
+ ## Philosophy
23
+
24
+ Galbe aims to provide a developer-friendly experience without compromising on performance. It embraces the "use the platform" mentality, leveraging modern web standards and the capabilities of the Bun runtime.
25
+
15
26
  ## Getting started
16
27
 
28
+ Create a new Galbe app:
29
+
17
30
  ```bash
18
31
  bun create galbe app
19
32
  cd app
@@ -27,6 +40,10 @@ install it globally using `bun install -g galbe`.
27
40
 
28
41
  The detailed documentation is available at [galbe.dev](https://galbe.dev).
29
42
 
43
+ ## Community
44
+
45
+ - [GitHub Discussions](https://github.com/pierre-cm/galbe/discussions)
46
+
30
47
  ## Contributing
31
48
 
32
- Please refer to the [Contributing Guide](https://github.com/pierre-cm/galbe/blob/main/docs/CONTRIBUTING.md) to start contributing to Galbe.
49
+ Please refer to the [Contributing Guide](https://github.com/pierre-cm/galbe/blob/main/docs/community/CONTRIBUTING.md) to start contributing to Galbe.
@@ -9,10 +9,10 @@ import { CWD, fmtVal, silentExec } from '../util'
9
9
  import { Galbe } from '../../src'
10
10
  import { defineRoutes, GalbeProxy } from '../../src/routes'
11
11
  import { BuildConfig } from 'bun'
12
- import { existsSync } from 'fs'
12
+ import { cpSync, existsSync } from 'fs'
13
13
  import { softMerge } from '../../src/util'
14
14
 
15
- const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string) => {
15
+ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string, outPath: string) => {
16
16
  const buildPath = resolve(tmpdir(), buildId)
17
17
  const indexDir = dirname(indexPath)
18
18
 
@@ -34,6 +34,13 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string) =>
34
34
  await proxy.init()
35
35
 
36
36
  if (errors.length) throw errors
37
+
38
+ // Copy static assets next to the bundle so the runtime can resolve them
39
+ // via static-${BUILD_ID}/<target> at request time.
40
+ for (const { target } of proxy._staticTargets) {
41
+ cpSync(target, `${outPath}/static-${buildId}/${target}`, { recursive: true, dereference: true })
42
+ }
43
+
37
44
  await mkdir(buildPath, { recursive: true })
38
45
 
39
46
  let buildIndex =
@@ -76,7 +83,6 @@ export default (cmd: Command) => {
76
83
  }
77
84
 
78
85
  Bun.env.GALBE_BUILD = buildID
79
- Bun.env.GALBE_BUILD_OUT = outPath
80
86
 
81
87
  const bunfig = config ? (await import(resolve(CWD, config)))?.default || {} : {}
82
88
 
@@ -101,7 +107,7 @@ export default (cmd: Command) => {
101
107
  }
102
108
  let buildIndex: string = ''
103
109
  try {
104
- buildIndex = await createBuildIndex(index, g, buildID)
110
+ buildIndex = await createBuildIndex(index, g, buildID, outPath)
105
111
  } catch (errors) {
106
112
  console.log(`\nerror: build errors`)
107
113
  for (let error of errors) console.log(error)
@@ -0,0 +1,156 @@
1
+ import { Command, Option } from 'commander'
2
+ import { resolve } from 'path'
3
+ import { CWD, fmtList, instanciateRoutes, silentExec, abbreviateVar } from '../../../util'
4
+ import { $T, Galbe, GalbeCLICommand, GalbeCLIOptions } from '../../../../src'
5
+ import { walkRoutes } from '../../../../src/util'
6
+ import { schemaToTypeStr, Optional, STSchema } from '../../../../src/schema'
7
+
8
+ const cliTargets = ['cac']
9
+ const cliModes = ['standalone', 'module']
10
+
11
+ export default (cmd: Command) => {
12
+ cmd
13
+ .description('generate a \x1b[1;30m\x1b[36mGalbe\x1b[0m CLI')
14
+ .argument('<index>', 'index file (.ts or .js)')
15
+ .addOption(new Option('-o, --out <file>', 'output file'))
16
+ .addOption(
17
+ new Option('-t, --target <target>', `CLI target ${fmtList(cliTargets)}`).default('cac').argParser(v => {
18
+ if (cliTargets.includes(v)) return v
19
+ console.log(`error: target must be one of ${fmtList(cliTargets)}`)
20
+ process.exit(1)
21
+ })
22
+ )
23
+ .addOption(
24
+ new Option('-m, --mode <mode>', `output mode ${fmtList(cliModes)}`).default('standalone').argParser(v => {
25
+ if (cliModes.includes(v)) return v
26
+ console.log(`error: mode must be one of ${fmtList(cliModes)}`)
27
+ process.exit(1)
28
+ })
29
+ )
30
+ .addOption(new Option('-c, --config <file>', 'config file (.ts or .js)'))
31
+ .action(async (index, props) => {
32
+ let { target, mode, out, config } = props
33
+
34
+ if (!out) out = mode === 'module' ? 'dist/cli.ts' : 'dist/cli'
35
+
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
+ Bun.write(Bun.stdout, '💻 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m CLI\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 metaRoutes = g.meta?.reduce(
60
+ (routes, c) => ({ ...routes, ...c.routes }),
61
+ {} as Record<string, Record<string, Record<string, any>>>
62
+ )
63
+
64
+ let commands: GalbeCLICommand[] = []
65
+
66
+ walkRoutes(g.router.routes, r => {
67
+ let meta = metaRoutes?.[r.path]?.[r.method]
68
+ let [_, summary, description] = meta?.head?.match(/^([^\n]*)\n\n(.*)/) || []
69
+ if (!summary) description = meta?.head
70
+
71
+ const name =
72
+ meta?.operationId ||
73
+ `${r.method}-${r.path
74
+ .replace(/\//g, '-')
75
+ .replace(/:/g, '')
76
+ .replace(/^-/, '')
77
+ .replace(/-+/g, '-')
78
+ .replace(/-$/, '')}`
79
+
80
+ const pathT = r.path.replaceAll(/:([^\/]+)/g, '${$1}')
81
+
82
+ const params = Object.fromEntries(
83
+ [...r.path.matchAll(/:([^\/]+)/g)]?.map(m => [
84
+ m?.[1],
85
+ r.schema?.params?.[m?.[1]]
86
+ ? {
87
+ type: schemaToTypeStr(r.schema.params[m[1]]),
88
+ ...(r.schema.params[m[1]]?.description
89
+ ? { description: r.schema.params[m[1]].description as string }
90
+ : {}),
91
+ }
92
+ : { type: 'string' },
93
+ ])
94
+ )
95
+
96
+ commands.push({
97
+ name,
98
+ tags: meta?.tags ? (Array.isArray(meta.tags) ? meta.tags : [meta.tags]) : [],
99
+ description: summary || description,
100
+ route: r,
101
+ pathT,
102
+ arguments: Object.entries(
103
+ (params || {}) as Record<string, { type: string; description?: string }>
104
+ ).map(([k, p]) => ({
105
+ name: k,
106
+ type: p.type === 'boolean' ? '' : `<${p.type}>`,
107
+ description: p?.description || '',
108
+ })),
109
+ options: Object.entries((r.schema?.query || {}) as Record<string, STSchema>).map(([k, o]) => {
110
+ const type = schemaToTypeStr({ ...o, [Optional]: false })
111
+ return {
112
+ name: k,
113
+ short: abbreviateVar(k),
114
+ type: type === 'boolean' ? '' : `<${type}>`,
115
+ description: (o?.description as string) || '',
116
+ default: o.default,
117
+ }
118
+ }),
119
+ })
120
+ })
121
+
122
+ // Plugin CLI hooks run first
123
+ for (let p of g.plugins) {
124
+ if (p.cli) {
125
+ const result = await p.cli(commands)
126
+ if (Array.isArray(result)) commands = result
127
+ }
128
+ }
129
+
130
+ // Load user config
131
+ let userTransform: ((commands: GalbeCLICommand[]) => GalbeCLICommand[]) | undefined
132
+ let userOptions: GalbeCLIOptions | undefined
133
+ if (config) {
134
+ try {
135
+ const configModule = await import(resolve(CWD, config))
136
+ userTransform = configModule.transform
137
+ userOptions = configModule.options
138
+ } catch (err) {
139
+ console.log(`\nerror: config file import failed`)
140
+ console.log(err)
141
+ return process.exit(1)
142
+ }
143
+ }
144
+
145
+ // User transform runs last (user wins)
146
+ if (userTransform) commands = userTransform(commands)
147
+
148
+ if (target === 'cac') {
149
+ const { generate } = await import('./targets/cac')
150
+ await generate({ commands, mode, out, pckg, options: userOptions })
151
+ }
152
+
153
+ Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
154
+ process.exit(0)
155
+ })
156
+ }