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