galbe 0.14.0 → 0.15.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.
- package/bin/commands/build.ts +10 -8
- package/bin/commands/generate/cli/index.ts +22 -15
- package/bin/commands/generate/cli/targets/cac.ts +189 -64
- package/bin/commands/generate/client.ts +520 -110
- package/bin/commands/generate/code.ts +11 -3
- package/bin/commands/generate/model.ts +2 -2
- package/bin/res/client.runtime.ts +184 -0
- package/bin/res/client.template.ts +1 -1
- package/package.json +2 -3
- package/src/extras.ts +1 -1
- package/src/index.ts +10 -10
- package/src/server.ts +1 -1
- package/src/types.ts +28 -4
- package/scripts/hooks/prepare-commit-msg +0 -10
- package/scripts/postinstall.ts +0 -9
package/bin/commands/build.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { $ } from 'bun'
|
|
|
3
3
|
import { Command, Option } from 'commander'
|
|
4
4
|
import { resolve, relative, dirname } from 'path'
|
|
5
5
|
import { tmpdir } from 'os'
|
|
6
|
-
import { mkdir, rm
|
|
6
|
+
import { mkdir, rm } from 'fs/promises'
|
|
7
7
|
|
|
8
8
|
import { CWD, fmtVal, silentExec } from '../util'
|
|
9
9
|
import { Galbe } from '../../src'
|
|
10
10
|
import { defineRoutes, GalbeProxy } from '../../src/routes'
|
|
11
|
-
import { BuildConfig } from 'bun'
|
|
11
|
+
import type { BuildConfig } from 'bun'
|
|
12
12
|
import { cpSync, existsSync } from 'fs'
|
|
13
13
|
import { softMerge } from '../../src/util'
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string, ou
|
|
|
20
20
|
if (existsSync(`${indexDir}/galbe.config.ts`)) configPath = `${indexDir}/galbe.config.ts`
|
|
21
21
|
else if (existsSync(`${indexDir}/galbe.config.js`)) configPath = `${indexDir}/galbe.config.js`
|
|
22
22
|
|
|
23
|
-
const routes = new Map<string, { filepath: string
|
|
23
|
+
const routes = new Map<string, { filepath: string; static?: { path: string; root: string } }>()
|
|
24
24
|
let errors: any[] = []
|
|
25
25
|
// Create GalbeProxy here
|
|
26
26
|
// use it to define routes
|
|
@@ -45,8 +45,10 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string, ou
|
|
|
45
45
|
|
|
46
46
|
let buildIndex =
|
|
47
47
|
`import galbe from '${relative(buildPath, indexPath)}';\n` +
|
|
48
|
-
(configPath ? `import config from '${relative(buildPath, configPath)}';\n` : '') +
|
|
49
|
-
(configPath
|
|
48
|
+
(configPath ? `import config from '${relative(buildPath, configPath)}';\n` : '') +
|
|
49
|
+
(configPath
|
|
50
|
+
? `import {softMerge} from '${relative(buildPath, `${indexDir}/node_modules/galbe/src/util`)}';\n`
|
|
51
|
+
: '') +
|
|
50
52
|
(configPath ? `let conf = galbe.config;\ngalbe.config = softMerge(config, conf)\n` : '') +
|
|
51
53
|
`${[...routes.values()].map((r, idx) => `import _${idx} from '${relative(buildPath, r.filepath)}'`).join(';\n')}\n` +
|
|
52
54
|
`Bun.env.BUN_ENV = 'production';\n` +
|
|
@@ -86,7 +88,7 @@ export default (cmd: Command) => {
|
|
|
86
88
|
|
|
87
89
|
const bunfig = config ? (await import(resolve(CWD, config)))?.default || {} : {}
|
|
88
90
|
|
|
89
|
-
if
|
|
91
|
+
if (existsSync(outPath)) await rm(outPath, { recursive: true })
|
|
90
92
|
|
|
91
93
|
let error = null
|
|
92
94
|
Bun.write(Bun.stdout, '📦 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m app\x1b[0m')
|
|
@@ -110,7 +112,7 @@ export default (cmd: Command) => {
|
|
|
110
112
|
buildIndex = await createBuildIndex(index, g, buildID, outPath)
|
|
111
113
|
} catch (errors) {
|
|
112
114
|
console.log(`\nerror: build errors`)
|
|
113
|
-
for (let error of errors) console.log(error)
|
|
115
|
+
for (let error of errors as any[]) console.log(error)
|
|
114
116
|
return process.exit(1)
|
|
115
117
|
}
|
|
116
118
|
if (!buildIndex) {
|
|
@@ -134,7 +136,7 @@ export default (cmd: Command) => {
|
|
|
134
136
|
console.log(...bo.logs)
|
|
135
137
|
}
|
|
136
138
|
if (compile) {
|
|
137
|
-
await $`bun build --compile ${resolve(CWD, out, 'index.js')} --outfile ${outPath}/bin`
|
|
139
|
+
await $`bun build --compile --minify --sourcemap --bytecode ${resolve(CWD, out, 'index.js')} --outfile ${outPath}/bin`
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
await rm(dirname(buildIndex), { recursive: true })
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command, Option } from 'commander'
|
|
2
2
|
import { resolve } from 'path'
|
|
3
3
|
import { CWD, fmtList, instanciateRoutes, silentExec, abbreviateVar } from '../../../util'
|
|
4
|
-
import { $T, Galbe, GalbeCLICommand, GalbeCLIOptions } from '../../../../src'
|
|
4
|
+
import { $T, Galbe, type GalbeCLICommand, type GalbeCLIOptions } from '../../../../src'
|
|
5
5
|
import { walkRoutes } from '../../../../src/util'
|
|
6
|
-
import { schemaToTypeStr, Optional, STSchema } from '../../../../src/schema'
|
|
6
|
+
import { schemaToTypeStr, Optional, type STSchema } from '../../../../src/schema'
|
|
7
7
|
|
|
8
8
|
const cliTargets = ['cac']
|
|
9
9
|
const cliModes = ['standalone', 'module']
|
|
@@ -82,11 +82,11 @@ export default (cmd: Command) => {
|
|
|
82
82
|
const params = Object.fromEntries(
|
|
83
83
|
[...r.path.matchAll(/:([^\/]+)/g)]?.map(m => [
|
|
84
84
|
m?.[1],
|
|
85
|
-
r.schema?.params?.[m
|
|
85
|
+
(r.schema?.params as Record<string, STSchema>)?.[m[1]]
|
|
86
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 }
|
|
87
|
+
type: schemaToTypeStr((r.schema.params as Record<string, STSchema>)[m[1]]),
|
|
88
|
+
...((r.schema.params as Record<string, STSchema>)[m[1]]?.description
|
|
89
|
+
? { description: (r.schema.params as Record<string, STSchema>)[m[1]].description as string }
|
|
90
90
|
: {}),
|
|
91
91
|
}
|
|
92
92
|
: { type: 'string' },
|
|
@@ -99,13 +99,13 @@ export default (cmd: Command) => {
|
|
|
99
99
|
description: summary || description,
|
|
100
100
|
route: r,
|
|
101
101
|
pathT,
|
|
102
|
-
arguments: Object.entries(
|
|
103
|
-
(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
arguments: Object.entries((params || {}) as Record<string, { type: string; description?: string }>).map(
|
|
103
|
+
([k, p]) => ({
|
|
104
|
+
name: k,
|
|
105
|
+
type: p.type === 'boolean' ? '' : `<${p.type}>`,
|
|
106
|
+
description: p?.description || '',
|
|
107
|
+
})
|
|
108
|
+
),
|
|
109
109
|
options: Object.entries((r.schema?.query || {}) as Record<string, STSchema>).map(([k, o]) => {
|
|
110
110
|
const type = schemaToTypeStr({ ...o, [Optional]: false })
|
|
111
111
|
return {
|
|
@@ -147,10 +147,17 @@ export default (cmd: Command) => {
|
|
|
147
147
|
|
|
148
148
|
if (target === 'cac') {
|
|
149
149
|
const { generate } = await import('./targets/cac')
|
|
150
|
-
await generate({
|
|
150
|
+
await generate({
|
|
151
|
+
commands,
|
|
152
|
+
mode,
|
|
153
|
+
out,
|
|
154
|
+
pckg,
|
|
155
|
+
options: userOptions,
|
|
156
|
+
configPath: config ? resolve(CWD, config) : undefined,
|
|
157
|
+
})
|
|
151
158
|
}
|
|
152
159
|
|
|
153
|
-
Bun.write(Bun.stdout,
|
|
160
|
+
Bun.write(Bun.stdout, ` : \x1b[1;30m\x1b[32mdone\x1b[0m\nCLI ${mode} generated at ${out}\n`)
|
|
154
161
|
process.exit(0)
|
|
155
162
|
})
|
|
156
163
|
}
|
|
@@ -11,6 +11,7 @@ export interface GenerateOptions {
|
|
|
11
11
|
out: string
|
|
12
12
|
pckg: any
|
|
13
13
|
options?: GalbeCLIOptions
|
|
14
|
+
configPath?: string
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function generate(opts: GenerateOptions): Promise<void> {
|
|
@@ -19,7 +20,61 @@ export async function generate(opts: GenerateOptions): Promise<void> {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async function buildStandalone(opts: GenerateOptions): Promise<void> {
|
|
22
|
-
|
|
23
|
+
// Bundle config as an IIFE to avoid importing the full config module at startup
|
|
24
|
+
// (a direct import would run all module-level side effects before the CLI is set up).
|
|
25
|
+
let inlinedConfigCode: string | undefined
|
|
26
|
+
if (opts.configPath) {
|
|
27
|
+
const tmpDir = await mkdtemp(resolve(tmpdir(), 'galbe-cli-cfg-'))
|
|
28
|
+
try {
|
|
29
|
+
const allExports = [
|
|
30
|
+
'__galbe_baseUrl',
|
|
31
|
+
'__galbe_headers',
|
|
32
|
+
'__galbe_requestInterceptor',
|
|
33
|
+
'__galbe_responseFormatter',
|
|
34
|
+
]
|
|
35
|
+
const entry = [
|
|
36
|
+
`import { options as __cfg } from ${JSON.stringify(opts.configPath)}`,
|
|
37
|
+
`const __galbe_baseUrl = __cfg?.baseUrl`,
|
|
38
|
+
`const __galbe_headers = __cfg?.headers`,
|
|
39
|
+
`const __galbe_requestInterceptor = __cfg?.requestInterceptor`,
|
|
40
|
+
`const __galbe_responseFormatter = __cfg?.responseFormatter`,
|
|
41
|
+
`export { ${allExports.join(', ')} }`,
|
|
42
|
+
].join('\n')
|
|
43
|
+
const entryPath = resolve(tmpDir, 'entry.ts')
|
|
44
|
+
const outPath = resolve(tmpDir, 'bundled.js')
|
|
45
|
+
await Bun.write(entryPath, entry)
|
|
46
|
+
await $`bun build --bundle --format esm ${entryPath} --outfile ${outPath}`.quiet()
|
|
47
|
+
const bundled = await Bun.file(outPath).text()
|
|
48
|
+
|
|
49
|
+
const exportMatch = bundled.match(/export\s*\{([^}]+)\}/)
|
|
50
|
+
const codeBody = exportMatch ? bundled.replace(/export\s*\{[^}]+\}\s*;?\s*/g, '').trim() : bundled.trim()
|
|
51
|
+
|
|
52
|
+
let returnExpr = `{ ${allExports.join(', ')} }`
|
|
53
|
+
if (exportMatch) {
|
|
54
|
+
const entries = exportMatch[1]
|
|
55
|
+
.split(',')
|
|
56
|
+
.map(e => {
|
|
57
|
+
const parts = e.trim().split(/\s+as\s+/)
|
|
58
|
+
return { local: parts[0].trim(), exported: (parts[1] ?? parts[0]).trim() }
|
|
59
|
+
})
|
|
60
|
+
.filter(e => allExports.includes(e.exported))
|
|
61
|
+
if (entries.length) {
|
|
62
|
+
const props = entries.map(e => (e.local === e.exported ? e.exported : `${e.exported}: ${e.local}`)).join(', ')
|
|
63
|
+
returnExpr = `{ ${props} }`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const indented = codeBody
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map(l => ' ' + l)
|
|
70
|
+
.join('\n')
|
|
71
|
+
inlinedConfigCode = `const { ${allExports.join(', ')} } = (() => {\n${indented}\n return ${returnExpr}\n})()`
|
|
72
|
+
} finally {
|
|
73
|
+
await rm(tmpDir, { recursive: true })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const code = generateSource(opts.commands, 'standalone', opts.pckg, opts.options, undefined, inlinedConfigCode)
|
|
23
78
|
const buildDir = await mkdtemp(resolve(tmpdir(), 'galbe-cli-'))
|
|
24
79
|
try {
|
|
25
80
|
await Bun.write(resolve(buildDir, 'package.json'), JSON.stringify({ dependencies: { cac: 'latest' } }))
|
|
@@ -33,7 +88,56 @@ async function buildStandalone(opts: GenerateOptions): Promise<void> {
|
|
|
33
88
|
}
|
|
34
89
|
|
|
35
90
|
async function buildModule(opts: GenerateOptions): Promise<void> {
|
|
36
|
-
|
|
91
|
+
// For module mode, the output must be self-contained (only cac as external dep).
|
|
92
|
+
// Bundle the config and its transitive deps into an IIFE snippet that is embedded
|
|
93
|
+
// directly in the generated file.
|
|
94
|
+
let inlinedConfigCode: string | undefined
|
|
95
|
+
if (opts.configPath) {
|
|
96
|
+
const tmpDir = await mkdtemp(resolve(tmpdir(), 'galbe-cli-cfg-'))
|
|
97
|
+
try {
|
|
98
|
+
const entry = [
|
|
99
|
+
`import { options as __cfg } from ${JSON.stringify(opts.configPath)}`,
|
|
100
|
+
`const __galbe_requestInterceptor = __cfg?.requestInterceptor`,
|
|
101
|
+
`const __galbe_responseFormatter = __cfg?.responseFormatter`,
|
|
102
|
+
`export { __galbe_requestInterceptor, __galbe_responseFormatter }`,
|
|
103
|
+
].join('\n')
|
|
104
|
+
const entryPath = resolve(tmpDir, 'entry.ts')
|
|
105
|
+
const outPath = resolve(tmpDir, 'bundled.js')
|
|
106
|
+
await Bun.write(entryPath, entry)
|
|
107
|
+
await $`bun build --bundle --format esm ${entryPath} --outfile ${outPath}`.quiet()
|
|
108
|
+
const bundled = await Bun.file(outPath).text()
|
|
109
|
+
|
|
110
|
+
// Parse "export { localA as exportedA, ... }" and transform to a return expression
|
|
111
|
+
// so that renaming by the bundler doesn't break the IIFE interface.
|
|
112
|
+
const exportMatch = bundled.match(/export\s*\{([^}]+)\}/)
|
|
113
|
+
const codeBody = exportMatch ? bundled.replace(/export\s*\{[^}]+\}\s*;?\s*/g, '').trim() : bundled.trim()
|
|
114
|
+
|
|
115
|
+
let returnExpr = '{ __galbe_requestInterceptor, __galbe_responseFormatter }'
|
|
116
|
+
if (exportMatch) {
|
|
117
|
+
const entries = exportMatch[1]
|
|
118
|
+
.split(',')
|
|
119
|
+
.map(e => {
|
|
120
|
+
const parts = e.trim().split(/\s+as\s+/)
|
|
121
|
+
return { local: parts[0].trim(), exported: (parts[1] ?? parts[0]).trim() }
|
|
122
|
+
})
|
|
123
|
+
.filter(e => e.exported === '__galbe_requestInterceptor' || e.exported === '__galbe_responseFormatter')
|
|
124
|
+
if (entries.length) {
|
|
125
|
+
const props = entries.map(e => (e.local === e.exported ? e.exported : `${e.exported}: ${e.local}`)).join(', ')
|
|
126
|
+
returnExpr = `{ ${props} }`
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const indented = codeBody
|
|
131
|
+
.split('\n')
|
|
132
|
+
.map(l => ' ' + l)
|
|
133
|
+
.join('\n')
|
|
134
|
+
inlinedConfigCode = `const { __galbe_requestInterceptor, __galbe_responseFormatter } = (() => {\n${indented}\n return ${returnExpr}\n})()`
|
|
135
|
+
} finally {
|
|
136
|
+
await rm(tmpDir, { recursive: true })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const code = generateSource(opts.commands, 'module', opts.pckg, opts.options, undefined, inlinedConfigCode)
|
|
37
141
|
await Bun.write(resolve(CWD, opts.out), code)
|
|
38
142
|
}
|
|
39
143
|
|
|
@@ -65,7 +169,7 @@ function generateSingleCommand(c: GalbeCLICommand, cliVar: string, includeTag: b
|
|
|
65
169
|
const builtinShorts = new Set(['H', 'Q', 'b', 'B'])
|
|
66
170
|
const hide = new Set(c.hideOptions ?? [])
|
|
67
171
|
const tagPrefix = includeTag && c.tags.length > 0 ? c.tags.map(t => t.toLowerCase()).join(' ') + ' ' : ''
|
|
68
|
-
const pathArgs = (c.arguments || []).map(a => `
|
|
172
|
+
const pathArgs = (c.arguments || []).map(a => ` ${a.type.replace(/\w+/, a.name)}`).join('')
|
|
69
173
|
const cmdStr = `${tagPrefix}${c.name}${pathArgs}`
|
|
70
174
|
const desc = JSON.stringify(c.description || '')
|
|
71
175
|
|
|
@@ -73,7 +177,7 @@ function generateSingleCommand(c: GalbeCLICommand, cliVar: string, includeTag: b
|
|
|
73
177
|
.map(o => {
|
|
74
178
|
const cleanName = sanitizeOptionName(o.name)
|
|
75
179
|
const short = o.short && !builtinShorts.has(o.short) ? `-${o.short}, ` : ''
|
|
76
|
-
const optType = o.type
|
|
180
|
+
const optType = o.type != null ? o.type : '[string]'
|
|
77
181
|
const defVal = o.default !== undefined ? `, { default: ${serializeValue(o.default)} }` : ''
|
|
78
182
|
return ` .option('${short}--${cleanName} ${optType}', ${JSON.stringify(o.description || o.name)}${defVal})`
|
|
79
183
|
})
|
|
@@ -91,9 +195,7 @@ function generateSingleCommand(c: GalbeCLICommand, cliVar: string, includeTag: b
|
|
|
91
195
|
const method = c.route.method.toUpperCase()
|
|
92
196
|
const customAction = c.action ? `await (${c.action.toString()})(options as any)\n ` : ''
|
|
93
197
|
|
|
94
|
-
const argsObj = (c.arguments || []).length
|
|
95
|
-
? `{ ${(c.arguments || []).map(a => a.name).join(', ')} }`
|
|
96
|
-
: '{}'
|
|
198
|
+
const argsObj = (c.arguments || []).length ? `{ ${(c.arguments || []).map(a => a.name).join(', ')} }` : '{}'
|
|
97
199
|
|
|
98
200
|
const commandMeta: Record<string, any> = { name: c.name, tags: c.tags }
|
|
99
201
|
if (c.description !== undefined) commandMeta.description = c.description
|
|
@@ -153,7 +255,10 @@ function nodeToBlock(node: CommandNode, tagPath: string[], appName: string, vers
|
|
|
153
255
|
|
|
154
256
|
const subBlocks = subGroupEntries.map(([tag, subNode], i) => {
|
|
155
257
|
const sub = nodeToBlock(subNode, [...tagPath, tag], appName, version)
|
|
156
|
-
const indented = sub
|
|
258
|
+
const indented = sub
|
|
259
|
+
.split('\n')
|
|
260
|
+
.map(l => ' ' + l)
|
|
261
|
+
.join('\n')
|
|
157
262
|
return `${i === 0 ? 'if' : 'else if'} (${argvExpr} === ${JSON.stringify(tag)}) {\n${indented}\n}`
|
|
158
263
|
})
|
|
159
264
|
|
|
@@ -182,7 +287,10 @@ function nodeToBlock(node: CommandNode, tagPath: string[], appName: string, vers
|
|
|
182
287
|
.join('\n')
|
|
183
288
|
|
|
184
289
|
if (subBlocks.length > 0) {
|
|
185
|
-
const indentedCli = cliBlockLines
|
|
290
|
+
const indentedCli = cliBlockLines
|
|
291
|
+
.split('\n')
|
|
292
|
+
.map(l => ' ' + l)
|
|
293
|
+
.join('\n')
|
|
186
294
|
return `${subBlocks.join('\n')}\nelse {\n${indentedCli}\n}`
|
|
187
295
|
}
|
|
188
296
|
return cliBlockLines
|
|
@@ -213,20 +321,20 @@ function generateModuleCommands(commands: GalbeCLICommand[], appName: string): s
|
|
|
213
321
|
nodeLines.push(genSubCacSetup(subNode, subTagPath))
|
|
214
322
|
nodeLines.push(
|
|
215
323
|
`const ${grpVar} = ${subVar}.command(${JSON.stringify(tag + ' [..._sub]')}, ${JSON.stringify(tag + ' commands')})` +
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
324
|
+
`\n .allowUnknownOptions()` +
|
|
325
|
+
`\n .action(() => {` +
|
|
326
|
+
`\n const _rawArgv = process.argv.slice(2)` +
|
|
327
|
+
`\n const _tagIdx = _rawArgv.findIndex((a: string) => a === ${JSON.stringify(tag)})` +
|
|
328
|
+
`\n const _subArgv = _tagIdx >= 0 ? _rawArgv.slice(_tagIdx + 1) : []` +
|
|
329
|
+
`\n const ${knownVar} = new Set(${subSubVar}.commands.map((c: any) => c.name))` +
|
|
330
|
+
`\n if (!_subArgv[0] || (!_subArgv[0].startsWith('-') && !${knownVar}.has(_subArgv[0]))) {` +
|
|
331
|
+
`\n ${subSubVar}.outputHelp()` +
|
|
332
|
+
`\n process.exit(_subArgv[0] ? 1 : 0)` +
|
|
333
|
+
`\n }` +
|
|
334
|
+
`\n try { ${subSubVar}.parse(['', '', ..._subArgv]) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
|
|
335
|
+
`\n })` +
|
|
336
|
+
`\n;(${grpVar} as any).rawName = ${JSON.stringify(tag)}` +
|
|
337
|
+
`\n;(${grpVar} as any).outputHelp = () => ${subSubVar}.outputHelp()`
|
|
230
338
|
)
|
|
231
339
|
}
|
|
232
340
|
|
|
@@ -241,20 +349,20 @@ function generateModuleCommands(commands: GalbeCLICommand[], appName: string): s
|
|
|
241
349
|
lines.push(genSubCacSetup(subNode, [tag]))
|
|
242
350
|
lines.push(
|
|
243
351
|
`const ${grpVar} = parent.command(${JSON.stringify(tag + ' [..._sub]')}, ${JSON.stringify(tag + ' commands')})` +
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
352
|
+
`\n .allowUnknownOptions()` +
|
|
353
|
+
`\n .action(() => {` +
|
|
354
|
+
`\n const _rawArgv = process.argv.slice(2)` +
|
|
355
|
+
`\n const _tagIdx = _rawArgv.findIndex((a: string) => a === ${JSON.stringify(tag)})` +
|
|
356
|
+
`\n const _subArgv = _tagIdx >= 0 ? _rawArgv.slice(_tagIdx + 1) : []` +
|
|
357
|
+
`\n const ${knownVar} = new Set(${subVar}.commands.map((c: any) => c.name))` +
|
|
358
|
+
`\n if (!_subArgv[0] || (!_subArgv[0].startsWith('-') && !${knownVar}.has(_subArgv[0]))) {` +
|
|
359
|
+
`\n ${subVar}.outputHelp()` +
|
|
360
|
+
`\n process.exit(_subArgv[0] ? 1 : 0)` +
|
|
361
|
+
`\n }` +
|
|
362
|
+
`\n try { ${subVar}.parse(['', '', ..._subArgv]) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
|
|
363
|
+
`\n })` +
|
|
364
|
+
`\n;(${grpVar} as any).rawName = ${JSON.stringify(tag)}` +
|
|
365
|
+
`\n;(${grpVar} as any).outputHelp = () => ${subVar}.outputHelp()`
|
|
258
366
|
)
|
|
259
367
|
}
|
|
260
368
|
|
|
@@ -262,20 +370,21 @@ function generateModuleCommands(commands: GalbeCLICommand[], appName: string): s
|
|
|
262
370
|
|
|
263
371
|
lines.push(
|
|
264
372
|
`const _parseOrig = (parent as any).parse.bind(parent)` +
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
373
|
+
`\n;(parent as any).parse = (argv?: string[]) => {` +
|
|
374
|
+
`\n const _a: string[] = argv ?? process.argv` +
|
|
375
|
+
`\n const _positionals = _a.slice(2).filter((x: string) => !x.startsWith('-'))` +
|
|
376
|
+
`\n if (_positionals.length === 0 && !_a.slice(2).includes('--help') && !_a.slice(2).includes('-h')) {` +
|
|
377
|
+
`\n parent.outputHelp()` +
|
|
378
|
+
`\n process.exit(0)` +
|
|
379
|
+
`\n }` +
|
|
380
|
+
`\n let _r: any` +
|
|
381
|
+
`\n try { _r = _parseOrig(argv) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
|
|
382
|
+
`\n if (!(parent as any).matchedCommand && _positionals.length > 0 && !_a.slice(2).includes('--help') && !_a.slice(2).includes('-h')) {` +
|
|
383
|
+
`\n console.error(\`error: Unknown command "\${_positionals[0]}"\`)` +
|
|
384
|
+
`\n process.exit(1)` +
|
|
385
|
+
`\n }` +
|
|
386
|
+
`\n return _r` +
|
|
387
|
+
`\n}`
|
|
279
388
|
)
|
|
280
389
|
|
|
281
390
|
return lines.filter(Boolean).join('\n\n')
|
|
@@ -413,7 +522,7 @@ ${fetchApiSignature}
|
|
|
413
522
|
const elapsed = (Bun.nanoseconds() - t0) / 1_000_000
|
|
414
523
|
|
|
415
524
|
if (${formatterRef}) {
|
|
416
|
-
Bun.write(Bun.stdout, await ${formatterRef}(_res))
|
|
525
|
+
Bun.write(Bun.stdout, await ${formatterRef}(_res, command, cliArgs, cliOptions))
|
|
417
526
|
} else {
|
|
418
527
|
if (fmt.has('s')) Bun.write(Bun.stdout, \`\${_res.status}\\n\`)
|
|
419
528
|
if (fmt.has('h')) Bun.write(Bun.stdout, _fmtObject(_headersToObj(_res.headers), fmt.has('p')) + '\\n')
|
|
@@ -446,16 +555,26 @@ function generateSource(
|
|
|
446
555
|
commands: GalbeCLICommand[],
|
|
447
556
|
mode: 'standalone' | 'module',
|
|
448
557
|
pckg: any,
|
|
449
|
-
options?: GalbeCLIOptions
|
|
558
|
+
options?: GalbeCLIOptions,
|
|
559
|
+
_unused?: undefined,
|
|
560
|
+
// pre-bundled IIFE snippet; standalone exports all 4 config values, module exports only functions
|
|
561
|
+
inlinedConfigCode?: string
|
|
450
562
|
): string {
|
|
451
563
|
const name = pckg?.name || 'galbe-cli'
|
|
452
564
|
const version = pckg?.version || '0.1.0'
|
|
453
565
|
const nameVersion = `${name}/${version}/cli`
|
|
454
566
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
const
|
|
567
|
+
// Standalone with config: all values come from the IIFE (avoids module-level side effects).
|
|
568
|
+
// Module with config: scalars serialized from options; functions come from the IIFE.
|
|
569
|
+
const standaloneInlined = mode === 'standalone' && !!inlinedConfigCode
|
|
570
|
+
const bakedBaseUrl = standaloneInlined ? '__galbe_baseUrl' : serializeValue(options?.baseUrl)
|
|
571
|
+
const bakedHeaders = standaloneInlined ? '__galbe_headers ?? {}' : serializeValue(options?.headers ?? {})
|
|
572
|
+
const bakedRequestInterceptor = !!inlinedConfigCode
|
|
573
|
+
? '__galbe_requestInterceptor'
|
|
574
|
+
: serializeValue(options?.requestInterceptor)
|
|
575
|
+
const bakedResponseFormatter = !!inlinedConfigCode
|
|
576
|
+
? '__galbe_responseFormatter'
|
|
577
|
+
: serializeValue(options?.responseFormatter)
|
|
459
578
|
|
|
460
579
|
const utils = generateUtilities()
|
|
461
580
|
const fetchApi = generateFetchApi(
|
|
@@ -478,7 +597,7 @@ function generateSource(
|
|
|
478
597
|
${header}
|
|
479
598
|
|
|
480
599
|
import { cac } from 'cac'
|
|
481
|
-
${utils}
|
|
600
|
+
${inlinedConfigCode ? '\n' + inlinedConfigCode + '\n' : ''}${utils}
|
|
482
601
|
${fetchApi}
|
|
483
602
|
|
|
484
603
|
const _argv = process.argv.slice(2)
|
|
@@ -491,10 +610,16 @@ ${block}
|
|
|
491
610
|
const defaultOptsEntries: string[] = []
|
|
492
611
|
if (options?.baseUrl !== undefined) defaultOptsEntries.push(` baseUrl: ${serializeValue(options.baseUrl)}`)
|
|
493
612
|
if (options?.headers !== undefined) defaultOptsEntries.push(` headers: ${serializeValue(options.headers)}`)
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
defaultOptsEntries.push(` responseFormatter:
|
|
613
|
+
if (inlinedConfigCode) {
|
|
614
|
+
// Functions are defined by the inlined IIFE; always include them (undefined = no-op)
|
|
615
|
+
defaultOptsEntries.push(` requestInterceptor: __galbe_requestInterceptor as CLIOptions['requestInterceptor']`)
|
|
616
|
+
defaultOptsEntries.push(` responseFormatter: __galbe_responseFormatter as CLIOptions['responseFormatter']`)
|
|
617
|
+
} else {
|
|
618
|
+
if (options?.requestInterceptor !== undefined)
|
|
619
|
+
defaultOptsEntries.push(` requestInterceptor: ${serializeValue(options.requestInterceptor)}`)
|
|
620
|
+
if (options?.responseFormatter !== undefined)
|
|
621
|
+
defaultOptsEntries.push(` responseFormatter: ${serializeValue(options.responseFormatter)}`)
|
|
622
|
+
}
|
|
498
623
|
const defaultOpts = defaultOptsEntries.length ? `{\n${defaultOptsEntries.join(',\n')}\n}` : '{}'
|
|
499
624
|
|
|
500
625
|
return `${header}
|
|
@@ -515,9 +640,9 @@ export type CLIOptions = {
|
|
|
515
640
|
baseUrl?: string | (() => string)
|
|
516
641
|
headers?: Record<string, string>
|
|
517
642
|
requestInterceptor?: (req: Request, command: CommandInfo, args: Record<string, string>, options: Record<string, any>) => Promise<Request> | Request
|
|
518
|
-
responseFormatter?: (res: Response) => Promise<string> | string
|
|
643
|
+
responseFormatter?: (res: Response, command: CommandInfo, args: Record<string, string>, options: Record<string, any>) => Promise<string> | string
|
|
519
644
|
}
|
|
520
|
-
${utils}
|
|
645
|
+
${inlinedConfigCode ? '\n' + inlinedConfigCode + '\n' : ''}${utils}
|
|
521
646
|
${fetchApi}
|
|
522
647
|
|
|
523
648
|
const _defaultOptions: CLIOptions = ${defaultOpts}
|