duto 0.260808.0 → 0.260915.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.
Files changed (98) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +35 -13
  3. package/package.json +54 -13
  4. package/src/cli.ts +45 -0
  5. package/src/commands/new.ts +147 -0
  6. package/src/commands/templates/base/.editorconfig +21 -0
  7. package/src/commands/templates/base/.env.dev.example +2 -0
  8. package/src/commands/templates/base/.env.prod.example +2 -0
  9. package/src/commands/templates/base/README.md +29 -0
  10. package/src/commands/templates/base/_gitignore +100 -0
  11. package/src/commands/templates/base/actions/blog/index.get.ts +9 -0
  12. package/src/commands/templates/base/actions/blog/post/[id].get.ts +9 -0
  13. package/src/commands/templates/base/actions/index.get.ts +5 -0
  14. package/src/commands/templates/base/configs/duto.ts +11 -0
  15. package/src/commands/templates/base/configs/roles.ts +5 -0
  16. package/src/commands/templates/base/package.json +24 -0
  17. package/src/commands/templates/base/tsconfig.json +42 -0
  18. package/src/commands/templates/workerd/wrangler.toml +47 -0
  19. package/src/commands/upgrade.ts +30 -0
  20. package/src/enum.ts +2 -0
  21. package/src/index.d.ts +61 -0
  22. package/src/index.ts +19 -23
  23. package/src/middleware.ts +2 -0
  24. package/src/{cli → pages/cli}/commands/build.ts +13 -7
  25. package/src/{cli → pages/cli}/commands/dev.ts +11 -4
  26. package/src/{cli → pages/cli}/commands/preview.ts +6 -4
  27. package/src/pages/cli/index.ts +10 -0
  28. package/src/{cli → pages/cli}/utils.ts +4 -5
  29. package/src/{client → pages/client}/app.tsx +1 -1
  30. package/src/{client → pages/client}/router.ts +1 -1
  31. package/src/{compiler → pages/compiler}/file.ts +1 -1
  32. package/src/{compiler → pages/compiler}/index.ts +2 -4
  33. package/src/{compiler → pages/compiler}/meta.ts +2 -2
  34. package/src/{compiler → pages/compiler}/routes.ts +1 -1
  35. package/src/{compiler → pages/compiler}/types.ts +10 -0
  36. package/src/{compiler → pages/compiler}/vite-plugin.ts +20 -45
  37. package/src/{i18n → pages/i18n}/client.ts +2 -2
  38. package/src/{i18n → pages/i18n}/element.ts +1 -1
  39. package/src/{i18n → pages/i18n}/server.ts +1 -1
  40. package/src/pages/index.ts +28 -0
  41. package/src/{preact → pages/preact}/server.ts +1 -1
  42. package/src/server/adapter/aws.ts +4 -0
  43. package/src/server/adapter/workerd.ts +4 -0
  44. package/src/server/app.ts +91 -0
  45. package/src/server/auth/ability.ts +72 -0
  46. package/src/server/auth/authnz.ts +86 -0
  47. package/src/server/auth/index.ts +5 -0
  48. package/src/server/auth/token.ts +147 -0
  49. package/src/server/auth/types.ts +2 -0
  50. package/src/server/cli/commands/build.ts +41 -0
  51. package/src/server/cli/commands/deploy.ts +81 -0
  52. package/src/server/cli/commands/dev.ts +279 -0
  53. package/src/server/cli/commands/make.ts +98 -0
  54. package/src/server/cli/commands/migrate.ts +93 -0
  55. package/src/server/cli/commands/routes.ts +78 -0
  56. package/src/server/cli/index.ts +27 -0
  57. package/src/server/cli/stubs.ts +39 -0
  58. package/src/server/cli/types.ts +3 -0
  59. package/src/server/cli/utils.ts +437 -0
  60. package/src/server/config.ts +19 -0
  61. package/src/server/db/d1.d.ts +1 -0
  62. package/src/server/db/d1.ts +1 -0
  63. package/src/server/db/dynamodb.d.ts +1 -0
  64. package/src/server/db/dynamodb.ts +1 -0
  65. package/src/server/db/index.ts +1 -0
  66. package/src/server/entry.ts +19 -0
  67. package/src/server/handle.ts +30 -0
  68. package/src/server/middlewares/auth.ts +40 -0
  69. package/src/server/middlewares/logger.ts +30 -0
  70. package/src/server/open-api/register.ts +29 -0
  71. package/src/server/open-api/spec.ts +114 -0
  72. package/src/server/request.ts +265 -0
  73. package/src/server/response.ts +142 -0
  74. package/src/server/routes.ts +473 -0
  75. package/src/server/types.ts +142 -0
  76. package/src/server/utils/environment.ts +17 -0
  77. package/src/server/utils.ts +71 -0
  78. package/src/server/validator.ts +70 -0
  79. package/src/utils.ts +15 -0
  80. package/src/cli/commands/dev-old.ts +0 -213
  81. package/src/cli/index.ts +0 -28
  82. package/src/compiler/utils.ts +0 -5
  83. /package/src/{client → pages/client}/hmr.ts +0 -0
  84. /package/src/{client → pages/client}/island.ts +0 -0
  85. /package/src/{client → pages/client}/link.ts +0 -0
  86. /package/src/{client → pages/client}/storage.ts +0 -0
  87. /package/src/{compiler → pages/compiler}/aliases.ts +0 -0
  88. /package/src/{compiler → pages/compiler}/assets.ts +0 -0
  89. /package/src/{compiler → pages/compiler}/ast.ts +0 -0
  90. /package/src/{compiler → pages/compiler}/constants.ts +0 -0
  91. /package/src/{compiler → pages/compiler}/island.ts +0 -0
  92. /package/src/{components → pages/components}/link.tsx +0 -0
  93. /package/src/{preact → pages/preact}/client-dev.ts +0 -0
  94. /package/src/{preact → pages/preact}/client.ts +0 -0
  95. /package/src/{preact → pages/preact}/context.ts +0 -0
  96. /package/src/{preact → pages/preact}/signals.ts +0 -0
  97. /package/src/{preact → pages/preact}/static-html.ts +0 -0
  98. /package/src/{preact → pages/preact}/types.ts +0 -0
