@bakery-framework/plugin-vue 1.0.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/src/utils.ts ADDED
@@ -0,0 +1,608 @@
1
+ import {
2
+ readdir as nodeReaddir,
3
+ rename as nodeRename,
4
+ unlink as nodeUnlink,
5
+ } from 'node:fs/promises'
6
+ import { LRUCache } from '@bakery-framework/core/cache/lru'
7
+ import { Bakery } from '@bakery-framework/core/core/bakery'
8
+ import { Logger } from '@bakery-framework/core/logger'
9
+ import { fs, is, Try } from '@bakery-framework/core/utils'
10
+ import type { ParsedCacheEntry, ServerResponseOptions, VueMeta } from './types'
11
+
12
+ const logger = new Logger('vue')
13
+
14
+ export let VUE_VERSION = 'vue'
15
+ export function initVueVersion() {
16
+ try {
17
+ const pkgPath = Bun.resolveSync('vue/package.json', process.cwd())
18
+ const content = fs.readFileSync(pkgPath)
19
+ if (content) {
20
+ const pkg = JSON.parse(content)
21
+ if (pkg?.version) VUE_VERSION = pkg.version
22
+ }
23
+ } catch {
24
+ // Vue is resolved from the app's cwd and may not be installed there at
25
+ // all. The pinned default is the fallback, and the caller renders either way.
26
+ }
27
+ return VUE_VERSION
28
+ }
29
+
30
+ export const RX_IMPORT_VUE_FILE =
31
+ /(?:from\s+|import\s+|import\()(['"][^'"]*\.vue['"])/g
32
+ export const RX_EXPORT_BRACE = /\bexport\s*\{([\s\S]*?)\}/g
33
+ export const RX_EXPORT_HANGING = /\bexport\s+(const|let|var)\s+(\w+)\s*=\s*/g
34
+ export const RX_EXPORT_FUNCTION = /\bexport\s+(?:async\s+)?function\s+(\w+)/g
35
+ /** `export const fn = async (a) => {}` / `= function () {}` — callable, not data. */
36
+ export const RX_EXPORT_CALLABLE_CONST =
37
+ /\bexport\s+(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:function\b|(?:\([^)]*\)|\w+)\s*=>)/g
38
+ export const RX_TOP_LEVEL_IMPORT =
39
+ /^\s*import\s*(?:(?:[\w_$*{},\s\n]+from\s*)?['"`][^'"`\n]+['"`]|['"`][^'"`\n]+['"`]);?/
40
+
41
+ export const cacheDir = fs.resolve(Bakery.cacheDir, 'vue')
42
+ export const VUE_SERVER_DATA_TOKEN = '__BAKERY_VUE_SERVER_DATA__'
43
+
44
+ /** Prefix for names the generated wrapper reserves, so user code can't collide. */
45
+ const INTERNAL_PREFIX = '__bkry_'
46
+ const SERVER_SCRIPT_TIMEOUT_MS = 5000
47
+
48
+ export const parsedCache = new LRUCache<string, ParsedCacheEntry>(1000)
49
+
50
+ /**
51
+ * Escaping is owned by the framework, not by this plugin \u2014 core and other
52
+ * plugins were importing it from here, which would make `@bakery-framework/core` depend
53
+ * on `@bakery-framework/vue` once these ship as separate packages. Re-exported so this
54
+ * plugin's own modules and tests keep their existing import path.
55
+ */
56
+ export { escapeHtml, escapeScriptJson } from '@bakery-framework/core/utils/http'
57
+
58
+ const RX_SERVER_SCRIPT_OPEN = /<script\b(?=[^>]*\bserver(?=[\s/>=]))[^>]*>/gi
59
+ const CLOSING_TAG = '</script'
60
+
61
+ /**
62
+ * Find the `</script>` that actually closes a server block, skipping ones that
63
+ * appear inside string literals, template literals, or comments. A plain
64
+ * non-greedy regex stops at the first match — so server code containing
65
+ * `"</script>"` would be cut short and its remainder left in the client bundle.
66
+ */
67
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: character scanner — locates a block end past nested quotes
68
+ function findServerScriptEnd(raw: string, start: number): number {
69
+ let index = start
70
+
71
+ while (index < raw.length) {
72
+ const char = raw[index]
73
+ const next = raw[index + 1]
74
+
75
+ if (char === '/' && next === '/') {
76
+ const nl = raw.indexOf('\n', index)
77
+ index = nl === -1 ? raw.length : nl
78
+ continue
79
+ }
80
+
81
+ if (char === '/' && next === '*') {
82
+ const end = raw.indexOf('*/', index + 2)
83
+ index = end === -1 ? raw.length : end + 2
84
+ continue
85
+ }
86
+
87
+ if (char === '"' || char === "'" || char === '`') {
88
+ index += 1
89
+ while (index < raw.length) {
90
+ if (raw[index] === '\\') {
91
+ index += 2
92
+ continue
93
+ }
94
+ if (raw[index] === char) break
95
+ index += 1
96
+ }
97
+ index += 1
98
+ continue
99
+ }
100
+
101
+ if (
102
+ char === '<' &&
103
+ raw.slice(index, index + CLOSING_TAG.length).toLowerCase() === CLOSING_TAG
104
+ ) {
105
+ return index
106
+ }
107
+
108
+ index += 1
109
+ }
110
+
111
+ return raw.length
112
+ }
113
+
114
+ /** Collect every `<script server>` block; a file may declare more than one. */
115
+ export function extractServerScripts(raw: string): {
116
+ script: string
117
+ clean: string
118
+ } {
119
+ const blocks: string[] = []
120
+ let clean = ''
121
+ let cursor = 0
122
+
123
+ RX_SERVER_SCRIPT_OPEN.lastIndex = 0
124
+ let open: RegExpExecArray | null
125
+
126
+ while ((open = RX_SERVER_SCRIPT_OPEN.exec(raw))) {
127
+ const bodyStart = open.index + open[0].length
128
+ const bodyEnd = findServerScriptEnd(raw, bodyStart)
129
+
130
+ blocks.push(raw.slice(bodyStart, bodyEnd))
131
+ clean += raw.slice(cursor, open.index)
132
+
133
+ const tagEnd = raw.indexOf('>', bodyEnd)
134
+ cursor = bodyEnd >= raw.length || tagEnd === -1 ? raw.length : tagEnd + 1
135
+ RX_SERVER_SCRIPT_OPEN.lastIndex = cursor
136
+ }
137
+
138
+ clean += raw.slice(cursor)
139
+ return { script: blocks.join('\n'), clean }
140
+ }
141
+
142
+ export function extractImportsAndBody(code: string) {
143
+ const imports: string[] = []
144
+
145
+ const codeLength = code.length
146
+ let index = 0
147
+ let lastImportEnd = 0
148
+
149
+ while (index < codeLength) {
150
+ const remaining = code.slice(index)
151
+ const wsMatch = remaining.match(/^\s+|^\/\/.*|^\/\*[\s\S]*?\*\//)
152
+ if (wsMatch) {
153
+ index += wsMatch[0].length
154
+ continue
155
+ }
156
+
157
+ const importMatch = remaining.match(RX_TOP_LEVEL_IMPORT)
158
+ if (importMatch && importMatch.index === 0) {
159
+ imports.push(importMatch[0])
160
+ index += importMatch[0].length
161
+ lastImportEnd = index
162
+ } else {
163
+ break
164
+ }
165
+ }
166
+
167
+ return { imports: imports.join('\n'), body: code.slice(lastImportEnd) }
168
+ }
169
+
170
+ /**
171
+ * The compiled server block runs from the cache directory, so relative imports
172
+ * must be made absolute. `import(` is included because dynamic imports are the
173
+ * normal way a server action pulls in an API handler.
174
+ */
175
+ export function rewriteRelativeImports(
176
+ code: string,
177
+ filePath?: string,
178
+ ): string {
179
+ if (!filePath) return code
180
+ const dir = fs.dirname(filePath)
181
+ return code.replace(
182
+ /(?:from\s+|import\s+|import\()(['"]\.[^'"]+['"])/g,
183
+ (match, quotePath: string) => {
184
+ const rel = quotePath.slice(1, -1)
185
+ const abs = fs.resolve(dir, rel).replace(/\\/g, '/')
186
+ return match.replace(quotePath, `'${abs}'`)
187
+ },
188
+ )
189
+ }
190
+
191
+ /**
192
+ * Find where the expression starting at `start` ends. Tracks bracket depth and
193
+ * skips strings/comments, so a multi-line object, function, or arrow body
194
+ * survives intact — stopping at the first newline breaks all three.
195
+ */
196
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: character scanner — brace and quote state machine
197
+ function findExpressionEnd(code: string, start: number): number {
198
+ let depth = 0
199
+ let index = start
200
+ let sawBlock = false
201
+ let lastMeaningful = ''
202
+ const startsWithFunction = /^\s*(?:async\s+)?function\b/.test(
203
+ code.slice(start),
204
+ )
205
+
206
+ while (index < code.length) {
207
+ const char = code[index]
208
+ const next = code[index + 1]
209
+
210
+ if (char === '/' && next === '/') {
211
+ const nl = code.indexOf('\n', index)
212
+ index = nl === -1 ? code.length : nl
213
+ continue
214
+ }
215
+
216
+ if (char === '/' && next === '*') {
217
+ const end = code.indexOf('*/', index + 2)
218
+ index = end === -1 ? code.length : end + 2
219
+ continue
220
+ }
221
+
222
+ if (char === '"' || char === "'" || char === '`') {
223
+ index += 1
224
+ while (index < code.length) {
225
+ if (code[index] === '\\') {
226
+ index += 2
227
+ continue
228
+ }
229
+ if (code[index] === char) break
230
+ index += 1
231
+ }
232
+ index += 1
233
+ lastMeaningful = char
234
+ continue
235
+ }
236
+
237
+ if (char === '(' || char === '[' || char === '{') {
238
+ depth += 1
239
+ index += 1
240
+ lastMeaningful = char
241
+ continue
242
+ }
243
+
244
+ if (char === ')' || char === ']' || char === '}') {
245
+ depth -= 1
246
+ if (depth <= 0 && char === '}') sawBlock = true
247
+ index += 1
248
+ lastMeaningful = char
249
+ continue
250
+ }
251
+
252
+ if (char === ';' && depth <= 0) return index
253
+
254
+ if (char === '\n' && depth <= 0) {
255
+ // ASI: only end the expression here if it already looks complete.
256
+ const incomplete =
257
+ lastMeaningful === '' ||
258
+ '+-*/%=&|^<>,?:.({['.includes(lastMeaningful) ||
259
+ (startsWithFunction && !sawBlock)
260
+ if (!incomplete) return index
261
+ index += 1
262
+ continue
263
+ }
264
+
265
+ if (!/\s/.test(char)) lastMeaningful = char
266
+ index += 1
267
+ }
268
+
269
+ return code.length
270
+ }
271
+
272
+ /**
273
+ * Rewrite `export default <expr>` into a plain const. Evaluating it at the end
274
+ * of the wrapper rather than inline puts value- and function-form defaults on
275
+ * one code path and lets middleware gate both.
276
+ */
277
+ function rewriteDefaultExport(body: string): {
278
+ code: string
279
+ hasDefault: boolean
280
+ } {
281
+ const match = body.match(/\bexport\s+default\s+/)
282
+ if (!match || match.index === undefined) {
283
+ return { code: body, hasDefault: false }
284
+ }
285
+
286
+ const exprStart = match.index + match[0].length
287
+ const exprEnd = findExpressionEnd(body, exprStart)
288
+ const expr = body.slice(exprStart, exprEnd)
289
+
290
+ const code =
291
+ body.slice(0, match.index) +
292
+ `const ${INTERNAL_PREFIX}defaultVal = ${expr};` +
293
+ body.slice(exprEnd)
294
+
295
+ return { code, hasDefault: true }
296
+ }
297
+
298
+ export function collectExportedFunctionNames(script: string): string[] {
299
+ const names: string[] = []
300
+ for (const m of script.matchAll(RX_EXPORT_FUNCTION)) names.push(m[1])
301
+ for (const m of script.matchAll(RX_EXPORT_CALLABLE_CONST)) names.push(m[1])
302
+ return [...new Set(names)]
303
+ }
304
+
305
+ export function compileServerBlock(
306
+ serverScript: string,
307
+ filePath?: string,
308
+ ): string {
309
+ let script = rewriteRelativeImports(serverScript, filePath)
310
+
311
+ script = script
312
+ .replace(/\bexport\s+type\s+\w+\s*=[\s\S]*?;/g, '')
313
+ .replace(/\bexport\s+interface\s+\w+[\s\S]*?\}/g, '')
314
+
315
+ const { imports, body } = extractImportsAndBody(script)
316
+
317
+ const exportedFnNames = collectExportedFunctionNames(body)
318
+ const { code: withDefault, hasDefault } = rewriteDefaultExport(body)
319
+ let compiledBody = withDefault
320
+
321
+ compiledBody = compiledBody.replace(
322
+ /\bexport\s+(?=(?:async\s+)?function\b)/g,
323
+ '',
324
+ )
325
+
326
+ compiledBody = compiledBody.replace(
327
+ /\bexport\s+(const|let|var)\s+\{([^}]+)\}\s*=\s*([\s\S]+?)(?:;|\n|$)/g,
328
+ (_, kind, dest, expr) => {
329
+ const keys = dest
330
+ .split(',')
331
+ .map((k: string) => k.trim())
332
+ .filter(Boolean)
333
+ const assignments = keys
334
+ .map((k: string) => `${INTERNAL_PREFIX}result['${k}'] = ${k};`)
335
+ .join(' ')
336
+ return `${kind} { ${dest} } = ${expr};\n${assignments}`
337
+ },
338
+ )
339
+
340
+ compiledBody = compiledBody
341
+ .replace(RX_EXPORT_HANGING, `$1 $2 = ${INTERNAL_PREFIX}result['$2'] = `)
342
+ .replace(RX_EXPORT_BRACE, (_, exportsContent) => {
343
+ const properties = exportsContent
344
+ .split(',')
345
+ .map((part: string) => {
346
+ const trimmed = part.trim()
347
+ if (!trimmed) return ''
348
+ if (trimmed.includes(' as ')) {
349
+ const [local, alias] = trimmed.split(/\s+as\s+/)
350
+ return `${alias}: ${local}`
351
+ }
352
+ return `${trimmed}: ${trimmed}`
353
+ })
354
+ .filter(Boolean)
355
+ .join(', ')
356
+ return `Object.assign(${INTERNAL_PREFIX}result, { ${properties} })`
357
+ })
358
+
359
+ const fnAssignments = exportedFnNames
360
+ .map(fn => `try { ${INTERNAL_PREFIX}result['${fn}'] = ${fn}; } catch {}`)
361
+ .join('\n')
362
+
363
+ // Only these names are reachable via `?__vue_action=`; anything else the
364
+ // script exports stays server-side data.
365
+ const actionAllowList = JSON.stringify(
366
+ exportedFnNames.filter(fn => fn !== 'middleware' && fn !== 'default'),
367
+ )
368
+
369
+ const p = INTERNAL_PREFIX
370
+
371
+ return `
372
+ ${imports}
373
+
374
+ const ${p}isResponseLike = (v: any) =>
375
+ v instanceof Response ||
376
+ (v && typeof v === 'object' && 'arrayBuffer' in v && 'size' in v)
377
+
378
+ const ${p}actions = new Set(${actionAllowList})
379
+
380
+ // NOTE: the top-level statements below run before middleware. Middleware can
381
+ // stop the response but cannot stop top-level code from executing — put
382
+ // auth-gated work inside an exported function, not at the top level.
383
+ export default async function ${p}server(req: any, body: any, actionName?: string, actionArgs?: any[]) {
384
+ const ${p}result: any = {}
385
+
386
+ // The allow-list is static — it is this script's own exports — so it can
387
+ // be answered before a line of the component runs. It used to be checked
388
+ // after the body, which meant naming an action that does not exist still
389
+ // executed every top-level statement in the file; with \`__vue_file\`
390
+ // pointing at another component, that was a way to run one page's server
391
+ // block from another page's route.
392
+ //
393
+ // It does sit ahead of middleware, so an unauthenticated caller can now
394
+ // tell an unknown action name from a known one. That is the smaller leak:
395
+ // the alternative is running the file to find out.
396
+ if (actionName && !${p}actions.has(actionName)) {
397
+ return { error: \`Action '\${actionName}' not found\` }
398
+ }
399
+
400
+ ${compiledBody}
401
+
402
+ ${fnAssignments}
403
+
404
+ if (typeof ${p}result['middleware'] === 'function') {
405
+ const ${p}mwRes = await ${p}result['middleware'](req, body);
406
+ if (${p}isResponseLike(${p}mwRes)) return ${p}mwRes;
407
+ }
408
+
409
+ if (actionName) {
410
+ if (!${p}actions.has(actionName) || !Object.hasOwn(${p}result, actionName)) {
411
+ return { error: \`Action '\${actionName}' not found\` }
412
+ }
413
+ const ${p}args = Array.isArray(actionArgs) ? actionArgs : []
414
+ return await ${p}result[actionName](...${p}args)
415
+ }
416
+ ${
417
+ hasDefault
418
+ ? `
419
+ let ${p}defaultRes: any = ${p}defaultVal;
420
+ if (typeof ${p}defaultRes === 'function') {
421
+ ${p}defaultRes = await ${p}defaultRes(req, body);
422
+ }
423
+ if (${p}isResponseLike(${p}defaultRes)) return ${p}defaultRes;
424
+ if (typeof ${p}defaultRes === 'object' && ${p}defaultRes !== null) {
425
+ Object.assign(${p}result, ${p}defaultRes);
426
+ } else if (${p}defaultRes !== undefined) {
427
+ ${p}result['default'] = ${p}defaultRes;
428
+ }
429
+ `
430
+ : ''
431
+ }
432
+ return ${p}result
433
+ }
434
+ `
435
+ }
436
+
437
+ /** Drop stale compiled versions of the same source so the cache dir stays bounded. */
438
+ async function pruneServerCache(dir: string, id: string, keepName: string) {
439
+ const entries = await Try(nodeReaddir(dir))
440
+ if (!entries) return
441
+ await Promise.all(
442
+ entries
443
+ .filter(
444
+ name =>
445
+ name.startsWith(`${id}_`) &&
446
+ name.endsWith('.ts') &&
447
+ name !== keepName,
448
+ )
449
+ .map(name => Try(nodeUnlink(fs.resolve(dir, name)))),
450
+ )
451
+ }
452
+
453
+ const inFlightCompiles = new Map<string, Promise<void>>()
454
+ let tempCounter = 0
455
+
456
+ function writeServerModule(
457
+ dir: string,
458
+ cacheName: string,
459
+ id: string,
460
+ script: string,
461
+ filePath?: string,
462
+ ): Promise<void> {
463
+ const cacheKey = fs.resolve(dir, cacheName)
464
+ const pending = inFlightCompiles.get(cacheKey)
465
+ if (pending) return pending
466
+
467
+ const task = (async () => {
468
+ await fs.mkdir(dir)
469
+ // Write to a temp path then rename, so a concurrent request can never
470
+ // import a half-written module.
471
+ const tempPath = `${cacheKey}.${process.pid}.${tempCounter++}.tmp`
472
+ await Bun.write(tempPath, compileServerBlock(script, filePath))
473
+ const [renameError] = await Try.catch(nodeRename(tempPath, cacheKey))
474
+ if (renameError) {
475
+ await Bun.write(cacheKey, Bun.file(tempPath))
476
+ await Try(nodeUnlink(tempPath))
477
+ }
478
+ await pruneServerCache(dir, id, cacheName)
479
+ })().finally(() => inFlightCompiles.delete(cacheKey))
480
+
481
+ inFlightCompiles.set(cacheKey, task)
482
+ return task
483
+ }
484
+
485
+ export async function getServerResponse(options: ServerResponseOptions) {
486
+ const { script, id, lastMod, req, body, filePath, actionName, actionArgs } =
487
+ options
488
+ if (!script.trim()) return {}
489
+
490
+ const dir = fs.resolve(cacheDir, 'server')
491
+ const cacheName = `${id}_${lastMod}.ts`
492
+ const cacheKey = fs.resolve(dir, cacheName)
493
+
494
+ if (!fs.exists(cacheKey)) {
495
+ await writeServerModule(dir, cacheName, id, script, filePath)
496
+ }
497
+
498
+ const importUrl = Bun.pathToFileURL(cacheKey).href
499
+ const [importError, mod] = await Try.catch(import(importUrl))
500
+ const exported = mod?.default
501
+
502
+ if (!exported) {
503
+ logger.log(
504
+ `Failed to load server script for ${id}: ${
505
+ importError?.message || 'no default export'
506
+ }`,
507
+ 'error',
508
+ )
509
+ return {}
510
+ }
511
+
512
+ let timer: ReturnType<typeof setTimeout> | undefined
513
+ try {
514
+ const timeoutPromise = new Promise((_, reject) => {
515
+ timer = setTimeout(
516
+ () =>
517
+ reject(
518
+ new Error(
519
+ `Server script execution timed out (${SERVER_SCRIPT_TIMEOUT_MS}ms limit)`,
520
+ ),
521
+ ),
522
+ SERVER_SCRIPT_TIMEOUT_MS,
523
+ )
524
+ })
525
+ const execPromise = Promise.resolve(
526
+ is.function(exported)
527
+ ? exported(req, body, actionName, actionArgs)
528
+ : exported,
529
+ )
530
+ const params = await Promise.race([execPromise, timeoutPromise])
531
+
532
+ if (
533
+ params instanceof Response ||
534
+ (params &&
535
+ typeof params === 'object' &&
536
+ 'arrayBuffer' in params &&
537
+ 'size' in params)
538
+ ) {
539
+ return params
540
+ }
541
+
542
+ if (actionName !== undefined) {
543
+ return params
544
+ }
545
+
546
+ return is.object(params) ? params : {}
547
+ } catch (err: any) {
548
+ logger.log(
549
+ `Server script error in ${id}: ${err?.message || err} at ${req.url}`,
550
+ 'error',
551
+ )
552
+ return {}
553
+ } finally {
554
+ clearTimeout(timer)
555
+ }
556
+ }
557
+
558
+ export function rewriteVueImports(code: string): string {
559
+ return code.replace(
560
+ RX_IMPORT_VUE_FILE,
561
+ (match, quote: string) =>
562
+ `${match.slice(0, -quote.length)}${quote.slice(0, -1)}?__vue_script=module${quote[0]}`,
563
+ )
564
+ }
565
+
566
+ /**
567
+ * A leading `<meta … />` directive. Quoted attribute values may contain `>`,
568
+ * so plain `[^>]` would truncate the tag. Anchored: only the prologue counts.
569
+ */
570
+ export const RX_VUE_META = /^<meta(?![\w-])((?:"[^"]*"|'[^']*'|[^>])*?)\/>/i
571
+ const RX_META_SKIPPABLE = /^\s+|^<!--[\s\S]*?-->/
572
+
573
+ export function parseVueMeta(raw: string): { meta: VueMeta; clean: string } {
574
+ const meta: VueMeta = { moduleOnly: false, pageOnly: false, title: null }
575
+
576
+ // Directives live in the file prologue only. Walking forward from the start
577
+ // (rather than scanning the whole file) keeps a `<meta />` inside a template
578
+ // or a script string from being treated as a directive.
579
+ let index = 0
580
+ let prologue = ''
581
+
582
+ while (index < raw.length) {
583
+ const rest = raw.slice(index)
584
+
585
+ const skippable = rest.match(RX_META_SKIPPABLE)
586
+ if (skippable) {
587
+ prologue += skippable[0]
588
+ index += skippable[0].length
589
+ continue
590
+ }
591
+
592
+ const tag = rest.match(RX_VUE_META)
593
+ if (!tag) break
594
+
595
+ const attrs = tag[1]
596
+ if (/\bmodule-only\b/i.test(attrs)) meta.moduleOnly = true
597
+ if (/\bpage-only\b/i.test(attrs)) meta.pageOnly = true
598
+
599
+ const titleMatch = attrs.match(
600
+ /\btitle\s*=\s*"([^"]*)"|\btitle\s*=\s*'([^']*)'/i,
601
+ )
602
+ if (titleMatch) meta.title = titleMatch[1] ?? titleMatch[2]
603
+
604
+ index += tag[0].length
605
+ }
606
+
607
+ return { meta, clean: prologue + raw.slice(index) }
608
+ }
package/src/vue.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type {} from '@bakery-framework/core/global.d.ts'
2
+
3
+ declare global {
4
+ var req: Request
5
+ var body: any
6
+ }