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 +18 -1
- package/bin/commands/build.ts +10 -4
- package/bin/commands/generate/cli/index.ts +156 -0
- package/bin/commands/generate/cli/targets/cac.ts +535 -0
- package/bin/commands/generate/client.ts +32 -109
- package/bin/commands/generate/code/openapi.parser.ts +428 -133
- package/bin/commands/generate/code/route-merge.ts +248 -0
- package/bin/commands/generate/code.ts +110 -23
- package/bin/commands/generate/index.ts +2 -0
- package/package.json +4 -1
- package/src/cookies.ts +87 -0
- package/src/extras/spec/openapi.serializer.ts +236 -101
- package/src/extras.ts +1 -0
- package/src/index.ts +4 -7
- package/src/parser.ts +104 -63
- package/src/router.ts +34 -20
- package/src/routes.ts +14 -10
- package/src/schema.ts +82 -95
- package/src/server.ts +35 -21
- package/src/types.ts +180 -61
- package/src/util.ts +10 -18
- package/src/validator.ts +62 -32
- package/bin/res/cli.template.js +0 -122
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import type { RoutePlanEntry, ScopePlan } from './openapi.parser'
|
|
3
|
+
|
|
4
|
+
const METHODS = new Set(['get', 'put', 'patch', 'post', 'delete', 'options', 'head'])
|
|
5
|
+
|
|
6
|
+
export type RouteId = string
|
|
7
|
+
|
|
8
|
+
export const routeId = (method: string, path: string): RouteId => `${method.toUpperCase()} ${path}`
|
|
9
|
+
|
|
10
|
+
export type MergeOptions = {
|
|
11
|
+
/** Delete routes that exist in code but are absent from the plan. Default false (kept as `stale`). */
|
|
12
|
+
removeStale?: boolean
|
|
13
|
+
/** Existing route ids (`${METHOD} ${path}`) to leave alone even if absent from plan. */
|
|
14
|
+
ignore?: Set<RouteId>
|
|
15
|
+
/** Existing-route id -> new id. Applied before diffing so a renamed route is treated as an update. */
|
|
16
|
+
rename?: Map<RouteId, RouteId>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type MergeResult = {
|
|
20
|
+
content: string
|
|
21
|
+
added: RouteId[]
|
|
22
|
+
updated: RouteId[]
|
|
23
|
+
removed: RouteId[]
|
|
24
|
+
/** Stale: in code but not in plan, neither removed nor explicitly ignored. */
|
|
25
|
+
stale: RouteId[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ExistingRoute = {
|
|
29
|
+
origId: RouteId
|
|
30
|
+
method: string
|
|
31
|
+
path: string
|
|
32
|
+
stmt: ts.ExpressionStatement
|
|
33
|
+
pathArg: ts.StringLiteral
|
|
34
|
+
schemaArg: ts.Identifier
|
|
35
|
+
jsdoc: ts.CommentRange | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ExistingFile = {
|
|
39
|
+
text: string
|
|
40
|
+
sourceFile: ts.SourceFile
|
|
41
|
+
routes: ExistingRoute[]
|
|
42
|
+
schemaImport: ts.ImportDeclaration | null
|
|
43
|
+
bodyOpenBracePos: number | null
|
|
44
|
+
bodyCloseBracePos: number | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const importPathForScope = (scopeKey: string): string => {
|
|
48
|
+
const deepness = scopeKey.split('/').length - 1
|
|
49
|
+
return `${Array(deepness).fill('../').join('')}schemas${scopeKey}.schema`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const renderFreshRouteFile = (scope: ScopePlan): string => {
|
|
53
|
+
const importPath = importPathForScope(scope.scopeKey)
|
|
54
|
+
const rDecl = scope.routes.map(r => ` ${r.meta}\ng.${r.call}`)
|
|
55
|
+
return (
|
|
56
|
+
`import { NotImplementedError, type Galbe } from 'galbe'\n` +
|
|
57
|
+
`import { ${[...scope.routeSchemaImports].sort().join(', ')} } from '${importPath}'\n\n` +
|
|
58
|
+
`export default (g: Galbe) => {\n` +
|
|
59
|
+
rDecl.map(d => d.replaceAll('\n', '\n ')).join('\n\n') +
|
|
60
|
+
`\n}\n`
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const findLeadingJsDoc = (sf: ts.SourceFile, stmt: ts.Node): ts.CommentRange | null => {
|
|
65
|
+
const ranges = ts.getLeadingCommentRanges(sf.text, stmt.pos) || []
|
|
66
|
+
let last: ts.CommentRange | null = null
|
|
67
|
+
for (const r of ranges) {
|
|
68
|
+
if (r.kind !== ts.SyntaxKind.MultiLineCommentTrivia) continue
|
|
69
|
+
const txt = sf.text.slice(r.pos, r.end)
|
|
70
|
+
if (txt.startsWith('/**')) last = r
|
|
71
|
+
}
|
|
72
|
+
return last
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parseExistingFile = (text: string, scope: ScopePlan): ExistingFile => {
|
|
76
|
+
const sf = ts.createSourceFile('route.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
77
|
+
|
|
78
|
+
const expectedSuffix = `schemas${scope.scopeKey}.schema`
|
|
79
|
+
let schemaImport: ts.ImportDeclaration | null = null
|
|
80
|
+
let body: ts.Block | null = null
|
|
81
|
+
|
|
82
|
+
for (const stmt of sf.statements) {
|
|
83
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
84
|
+
const spec = stmt.moduleSpecifier
|
|
85
|
+
if (ts.isStringLiteral(spec) && spec.text.endsWith(expectedSuffix)) schemaImport = stmt
|
|
86
|
+
} else if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
87
|
+
const expr = stmt.expression
|
|
88
|
+
if (ts.isArrowFunction(expr) && ts.isBlock(expr.body)) body = expr.body
|
|
89
|
+
else if (ts.isFunctionExpression(expr)) body = expr.body
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const routes: ExistingRoute[] = []
|
|
94
|
+
if (body) {
|
|
95
|
+
for (const stmt of body.statements) {
|
|
96
|
+
if (!ts.isExpressionStatement(stmt)) continue
|
|
97
|
+
const call = stmt.expression
|
|
98
|
+
if (!ts.isCallExpression(call)) continue
|
|
99
|
+
const fn = call.expression
|
|
100
|
+
if (!ts.isPropertyAccessExpression(fn)) continue
|
|
101
|
+
if (!ts.isIdentifier(fn.expression) || fn.expression.text !== 'g') continue
|
|
102
|
+
if (!ts.isIdentifier(fn.name)) continue
|
|
103
|
+
const method = fn.name.text.toLowerCase()
|
|
104
|
+
if (!METHODS.has(method)) continue
|
|
105
|
+
const args = call.arguments
|
|
106
|
+
if (args.length < 2) continue
|
|
107
|
+
const pathArg = args[0]
|
|
108
|
+
const schemaArg = args[1]
|
|
109
|
+
if (!ts.isStringLiteral(pathArg)) continue
|
|
110
|
+
if (!ts.isIdentifier(schemaArg)) continue
|
|
111
|
+
|
|
112
|
+
routes.push({
|
|
113
|
+
origId: routeId(method, pathArg.text),
|
|
114
|
+
method,
|
|
115
|
+
path: pathArg.text,
|
|
116
|
+
stmt,
|
|
117
|
+
pathArg,
|
|
118
|
+
schemaArg,
|
|
119
|
+
jsdoc: findLeadingJsDoc(sf, stmt),
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
text,
|
|
126
|
+
sourceFile: sf,
|
|
127
|
+
routes,
|
|
128
|
+
schemaImport,
|
|
129
|
+
bodyOpenBracePos: body ? body.getStart(sf) : null,
|
|
130
|
+
bodyCloseBracePos: body ? body.end - 1 : null,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type Edit = { pos: number; end: number; text: string }
|
|
135
|
+
|
|
136
|
+
const applyEdits = (text: string, edits: Edit[]): string => {
|
|
137
|
+
// Apply in reverse order of pos so earlier offsets stay valid.
|
|
138
|
+
const sorted = [...edits].sort((a, b) => b.pos - a.pos || b.end - a.end)
|
|
139
|
+
let out = text
|
|
140
|
+
for (const e of sorted) out = out.slice(0, e.pos) + e.text + out.slice(e.end)
|
|
141
|
+
return out
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const mergeRouteFile = (
|
|
145
|
+
existing: string | null,
|
|
146
|
+
scope: ScopePlan,
|
|
147
|
+
opts: MergeOptions = {}
|
|
148
|
+
): MergeResult => {
|
|
149
|
+
const removeStale = opts.removeStale ?? false
|
|
150
|
+
const ignore = opts.ignore ?? new Set<RouteId>()
|
|
151
|
+
const rename = opts.rename ?? new Map<RouteId, RouteId>()
|
|
152
|
+
|
|
153
|
+
const planById = new Map<RouteId, RoutePlanEntry>()
|
|
154
|
+
for (const r of scope.routes) planById.set(routeId(r.method, r.path), r)
|
|
155
|
+
|
|
156
|
+
if (existing === null || existing.trim() === '') {
|
|
157
|
+
return {
|
|
158
|
+
content: renderFreshRouteFile(scope),
|
|
159
|
+
added: [...planById.keys()],
|
|
160
|
+
updated: [],
|
|
161
|
+
removed: [],
|
|
162
|
+
stale: [],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const file = parseExistingFile(existing, scope)
|
|
167
|
+
const sf = file.sourceFile
|
|
168
|
+
|
|
169
|
+
const existingByEffectiveId = new Map<RouteId, ExistingRoute>()
|
|
170
|
+
for (const er of file.routes) {
|
|
171
|
+
const effId = rename.get(er.origId) ?? er.origId
|
|
172
|
+
existingByEffectiveId.set(effId, er)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const updates: { er: ExistingRoute; entry: RoutePlanEntry }[] = []
|
|
176
|
+
const removals: ExistingRoute[] = []
|
|
177
|
+
const stale: ExistingRoute[] = []
|
|
178
|
+
|
|
179
|
+
for (const er of file.routes) {
|
|
180
|
+
const effId = rename.get(er.origId) ?? er.origId
|
|
181
|
+
const planEntry = planById.get(effId)
|
|
182
|
+
if (planEntry) updates.push({ er, entry: planEntry })
|
|
183
|
+
else if (ignore.has(er.origId)) stale.push(er)
|
|
184
|
+
else if (removeStale) removals.push(er)
|
|
185
|
+
else stale.push(er)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const additions: RoutePlanEntry[] = []
|
|
189
|
+
for (const [id, entry] of planById) {
|
|
190
|
+
if (!existingByEffectiveId.has(id)) additions.push(entry)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Compute final imported schema names.
|
|
194
|
+
const finalImports = new Set<string>()
|
|
195
|
+
for (const { entry } of updates) finalImports.add(entry.schemaName)
|
|
196
|
+
for (const er of stale) finalImports.add(er.schemaArg.text)
|
|
197
|
+
for (const entry of additions) finalImports.add(entry.schemaName)
|
|
198
|
+
|
|
199
|
+
const edits: Edit[] = []
|
|
200
|
+
|
|
201
|
+
for (const { er, entry } of updates) {
|
|
202
|
+
if (er.path !== entry.path) {
|
|
203
|
+
edits.push({ pos: er.pathArg.getStart(sf), end: er.pathArg.end, text: JSON.stringify(entry.path) })
|
|
204
|
+
}
|
|
205
|
+
if (er.schemaArg.text !== entry.schemaName) {
|
|
206
|
+
edits.push({ pos: er.schemaArg.getStart(sf), end: er.schemaArg.end, text: entry.schemaName })
|
|
207
|
+
}
|
|
208
|
+
if (er.jsdoc) {
|
|
209
|
+
edits.push({ pos: er.jsdoc.pos, end: er.jsdoc.end, text: entry.meta })
|
|
210
|
+
} else if (entry.meta) {
|
|
211
|
+
const stmtStart = er.stmt.getStart(sf)
|
|
212
|
+
edits.push({ pos: stmtStart, end: stmtStart, text: `${entry.meta}\n ` })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const er of removals) {
|
|
217
|
+
edits.push({ pos: er.stmt.pos, end: er.stmt.end, text: '' })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (file.schemaImport) {
|
|
221
|
+
const sortedImports = [...finalImports].sort()
|
|
222
|
+
if (sortedImports.length === 0) {
|
|
223
|
+
edits.push({ pos: file.schemaImport.pos, end: file.schemaImport.end, text: '' })
|
|
224
|
+
} else {
|
|
225
|
+
const importPath = importPathForScope(scope.scopeKey)
|
|
226
|
+
const newText = `import { ${sortedImports.join(', ')} } from '${importPath}'`
|
|
227
|
+
edits.push({
|
|
228
|
+
pos: file.schemaImport.getStart(sf),
|
|
229
|
+
end: file.schemaImport.end,
|
|
230
|
+
text: newText,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (additions.length > 0 && file.bodyCloseBracePos !== null) {
|
|
236
|
+
const block = additions.map(a => `\n ${a.meta}\n ${`g.${a.call}`.replaceAll('\n', '\n ')}\n`).join('')
|
|
237
|
+
edits.push({ pos: file.bodyCloseBracePos, end: file.bodyCloseBracePos, text: block })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
content: applyEdits(existing, edits),
|
|
242
|
+
added: additions.map(a => routeId(a.method, a.path)),
|
|
243
|
+
updated: updates.map(u => routeId(u.entry.method, u.entry.path)),
|
|
244
|
+
removed: removals.map(r => r.origId),
|
|
245
|
+
stale: stale.map(s => s.origId),
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
@@ -2,14 +2,45 @@ import { $ } from 'bun'
|
|
|
2
2
|
import { devNull } from 'os'
|
|
3
3
|
import { Command, Option } from 'commander'
|
|
4
4
|
import { resolve, relative, extname } from 'path'
|
|
5
|
-
import {
|
|
5
|
+
import { exists, readFile } from 'fs/promises'
|
|
6
6
|
|
|
7
7
|
import { CWD, fmtList, fmtVal } from '../../util'
|
|
8
|
-
import {
|
|
8
|
+
import { applyPlan, planFromOapi, type GenerationPlan } from './code/openapi.parser'
|
|
9
|
+
import { mergeRouteFile, type MergeOptions, type RouteId } from './code/route-merge'
|
|
9
10
|
|
|
10
11
|
const srcTargets = ['ts', 'js']
|
|
11
12
|
const inputFormats = ['openapi:3.0:yaml', 'openapi:3.0:json']
|
|
12
13
|
|
|
14
|
+
const collectFlag = (value: string, prev: string[] = []) => [...prev, value]
|
|
15
|
+
|
|
16
|
+
const parseRename = (raw: string): [RouteId, RouteId] => {
|
|
17
|
+
const eq = raw.indexOf('=')
|
|
18
|
+
if (eq < 0) throw new Error(`invalid --rename value ${fmtVal(raw)} (expected "OLD=NEW")`)
|
|
19
|
+
const from = raw.slice(0, eq).trim()
|
|
20
|
+
const to = raw.slice(eq + 1).trim()
|
|
21
|
+
if (!from || !to) throw new Error(`invalid --rename value ${fmtVal(raw)}`)
|
|
22
|
+
return [from, to]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ScopedDiff = { scope: string; id: RouteId }
|
|
26
|
+
|
|
27
|
+
const printDiff = (diff: {
|
|
28
|
+
added: ScopedDiff[]
|
|
29
|
+
updated: ScopedDiff[]
|
|
30
|
+
removed: ScopedDiff[]
|
|
31
|
+
stale: ScopedDiff[]
|
|
32
|
+
}) => {
|
|
33
|
+
const out = (label: string, prefix: string, items: ScopedDiff[]) => {
|
|
34
|
+
if (!items.length) return
|
|
35
|
+
console.log(`${label} (${items.length}):`)
|
|
36
|
+
for (const x of items) console.log(` ${prefix} ${x.id} \x1b[2m(scope: ${x.scope})\x1b[0m`)
|
|
37
|
+
}
|
|
38
|
+
out('Added', '\x1b[32m+\x1b[0m', diff.added)
|
|
39
|
+
out('Updated', '\x1b[33m~\x1b[0m', diff.updated)
|
|
40
|
+
out('Removed', '\x1b[31m-\x1b[0m', diff.removed)
|
|
41
|
+
out('Stale (kept in code)', '\x1b[2m·\x1b[0m', diff.stale)
|
|
42
|
+
}
|
|
43
|
+
|
|
13
44
|
export default (cmd: Command) => {
|
|
14
45
|
cmd
|
|
15
46
|
.description('generate \x1b[1;30m\x1b[36mGalbe\x1b[0m sources')
|
|
@@ -33,9 +64,20 @@ export default (cmd: Command) => {
|
|
|
33
64
|
.default('ts', fmtVal('ts'))
|
|
34
65
|
)
|
|
35
66
|
.addOption(new Option('-o, --out <dir>', 'output dir').default('src', fmtVal('src')))
|
|
36
|
-
.addOption(new Option('-
|
|
67
|
+
.addOption(new Option('-n, --dry-run', 'show planned changes without writing'))
|
|
68
|
+
.addOption(new Option('--remove-stale', 'delete routes present in code but absent from spec'))
|
|
69
|
+
.addOption(
|
|
70
|
+
new Option('--rename <pair>', '"OLD=NEW" preserve handler when route id changes (repeatable)')
|
|
71
|
+
.argParser(collectFlag)
|
|
72
|
+
.default([])
|
|
73
|
+
)
|
|
74
|
+
.addOption(
|
|
75
|
+
new Option('--ignore-route <route>', '"METHOD /path" leave alone if absent from spec (repeatable)')
|
|
76
|
+
.argParser(collectFlag)
|
|
77
|
+
.default([])
|
|
78
|
+
)
|
|
37
79
|
.action(async (input, props) => {
|
|
38
|
-
let { format, target, out,
|
|
80
|
+
let { format, target, out, dryRun, removeStale, rename, ignoreRoute } = props
|
|
39
81
|
|
|
40
82
|
let inputExt = extname(input)
|
|
41
83
|
if (inputExt === '.yml') inputExt = '.yaml'
|
|
@@ -43,34 +85,79 @@ export default (cmd: Command) => {
|
|
|
43
85
|
|
|
44
86
|
if (!format) format = `openapi:3.0:${inputExt.slice(1)}`
|
|
45
87
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
let renameMap: Map<RouteId, RouteId>
|
|
89
|
+
try {
|
|
90
|
+
renameMap = new Map<RouteId, RouteId>((rename as string[]).map(parseRename))
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.log(`error: ${(err as Error).message}`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
const ignoreSet = new Set<RouteId>((ignoreRoute as string[]).map(s => s.trim()))
|
|
96
|
+
const mergeOpts: MergeOptions = {
|
|
97
|
+
removeStale: !!removeStale,
|
|
98
|
+
rename: renameMap,
|
|
99
|
+
ignore: ignoreSet,
|
|
58
100
|
}
|
|
59
101
|
|
|
60
|
-
|
|
102
|
+
let plan: GenerationPlan
|
|
61
103
|
try {
|
|
62
104
|
let match = format.match(/^([^:]*):([^:]*):(.*)$/)
|
|
63
|
-
if (!match) throw new Error(`
|
|
105
|
+
if (!match) throw new Error(`invalid format ${format}`)
|
|
64
106
|
let [_, kind, version, ext] = match
|
|
65
|
-
if (kind
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
if (kind !== 'openapi') throw new Error('unknown format')
|
|
108
|
+
plan = await planFromOapi(relative(CWD, input), { version, ext, target })
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.log(`error: ${(err as Error).message}`)
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const outDir = resolve(CWD, out)
|
|
115
|
+
|
|
116
|
+
// Compute per-scope merge result.
|
|
117
|
+
const mergeResults: { scopeKey: string; content: string; added: RouteId[]; updated: RouteId[]; removed: RouteId[]; stale: RouteId[] }[] = []
|
|
118
|
+
for (const scope of plan.scopes) {
|
|
119
|
+
const routePath = resolve(outDir, `${scope.routeFile}.${target}`)
|
|
120
|
+
const existing = (await exists(routePath)) ? await readFile(routePath, 'utf-8') : null
|
|
121
|
+
const r = mergeRouteFile(existing, scope, mergeOpts)
|
|
122
|
+
mergeResults.push({ scopeKey: scope.scopeKey, ...r })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const diff = {
|
|
126
|
+
added: mergeResults.flatMap(r => r.added.map(id => ({ scope: r.scopeKey, id }))),
|
|
127
|
+
updated: mergeResults.flatMap(r => r.updated.map(id => ({ scope: r.scopeKey, id }))),
|
|
128
|
+
removed: mergeResults.flatMap(r => r.removed.map(id => ({ scope: r.scopeKey, id }))),
|
|
129
|
+
stale: mergeResults.flatMap(r => r.stale.map(id => ({ scope: r.scopeKey, id }))),
|
|
130
|
+
}
|
|
68
131
|
|
|
69
|
-
|
|
132
|
+
// Block when stale routes need an explicit decision (ignored ones are already resolved).
|
|
133
|
+
const unresolved = diff.stale.filter(s => !ignoreSet.has(s.id))
|
|
134
|
+
if (unresolved.length > 0 && !removeStale && !dryRun) {
|
|
135
|
+
console.log('The following routes exist in code but are not in the spec:')
|
|
136
|
+
for (const s of unresolved) console.log(` - ${s.id} \x1b[2m(scope: ${s.scope})\x1b[0m`)
|
|
137
|
+
console.log()
|
|
138
|
+
console.log('Re-run with one of:')
|
|
139
|
+
console.log(` ${fmtVal('--rename "OLD=NEW"')} treat as a rename, preserve handler`)
|
|
140
|
+
console.log(` ${fmtVal('--ignore-route "ROUTE"')} leave alone, keep as user-managed`)
|
|
141
|
+
console.log(` ${fmtVal('--remove-stale')} confirm deletion of stale routes`)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (dryRun) {
|
|
146
|
+
console.log('\x1b[1;30mDry run — no files will be written.\x1b[0m')
|
|
147
|
+
printDiff(diff)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Bun.write(Bun.stdout, '💻 \x1b[1;30mGenerating \x1b[36mGalbe\x1b[0m\x1b[1;30m sources\x1b[0m')
|
|
152
|
+
try {
|
|
153
|
+
const routeContents = new Map(mergeResults.map(r => [r.scopeKey, r.content]))
|
|
154
|
+
await applyPlan(plan, outDir, { routeContents })
|
|
155
|
+
await $`bunx prettier --write "${outDir}/**/*.{js,ts}" > ${devNull} && printf ""`
|
|
70
156
|
} catch (err) {
|
|
71
|
-
console.log(`error: ${err.message}`)
|
|
157
|
+
console.log(`error: ${(err as Error).message}`)
|
|
72
158
|
process.exit(1)
|
|
73
159
|
}
|
|
74
160
|
Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
161
|
+
printDiff(diff)
|
|
75
162
|
})
|
|
76
163
|
}
|
|
@@ -4,6 +4,7 @@ import client from './client'
|
|
|
4
4
|
import spec from './spec'
|
|
5
5
|
import code from './code'
|
|
6
6
|
import model from './model'
|
|
7
|
+
import cli from './cli/index'
|
|
7
8
|
|
|
8
9
|
export default (cmd: Command) => {
|
|
9
10
|
cmd.description('generate util')
|
|
@@ -11,4 +12,5 @@ export default (cmd: Command) => {
|
|
|
11
12
|
client(cmd.command('client'))
|
|
12
13
|
code(cmd.command('code'))
|
|
13
14
|
model(cmd.command('model'))
|
|
15
|
+
cli(cmd.command('cli'))
|
|
14
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "galbe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -61,6 +61,9 @@
|
|
|
61
61
|
"github": {
|
|
62
62
|
"requireBranch": "main",
|
|
63
63
|
"release": "true"
|
|
64
|
+
},
|
|
65
|
+
"npm": {
|
|
66
|
+
"publish": false
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
}
|
package/src/cookies.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type CookieOptions = {
|
|
2
|
+
path?: string
|
|
3
|
+
maxAge?: number
|
|
4
|
+
httpOnly?: boolean
|
|
5
|
+
sameSite?: true | false | 'lax' | 'strict' | 'none'
|
|
6
|
+
secure?: boolean
|
|
7
|
+
domain?: string
|
|
8
|
+
expires?: Date
|
|
9
|
+
}
|
|
10
|
+
export const parseCookie = (str: string) => {
|
|
11
|
+
type Cookie = {
|
|
12
|
+
name: string
|
|
13
|
+
value: string
|
|
14
|
+
} & CookieOptions
|
|
15
|
+
let cookie: Cookie = { name: '', value: '', path: '/' }
|
|
16
|
+
const entries = str.match(/([^=;\s]+)(?:=([^;]*))?/g)
|
|
17
|
+
if (entries) {
|
|
18
|
+
for (const [idx, entry] of entries.entries()) {
|
|
19
|
+
const [_, key, val] = [...(entry.match(/([^=]+)(?:=(.*))?/) || [])]
|
|
20
|
+
if (idx === 0) {
|
|
21
|
+
cookie.name = key
|
|
22
|
+
cookie.value = val
|
|
23
|
+
} else {
|
|
24
|
+
switch (key) {
|
|
25
|
+
case 'Path':
|
|
26
|
+
cookie.path = val
|
|
27
|
+
break
|
|
28
|
+
case 'Max-Age':
|
|
29
|
+
cookie.maxAge = parseInt(val)
|
|
30
|
+
break
|
|
31
|
+
case 'HttpOnly':
|
|
32
|
+
cookie.httpOnly = true
|
|
33
|
+
break
|
|
34
|
+
case 'SameSite':
|
|
35
|
+
cookie.sameSite =
|
|
36
|
+
({ true: true, false: false, lax: 'lax', strict: 'strict', none: 'none' } as const)[
|
|
37
|
+
val?.toLowerCase() || 'true'
|
|
38
|
+
] || true
|
|
39
|
+
break
|
|
40
|
+
case 'Secure':
|
|
41
|
+
cookie.secure = true
|
|
42
|
+
break
|
|
43
|
+
case 'Domain':
|
|
44
|
+
cookie.domain = val
|
|
45
|
+
break
|
|
46
|
+
case 'Expires':
|
|
47
|
+
cookie.expires = new Date(val)
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return cookie
|
|
54
|
+
}
|
|
55
|
+
export const stringifyCookie = (name: string, value: string, opt: CookieOptions = { path: '/' }) => {
|
|
56
|
+
const sameSite =
|
|
57
|
+
typeof opt.sameSite === 'string'
|
|
58
|
+
? ` SameSite=${capitalize(opt.sameSite)};`
|
|
59
|
+
: opt.sameSite === true
|
|
60
|
+
? ' SameSite=Lax;'
|
|
61
|
+
: ''
|
|
62
|
+
return (
|
|
63
|
+
`${name}=${value};` +
|
|
64
|
+
` path=${opt.path || '/'};` +
|
|
65
|
+
(opt.domain ? ` Domain=${opt.domain};` : '') +
|
|
66
|
+
(typeof opt.maxAge === 'number' ? ` Max-Age=${Math.floor(opt.maxAge)};` : '') +
|
|
67
|
+
(opt.expires ? ` Expires=${opt.expires.toUTCString()};` : '') +
|
|
68
|
+
(opt.secure ? ' Secure;' : '') +
|
|
69
|
+
sameSite +
|
|
70
|
+
(opt.httpOnly ? ' HttpOnly;' : '')
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function capitalize(str?: string) {
|
|
75
|
+
if (!str) return ''
|
|
76
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const readCookies = (cookies?: string | null) => {
|
|
80
|
+
if (!cookies) return {}
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
cookies.split(';').map(c => {
|
|
83
|
+
const [name, ...value] = c.split('=')
|
|
84
|
+
return [name.trim(), value.join('=').trim()]
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
}
|