@@ -0,0 +1,473 @@
1
+ import { Glob, file, write } from 'bun'
2
+
3
+ import { copyFileSync } from 'node:fs'
4
+ import { join, relative } from 'pathe'
5
+
6
+ import { camelCase, IMPORT, JSJSON, snakeCase } from 't0n'
7
+ import { rn, substep, warn } from 't0n/log'
8
+ import { config } from 'dotenv'
9
+ import { describeRoute, resolver, validator } from 'hono-openapi'
10
+ import { mimes } from 'hono/utils/mime'
11
+ import { STATUS_CODES } from 'node:http'
12
+ import $ from './app'
13
+ import _response from './response'
14
+ import _validator from './validator'
15
+ import { _root, _server } from '@/utils'
16
+ import { getLastCommitHash, ensureDir, nextId, verbAlias } from './utils'
17
+ import { generateOpenAPI, generateOpenAPIClient, openAPIConfig } from './open-api/spec'
18
+ import { highlightedMethod, highlightedURI } from './cli/utils'
19
+
20
+ import type { Routes, Rule, StandardSchemaV1 } from './types'
21
+ import { autorizedPath } from './middlewares/auth'
22
+ import { isDev } from './utils/environment'
23
+
24
+ export async function* scan(
25
+ dir: string | string[],
26
+ ext: string | string[],
27
+ ignore: string[] = ['*.d.*'],
28
+ cwd?: string,
29
+ ): AsyncGenerator<[string, string, any]> {
30
+ if (typeof dir !== 'string')
31
+ dir = ext.length === 1 ? dir[0] : `{${dir.join(',')}}`
32
+
33
+ if (typeof ext !== 'string')
34
+ ext = ext.length === 1 ? ext[0] : `{${ext.join(',')}}`
35
+
36
+ cwd ??= _root
37
+ for await (const file of new Glob(dir +'/**/*.'+ ext).scan({
38
+ cwd,
39
+ absolute: true,
40
+ onlyFiles: true, // @ts-ignore
41
+ ignore,
42
+ })) {
43
+ yield [file, file.split(cwd as string)[1], await IMPORT(file)]
44
+ }
45
+ }
46
+
47
+ const importName = (name: string | undefined, dirs: string[]) => (name || nextId()).replace(/\.ts$/, '')
48
+
49
+ function isSchema(obj: unknown): obj is StandardSchemaV1 {
50
+ return (
51
+ obj !== null &&
52
+ typeof obj === 'object' &&
53
+ '~standard' in obj &&
54
+ typeof (obj as StandardSchemaV1)['~standard'] === 'object'
55
+ )
56
+ }
57
+
58
+ function withoutChecks(obj: any) {
59
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj
60
+
61
+ for (const attr of [
62
+ 'format', 'pattern',
63
+ 'minLength', 'maxLength',
64
+ 'exclusiveMinimum', 'minimum',
65
+ 'exclusiveMaximum', 'maximum',
66
+ 'multipleOf',
67
+ 'minItems', 'maxItems', 'uniqueItems', 'contains',
68
+ 'minProperties', 'maxProperties', 'patternProperties', 'dependentRequired',
69
+ 'required',
70
+ ]) {
71
+ if (attr in obj)
72
+ delete obj[attr]
73
+ }
74
+
75
+ for (const key of ['items', 'properties']) {
76
+ if (key in obj) {
77
+ for (const prop in obj[key])
78
+ obj[key][prop] = withoutChecks(obj[key][prop])
79
+ }
80
+ }
81
+
82
+ return obj
83
+ }
84
+
85
+ async function ResolveDescribeSchema(obj: any, deep = false) {
86
+ if (!obj || typeof obj !== 'object') return obj
87
+
88
+ if (isSchema(obj)) {
89
+ const oas = await resolver(obj).toOpenAPISchema()
90
+ const cType = oas.schema.properties ? 'application/json' : 'text/plain'
91
+ return { content: { [cType]: {
92
+ schema: withoutChecks(oas.schema),
93
+ // TODO: obter o schema aqui..
94
+ // example:{
95
+ // id: '123',
96
+ // name: 'John Doe',
97
+ // image: 'image.jpg',
98
+ // token: 'dksjdksjdksjdksjd',
99
+ // exp: 987654321,
100
+ // }
101
+ }}}
102
+ }
103
+
104
+ if (obj.content && typeof obj.content === 'object') {
105
+ for (const mediaType in obj.content) {
106
+ const contentItem = obj.content[mediaType]
107
+
108
+ if (contentItem?.schema && isSchema(contentItem.schema)) {
109
+ const oas = await resolver(contentItem.schema).toOpenAPISchema()
110
+ contentItem.schema = withoutChecks(oas.schema)
111
+ // TODO: ler o schema dinamicamente
112
+ // contentItem.example = {
113
+ // id: '123',
114
+ // name: 'John Doe',
115
+ // image: 'image.jpg',
116
+ // token: 'dksjdksjdksjdksjd',
117
+ // exp: 987654321,
118
+ // }
119
+
120
+ }
121
+
122
+ if (mediaType in mimes) {
123
+ obj.content[mimes[mediaType]] = contentItem
124
+ delete obj.content[mediaType]
125
+ }
126
+ }
127
+
128
+ return obj
129
+ }
130
+
131
+ for (const key in obj) {
132
+ if (obj[key] && typeof obj[key] === 'object') {
133
+ obj[key] = await ResolveDescribeSchema(obj[key], true)
134
+
135
+ if (!deep && !obj[key]?.description) {
136
+ const desc = new Response(null, { status: Number(key) }).statusText || STATUS_CODES[key]
137
+ if (desc) obj[key].description = desc
138
+ }
139
+ }
140
+ }
141
+
142
+ return obj
143
+ }
144
+
145
+ let hasDuplicatedRoutes = false
146
+ export async function getRoutes(
147
+ dirs: string[] = ['actions', 'features', 'routes']
148
+ ): Promise<Routes> {
149
+ hasDuplicatedRoutes = false
150
+ const routes: Routes = []
151
+
152
+ let length = 0
153
+ const keys: Set<string> = new Set()
154
+ const bag: Record<string, string[]> = {}
155
+ const _route = (key: string, _: string) => {
156
+ if (!keys.has(key)) {
157
+ keys.add(key)
158
+ } else {
159
+ ; (bag[key] ||= []).push(_)
160
+ length++
161
+ }
162
+ }
163
+
164
+ for await (const [file, path, mod] of scan(dirs, ['ts', 'js'])) {
165
+ if (!mod?.handle && !mod?.handler) continue
166
+
167
+ const method = extractHttpVerb(path)
168
+ const uri = extractHttpPath(path)
169
+
170
+ const noAuth = autorizedPath(uri, method.toUpperCase())
171
+
172
+ const desc = mod?.desc || {}
173
+ const name = desc.summary || desc.name || uri.replace(/^\//, '') || 'index'
174
+
175
+ function parseRules(ruleFn: Function | undefined, fn: Function) {
176
+ const rules = typeof ruleFn === 'function' ? ruleFn(_validator) : null
177
+ return rules ? (Array.isArray(rules) ? rules : [rules]).flatMap(fn as any) : []
178
+ }
179
+
180
+ async function parseBody(ruleFn?: Function) {
181
+ const body = (await Promise.all(parseRules(ruleFn, async (rule: Rule) => {
182
+ if (!['json', 'form'].includes(rule.target))
183
+ return null
184
+
185
+ return { content: { [
186
+ rule.target === 'json'
187
+ ? 'application/json'
188
+ : 'multipart/form-data'
189
+ ]: { ...(await resolver(rule.schema).toOpenAPISchema()) } } }
190
+ }))).filter(Boolean)
191
+
192
+ return body.length === 1 ? body[0] : undefined
193
+ }
194
+
195
+ async function parseParams(ruleFn?: Function) {
196
+ const params = (await Promise.all(parseRules(ruleFn, async (rule: Rule) => {
197
+ if (['json', 'form'].includes(rule.target))
198
+ return null
199
+
200
+ const items = []
201
+ const oas = await resolver(rule.schema).toOpenAPISchema()
202
+
203
+ if (oas?.schema?.properties) {
204
+ const type = rule.target === 'param' ? 'path' : rule.target
205
+ items.push(Object.entries(oas.schema.properties).map(([name, props]) => ({
206
+ schema: props,
207
+ in: type,
208
+ name,
209
+ required: oas.schema.required?.includes(name) || false
210
+ })))
211
+ }
212
+
213
+ return items
214
+ }))).filter(Boolean).flat(Infinity)
215
+
216
+ return params.length ? params : undefined
217
+ }
218
+
219
+
220
+ routes.push({
221
+ method, path: uri,
222
+ name: snakeCase(name),
223
+ file,
224
+ rules: mod.rules,
225
+ handle: mod.handle || mod.handler,
226
+ middlewares: mod?.middlewares?.length ? mod.middlewares.flatMap((obj: any) => {
227
+ return typeof obj === 'string' ? obj : obj?.name || null
228
+ }).filter(Boolean) as Function[] : [],
229
+ desc: {
230
+ ...desc,
231
+ summary: desc.summary || desc.name || camelCase(path.replace(new RegExp(`^/(${dirs.join('|')})/`), '').replace(/\.[^/.]+(?:[.][^/.]+)*$/, '')),
232
+ parameters: await parseParams(mod.rules),
233
+ requestBody: await parseBody(mod.rules),
234
+ ...(noAuth ? {security: []} : {} ),
235
+ responses: {
236
+ ...(noAuth ? {} : {401: {description: 'Unauthorized'}} ),
237
+ 500: { $ref: '#/components/responses/500' },
238
+ ...(await ResolveDescribeSchema(desc?.responses)),
239
+ }
240
+ },
241
+ })
242
+
243
+ const routeKey = method +'|'+ uri
244
+ _route(name, file)
245
+ _route(routeKey, file)
246
+ }
247
+
248
+ if (length) {
249
+ hasDuplicatedRoutes = true
250
+ Object.entries(bag).forEach(([name, paths]) => {
251
+ if (name.includes('|')) {
252
+ let [method, uri] = name.split('|')
253
+ method = method.toUpperCase()
254
+ name = `${highlightedMethod(method, null, true)} "${highlightedURI(uri, method)}"`
255
+ }
256
+
257
+ warn(`Route ${name} has `+ (paths.length > 1 ? `registered ${paths.length} times:` : 'already been registered:'))
258
+ substep(...paths)
259
+ })
260
+
261
+ rn()
262
+ }
263
+
264
+ return sortRoutes(routes)
265
+ }
266
+
267
+ function extractHttpVerb(file: string) {
268
+ if (!file) return 'get'
269
+ const match = file.match(/\.(get|post|put|patch|delete|head|options)\.(?=[jt]s$)/i)
270
+ return match && match[1] ? match[1].toLowerCase() : 'get'
271
+ }
272
+
273
+ function extractHttpPath(file: string) {
274
+ const route = '/'+ file.replace(/\\/g, '/')
275
+ .replace(/^\/(actions|features|routes)/, '')
276
+ .replace(/\.[jt]s$/, '')
277
+ .replace(/\.(get|post|put|patch|delete|head|options)$/i, '')
278
+ .replace(/\/index$/, '')
279
+ .split('/')
280
+ .filter(Boolean)
281
+ .filter(part => !(part.startsWith('(') && part.endsWith(')')))
282
+ .map(part => part.startsWith('[') && part.endsWith(']') ? ':'+ part.slice(1, -1) : part)
283
+ .join('/')
284
+
285
+ return route === '/' ? '/' : route.replace(/\/$/, '')
286
+ }
287
+
288
+ export function sortRoutes(routes: Routes) {
289
+ const metas = new Map<string, { score: number, segmentsCount: number }>()
290
+
291
+ for (const route of routes)
292
+ metas.set(route.path, computeRouteMeta(route.path))
293
+
294
+ const list = routes.sort((a, b) => {
295
+ const metaA = metas.get(a.path)!
296
+ const metaB = metas.get(b.path)!
297
+
298
+ if (metaA.score === metaB.score)
299
+ return metaB.segmentsCount - metaA.segmentsCount
300
+
301
+ return metaB.score - metaA.score
302
+ })
303
+
304
+ if (list.length < 2) return list
305
+
306
+ const toMove = []
307
+ while (list.length && list.at(-1)?.path === '/') {
308
+ toMove.push(list.pop())
309
+ } // @ts-ignore
310
+ toMove.length && list.unshift(...toMove)
311
+ // while (list.length && list.at(-1)?.path === '/') {
312
+ // const last = list.pop()
313
+ // last && list.unshift(last)
314
+ // }
315
+
316
+ return list
317
+ }
318
+
319
+ function computeRouteMeta(path: string) {
320
+ const segments = path.split('/').filter(Boolean)
321
+
322
+ let score = 0
323
+ for (const segment of segments) {
324
+ if (segment === '*') {
325
+ continue
326
+ } else if (segment.startsWith(':')) {
327
+ score += 1
328
+ } else {
329
+ score += 10
330
+ }
331
+ }
332
+
333
+ return { score, segmentsCount: segments.length }
334
+ }
335
+
336
+ export async function getMiddlewares(dir: string = 'middlewares'): Promise<Routes> {
337
+ const mw: Routes = []
338
+
339
+ for await (const [file, path, mod] of scan(dir, ['ts', 'js'])) {
340
+ if (!mod?.handle && !mod?.handler) continue
341
+ // @ts-ignore
342
+ mw.push({
343
+ path: mod?.path || '*',
344
+ name: snakeCase(path.replace(/\\/g, '/')
345
+ .replace(new RegExp(`^\/(${dir})/`), '')
346
+ .replace(/\.[jt]s$/, '')
347
+ .replace(/\/index$/, '')
348
+ ),
349
+ file,
350
+ handle: mod.handle || mod.handler,
351
+ })
352
+ }
353
+
354
+ return mw
355
+ }
356
+
357
+ export async function getConfigs(dir: string = 'configs'): Promise<Record<string, any>> {
358
+ if (!join(_root, dir)) return {}
359
+ const configs: Record<string, any> = {}
360
+
361
+ for await (const [file, path, mod] of scan(dir, ['ts', 'js', 'cjs', 'mjs', 'json'])) {
362
+ const keyPath = path.replace(/\.[^/.]+$/, '').replaceAll('/', '.').split('.').slice(2)
363
+
364
+ keyPath.reduce((acc, key, index) => {
365
+ if (index === keyPath.length - 1) {
366
+ acc[key] = mod.default ?? mod
367
+ } else if (!acc[key] || typeof acc[key] !== 'object') {
368
+ acc[key] = {}
369
+ }
370
+
371
+ return acc[key]
372
+ }, configs)
373
+ }
374
+
375
+ return configs
376
+ }
377
+
378
+ export async function dependencyEntry(lib: string, root: string) {
379
+ const path = await import.meta.resolve(lib)
380
+ return relative(root, path.replace('file://', ''))
381
+ }
382
+
383
+ async function dependencyPath(lib: string) {
384
+ const entry = await dependencyEntry(lib, join(_root, '.duto'))
385
+ return entry.substring(0, entry.lastIndexOf(lib) + lib.length)
386
+ }
387
+
388
+ export async function cacheRoutes() {
389
+ const env = Object.entries(
390
+ config({ path: join(_root, '.env.prod') })?.parsed || {}
391
+ ).filter(([key, val]) => !key?.toLowerCase().startsWith('aws')) // prevent AWS credentials
392
+
393
+ const version = getLastCommitHash()
394
+ if (version) {
395
+ env.push(['VERSION_SHA', process.env['VERSION_SHA'] = version])
396
+ env.push(['VERSION_HASH', process.env['VERSION_HASH'] = version?.substring(0, 7)])
397
+ }
398
+
399
+ const rolePath = join(_root, 'configs/roles.ts')
400
+ ensureDir(rolePath)
401
+ const roleFile = file(rolePath)
402
+ if (!(await roleFile.exists()))
403
+ await write(rolePath, `export default {\n\n}`)
404
+
405
+ const configs = await getConfigs()
406
+ const middlewares = await getMiddlewares()
407
+ if (isDev()) {
408
+ const _file = join(_server, 'middlewares', 'logger.ts')
409
+ const _mod = await IMPORT(_file) // @ts-ignore
410
+ middlewares.unshift({
411
+ path: _mod?.path || '*',
412
+ name: 'dev_logger',
413
+ file: _file,
414
+ handle: _mod.handle,
415
+ })
416
+ }
417
+
418
+ const routes = await getRoutes()
419
+ if (hasDuplicatedRoutes)
420
+ throw new Error("The app can't build with duplicate routes")
421
+
422
+ // _validator.setParser((rule: Rule) => validator(rule.target, rule.schema, (result, c) => {
423
+ // if (!result.success) // @ts-ignore
424
+ // return _response.badRequest(result.error)
425
+ // }))
426
+
427
+ const oasConfig = openAPIConfig(configs.duto)
428
+ if (!oasConfig.disable) {
429
+ // @ts-ignore
430
+ const app = $({ routes, routeRegister: (app: Hono, route: Route) => {
431
+ app[route.method](route.path, describeRoute(route.desc))
432
+ // app[route.method](route.path, describeRoute(route.desc), ...resolveHandle(route.middlewares, route.handle, route.rules))
433
+ } })
434
+
435
+ configs.DUTO_OAS = JSON.stringify(await generateOpenAPI(app, oasConfig))
436
+ configs.DUTO_OAC = await generateOpenAPIClient(app, oasConfig)
437
+ configs.duto.docs = {path: oasConfig.path, auth: oasConfig?.auth}
438
+ }
439
+
440
+ const iPath = join(_root, '.duto/imports.mjs')
441
+ ensureDir(iPath)
442
+
443
+ const localfireEntry = await dependencyEntry('localflare-api', _root)
444
+ copyFileSync(localfireEntry, join(_root, '.duto/localfire.js'))
445
+
446
+ const _dutoDir = await dependencyPath('duto')
447
+
448
+ await write(iPath, `// AUTO-GENERATED FILE - DO NOT EDIT
449
+ ${env?.length ? `import { addEnv } from '${await dependencyPath('t0n')}/src/env'\naddEnv(${JSJSON(Object.fromEntries(env))})` : ''}
450
+ ${Object.entries(configs)?.length ? `import { addConfig } from '${_dutoDir}/src/server/config'\naddConfig(${JSJSON(configs)})` : ''}
451
+
452
+ ${middlewares.map(r => `import { handle as MW${r.name} } from '../${normalizeImportPath(r.file)}'`).join('\n')}
453
+ export const middlewares = [[${middlewares.map(mw => [
454
+ JSJSON(mw.path || false),
455
+ 'MW'+ mw.name,
456
+ JSJSON(mw.name),
457
+ ]).join('], [')}]]
458
+
459
+ ${routes.map(r => `import { handle as ${r.name}${r.rules ? ', rules as R'+r.name : ''} } from '../${normalizeImportPath(r.file)}'`).join('\n')}
460
+
461
+ export const routes = [[${routes.filter(r => r.method && r.path).map(route => [
462
+ verbAlias[route.method],
463
+ JSJSON(route.path),
464
+ JSJSON(route.middlewares),
465
+ ...(route.rules ? [route.name, 'R' + route.name] : [route.name]),
466
+ JSJSON(route.name)
467
+ ]).join('], [')}]]
468
+ `)
469
+ }
470
+
471
+ function normalizeImportPath(file: string) {
472
+ return relative(_root, file.replace(/\.tsx?$/i, '').replace(/(\/index)+$/i, '').replace(/\/+$/g, ''))
473
+ }
@@ -0,0 +1,142 @@
1
+ import type {
2
+ Hono, Env, Handler,
3
+ ErrorHandler, NotFoundHandler,
4
+ ValidationTargets,
5
+ // MiddlewareHandler,
6
+ } from 'hono'
7
+ import type { ResponseHeader } from 'hono/utils/headers'
8
+ // import type { StatusCode } from 'hono/utils/http-status'
9
+ import { mimes, type BaseMime } from 'hono/utils/mime'
10
+ import type { OpenAPIV3_1, OpenAPIV3 } from 'openapi-types'
11
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
12
+ import type { DescribeRouteOptions as RawDescribeRouteOptions, ResolverReturnType } from 'hono-openapi'
13
+ import type * as z from 'zod'
14
+ import type zm from 'zod/mini'
15
+ // import Action from './action'
16
+ import request from './request'
17
+ import response from './response'
18
+ import validator from './validator'
19
+
20
+ // export type { H, Handler, HandlerResponse } from 'hono/types'
21
+ export type {
22
+ Hono,
23
+ Env, Context, Next,
24
+ // ErrorHandler, NotFoundHandler,
25
+ MiddlewareHandler, // TODO: remove..
26
+ ValidationTargets,
27
+ } from 'hono'
28
+ export type { HTTPResponseError } from 'hono/types'
29
+
30
+ export type {
31
+ ContentfulStatusCode,
32
+ RedirectStatusCode,
33
+ StatusCode,
34
+ } from 'hono/utils/http-status'
35
+ export type { BaseMime, StandardSchemaV1 }
36
+
37
+ export type zObject = z.ZodTypeAny | zm.ZodMiniObject
38
+ type PublicMethods<T> = {
39
+ [K in keyof T]: K extends `#${string}` | `$${string}` | symbol | 'prototype' ? never : K
40
+ }[keyof T]
41
+
42
+ export type IRequest = Pick<request, PublicMethods<request>>
43
+ export type IResponse = Pick<typeof response, PublicMethods<typeof response>>
44
+
45
+ export type IValidator = Pick<typeof validator, PublicMethods<typeof validator>>
46
+ export type Rule = {
47
+ schema: zObject
48
+ target: keyof ValidationTargets
49
+ eTarget?: 'fieldErrors' | 'formErrors'
50
+ }
51
+ export type Rules = Rule[] | Rule | null
52
+ export type RuleFn = (schema: zObject) => Rule
53
+ export type RuleFactory = (validator: IValidator) => Rules
54
+
55
+ export type StandardSchema = StandardSchemaV1 | OpenAPIV3_1.ReferenceObject
56
+ export type DescribeRouteOptions = Omit<RawDescribeRouteOptions, 'responses'> & {
57
+ responses?: {
58
+ [key: string | number]: (Omit<OpenAPIV3.ResponseObject, 'description' | 'headers' | 'content' | 'links'> & {
59
+ description?: string,
60
+ headers?: { // TODO maybe dont accept ResolverReturnType
61
+ [header: string]: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.HeaderObject
62
+ },
63
+ content?: {
64
+ [key: BaseMime | keyof typeof mimes]: Omit<OpenAPIV3_1.MediaTypeObject, 'schema'> & {
65
+ schema?: StandardSchema | OpenAPIV3_1.SchemaObject | ResolverReturnType,
66
+ },
67
+ },
68
+ links?: { // TODO maybe dont accept ResolverReturnType
69
+ [link: string]: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.LinkObject
70
+ },
71
+ }) | StandardSchema,
72
+ },
73
+ }
74
+
75
+ export type Route = {
76
+ method: string,
77
+ path: string,
78
+ name: string,
79
+ file: string,
80
+ middlewares: Function[],
81
+ handle: Handlers,
82
+ rules?: Rules,
83
+ desc: DescribeRouteOptions,
84
+ }
85
+
86
+ // export type ActionType = Function | Handler | Action | (new () => Action)
87
+
88
+ export type Handle = Function | Handler
89
+ export type Handlers = Handle[]
90
+ // export type Handlers = (Function | Handler | (new () => Action))[]
91
+
92
+ export type Routes = Route[]
93
+
94
+ export type HeaderRecord =
95
+ | Record<'Content-Type', BaseMime>
96
+ | Record<ResponseHeader, string | string[]>
97
+ | Record<string, string | string[]>
98
+
99
+
100
+ export type InitFunction<E extends Env = Env> = (app: Hono<E>) => void
101
+ export type ServerOptions<E extends Env = Env> = Partial<{
102
+ routes: Routes,
103
+ middlewares: Routes[],
104
+ notFound: NotFoundHandler<E>,
105
+ onError: ErrorHandler<E>,
106
+ root: string,
107
+ app?: Hono<E>,
108
+ init?: InitFunction<E>,
109
+ }>
110
+
111
+ // export type MiddlewareType = MiddlewareHandler | Middleware | (new () => Middleware)
112
+
113
+ export type Errors = Record<string, string | string[]>
114
+ export type ErrorResponse = {
115
+ m?: string, // message
116
+ e?: Errors, // error bag
117
+ }
118
+
119
+ // TODO: not used..
120
+
121
+ // export type LambdaResponse = {
122
+ // statusCode: StatusCode,
123
+ // body: string,
124
+ // }
125
+
126
+ // export type ResponseHeadersInit = [
127
+ // string,
128
+ // string
129
+ // ][] | Record<"Content-Type", BaseMime> | Record<ResponseHeader, string> | Record<string, string> | Headers
130
+ // export type ResponseInit<T extends StatusCode = StatusCode> = {
131
+ // headers?: ResponseHeadersInit,
132
+ // status?: T,
133
+ // statusText?: string,
134
+ // }
135
+ // export type ResponseOrInit<T extends StatusCode = StatusCode> = ResponseInit<T> | Response
136
+ // export type JSONValue =
137
+ // | string
138
+ // | number
139
+ // | boolean
140
+ // | null
141
+ // | { [key: string]: JSONValue }
142
+ // | JSONValue[]
@@ -0,0 +1,17 @@
1
+ export const $prd = Symbol('prd')
2
+ export const $dev = Symbol('dev')
3
+
4
+ let env = $prd
5
+
6
+ export const getEnv = () => env
7
+ export const setEnv = (e: symbol) => env = e
8
+
9
+ export const isEnv = (e: symbol) => env === e
10
+
11
+ export const isDev = () => env === $dev
12
+ export const isDevelop = isDev
13
+ export const isDevelopment = isDev
14
+
15
+ export const isPrd = () => env === $prd
16
+ export const isProd = isPrd
17
+ export const isProduction = isPrd
@@ -0,0 +1,71 @@
1
+ import { mkdirSync, existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'pathe'
3
+ import { _root } from '@/utils'
4
+
5
+ export function getLastCommitHash(path: string = '.git') {
6
+ try {
7
+ const gitDir = join(_root, path)
8
+
9
+ let headContent = readFileSync(join(gitDir, 'HEAD'), 'utf8').trim()
10
+
11
+ if (headContent.startsWith('ref:'))
12
+ headContent = readFileSync(join(gitDir, headContent.substring(5)), 'utf8').trim()
13
+
14
+ return headContent
15
+ } catch (e) {
16
+ return null
17
+ }
18
+ }
19
+
20
+ export function ensureDir(filePath: string) {
21
+ const dir = dirname(filePath)
22
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
23
+ }
24
+
25
+
26
+
27
+ let id = '';
28
+ export const nextId = () => {
29
+ const chars = id.split('')
30
+ let i = chars.length - 1
31
+
32
+ while (i >= 0) {
33
+ if (chars[i] === 'z') {
34
+ chars[i] = 'A'
35
+ i--
36
+ } else if (chars[i] === 'Z') {
37
+ chars[i] = 'a'
38
+ i--
39
+ } else {
40
+ chars[i] = String.fromCharCode(chars[i].charCodeAt(0) + 1)
41
+ return id = chars.join('')
42
+ }
43
+ }
44
+
45
+ return id = 'a' + chars.join('')
46
+ }
47
+
48
+ export const verbAlias = {
49
+ get: 0,
50
+ post: 1,
51
+ put: 2,
52
+ patch: 3,
53
+ delete: 4,
54
+ head: 5,
55
+ options: 6,
56
+ connect: 7,
57
+ trace: 8,
58
+ } as Record<string, number>
59
+
60
+ export const getVerb = [
61
+ 'get',
62
+ 'post',
63
+ 'put',
64
+ 'patch',
65
+ 'delete',
66
+ 'head',
67
+ 'options',
68
+ 'connect',
69
+ 'trace',
70
+ ]
71
+