jax-hono 1.0.19 → 1.0.21
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 +32 -32
- package/bin/jax.js +66 -66
- package/bin/jax.ts +8 -8
- package/build/build.ts +19 -19
- package/build/generate-config.ts +122 -122
- package/build/generate-registry.ts +902 -902
- package/build/generated-path.ts +20 -20
- package/config.ts +86 -83
- package/core/api-controller.ts +290 -290
- package/core/app.ts +207 -207
- package/core/controller.ts +85 -85
- package/core/generated.ts +118 -96
- package/core/hono.ts +63 -63
- package/core/service.ts +44 -44
- package/docs/agent.md +169 -167
- package/docs/jax.md +329 -327
- package/helpers/config.ts +23 -23
- package/helpers/crud.ts +27 -27
- package/helpers/index.ts +4 -4
- package/helpers/route.ts +7 -7
- package/index.ts +121 -121
- package/middleware/api-response.ts +44 -44
- package/middleware/jwt.ts +90 -90
- package/middleware/request-log.ts +3 -3
- package/package.json +129 -124
- package/plugins/config.ts +8 -8
- package/plugins/define.ts +5 -5
- package/plugins/mongoose/index.ts +57 -57
- package/plugins/mongoose/model.ts +322 -322
- package/plugins/mongoose/schema.ts +30 -30
- package/plugins/session/index.ts +86 -86
- package/setup.ts +105 -105
- package/types/config.ts +9 -9
- package/types/context.ts +9 -9
- package/types/index.ts +2 -2
- package/utils/array.ts +7 -7
- package/utils/async.ts +47 -47
- package/utils/crypto.ts +6 -6
- package/utils/http.ts +106 -0
- package/utils/index.ts +3 -2
- package/utils/regexp.ts +14 -14
- package/utils/transform.ts +3 -3
|
@@ -1,902 +1,902 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { cwd } from 'node:process'
|
|
4
|
-
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
|
|
5
|
-
import { ensureGeneratedDir, generatedDir, packageDir, projectDir } from './generated-path'
|
|
6
|
-
|
|
7
|
-
const srcDir = join(cwd(), 'src')
|
|
8
|
-
const projectRequire = createRequire(join(projectDir, 'package.json'))
|
|
9
|
-
const moduleExtensions = new Set(['.ts', '.js', '.mjs', '.cjs'])
|
|
10
|
-
const generatedHeader = `// This file is generated by jax-hono/build/generate-registry.ts.
|
|
11
|
-
// Do not edit it directly.
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
type ControllerGroup = {
|
|
15
|
-
name: string
|
|
16
|
-
dirs: string[]
|
|
17
|
-
suffix?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type PluginConfigItem =
|
|
21
|
-
| false
|
|
22
|
-
| {
|
|
23
|
-
enable?: boolean
|
|
24
|
-
package?: string
|
|
25
|
-
options?: unknown
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type PluginConfig = Record<string, PluginConfigItem>
|
|
29
|
-
|
|
30
|
-
type EnabledPlugin = {
|
|
31
|
-
name: string
|
|
32
|
-
packageName: string
|
|
33
|
-
rootDir: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const controllerGroups: ControllerGroup[] = [
|
|
37
|
-
{ name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
|
|
38
|
-
{ name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
|
|
39
|
-
{ name: 'open', dirs: ['open', 'controllers/open', 'controller/open', 'modules'], suffix: '.open' },
|
|
40
|
-
{ name: 'client', dirs: ['client', 'controllers/client', 'controller/client', 'modules'], suffix: '.client' },
|
|
41
|
-
{ name: 'controller', dirs: ['controllers', 'controller', 'modules'], suffix: '.controller' },
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
const scopedControllerGroups = new Set(
|
|
45
|
-
controllerGroups
|
|
46
|
-
.filter((group) => group.name !== 'controller')
|
|
47
|
-
.map((group) => group.name),
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
function walkFiles(dir: string): string[] {
|
|
51
|
-
if (!existsSync(dir)) {
|
|
52
|
-
return []
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const files: string[] = []
|
|
56
|
-
|
|
57
|
-
for (const item of readdirSync(dir, { withFileTypes: true })) {
|
|
58
|
-
const itemPath = join(dir, item.name)
|
|
59
|
-
|
|
60
|
-
if (item.isDirectory()) {
|
|
61
|
-
files.push(...walkFiles(itemPath))
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (item.isFile() && moduleExtensions.has(parse(item.name).ext)) {
|
|
66
|
-
files.push(itemPath)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return files
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function writeGeneratedFile(fileName: string, content: string) {
|
|
74
|
-
const outputFile = join(generatedDir, fileName)
|
|
75
|
-
|
|
76
|
-
ensureGeneratedDir()
|
|
77
|
-
|
|
78
|
-
if (existsSync(outputFile) && readFileSync(outputFile, 'utf8') === content) {
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
writeFileSync(outputFile, content)
|
|
83
|
-
console.log(`Generated ${relative(projectDir, outputFile)}.`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function toImportPath(filePath: string) {
|
|
87
|
-
const path = filePath
|
|
88
|
-
.replace(parse(filePath).ext, '')
|
|
89
|
-
.replaceAll('\\', '/')
|
|
90
|
-
|
|
91
|
-
return path
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function toIdentifier(parts: string[]) {
|
|
95
|
-
const value = parts
|
|
96
|
-
.join('_')
|
|
97
|
-
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
98
|
-
.replace(/^([^a-zA-Z_$])/, '_$1')
|
|
99
|
-
|
|
100
|
-
return value || 'module'
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function toLowerCamelIdentifier(parts: string[], suffix = '') {
|
|
104
|
-
const words = parts
|
|
105
|
-
.join('/')
|
|
106
|
-
.split(/[^a-zA-Z0-9_$]+/)
|
|
107
|
-
.filter(Boolean)
|
|
108
|
-
const value = words
|
|
109
|
-
.map((word, index) => {
|
|
110
|
-
const normalized = toCamelCase(word)
|
|
111
|
-
|
|
112
|
-
return index === 0
|
|
113
|
-
? normalized.charAt(0).toLowerCase() + normalized.slice(1)
|
|
114
|
-
: toPascalCase(normalized)
|
|
115
|
-
})
|
|
116
|
-
.join('')
|
|
117
|
-
|
|
118
|
-
return toIdentifier([`${value}${suffix}`])
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function toCamelCase(value: string) {
|
|
122
|
-
return value.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase())
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function toPascalCase(value: string) {
|
|
126
|
-
const camel = toCamelCase(value)
|
|
127
|
-
|
|
128
|
-
return camel.charAt(0).toUpperCase() + camel.slice(1)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function toServiceKey(name: string) {
|
|
132
|
-
const baseName = name.endsWith('Service')
|
|
133
|
-
? name.slice(0, -'Service'.length)
|
|
134
|
-
: name
|
|
135
|
-
|
|
136
|
-
return baseName.replace(/^./, (char) => char.toLowerCase())
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function toMiddlewareKey(exportName: string) {
|
|
140
|
-
return exportName.endsWith('Middleware')
|
|
141
|
-
? exportName.slice(0, -'Middleware'.length)
|
|
142
|
-
: exportName
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function stripKnownSuffix(modulePath: string, suffixes: string[]) {
|
|
146
|
-
for (const suffix of suffixes) {
|
|
147
|
-
if (modulePath.endsWith(suffix)) {
|
|
148
|
-
return modulePath.slice(0, -suffix.length)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return modulePath
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function normalizeModulePath(modulePath: string) {
|
|
156
|
-
const parts = modulePath.split('/')
|
|
157
|
-
const last = parts.at(-1)
|
|
158
|
-
const parent = parts.at(-2)
|
|
159
|
-
|
|
160
|
-
if (!last || !parent) {
|
|
161
|
-
return modulePath
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (last === parent) {
|
|
165
|
-
return parts.slice(0, -1).join('/')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (last.startsWith(`${parent}-`) || last.startsWith(`${parent}_`)) {
|
|
169
|
-
return [...parts.slice(0, -2), last].join('/')
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return modulePath
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
176
|
-
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
177
|
-
.replace(parse(filePath).ext, '')
|
|
178
|
-
.replaceAll('\\', '/')
|
|
179
|
-
const withoutIndex = modulePath.endsWith('/index')
|
|
180
|
-
? modulePath.slice(0, -'/index'.length)
|
|
181
|
-
: modulePath
|
|
182
|
-
|
|
183
|
-
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
184
|
-
|
|
185
|
-
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function toPluginModulePath(pluginName: string, rootDir: string, filePath: string, suffixes: string[] = []) {
|
|
189
|
-
const modulePath = relative(rootDir, filePath)
|
|
190
|
-
.replace(parse(filePath).ext, '')
|
|
191
|
-
.replaceAll('\\', '/')
|
|
192
|
-
const withoutIndex = modulePath.endsWith('/index')
|
|
193
|
-
? modulePath.slice(0, -'/index'.length)
|
|
194
|
-
: modulePath
|
|
195
|
-
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
196
|
-
|
|
197
|
-
return normalizeModulePath(stripped ? `${pluginName}/${stripped}` : pluginName)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function uniqueSorted(values: string[]) {
|
|
201
|
-
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function getFilesFromDirs(dirs: string[]) {
|
|
205
|
-
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function getEnabledPluginFiles(suffix: string) {
|
|
209
|
-
return getEnabledPlugins().flatMap((plugin) => {
|
|
210
|
-
return walkFiles(plugin.rootDir)
|
|
211
|
-
.filter((filePath) => isSuffixedModuleFile(filePath, suffix))
|
|
212
|
-
.map((filePath) => ({ ...plugin, filePath }))
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isFileInDir(filePath: string, dirName: string) {
|
|
217
|
-
const rel = relative(join(srcDir, dirName), filePath)
|
|
218
|
-
|
|
219
|
-
return rel && !rel.startsWith('..')
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function isSuffixedModuleFile(filePath: string, suffix: string) {
|
|
223
|
-
return relative(srcDir, filePath)
|
|
224
|
-
.replace(parse(filePath).ext, '')
|
|
225
|
-
.replaceAll('\\', '/')
|
|
226
|
-
.endsWith(suffix)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function isModuleFeatureFile(filePath: string, suffix: string) {
|
|
230
|
-
return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function hasDefaultExport(filePath: string) {
|
|
234
|
-
return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function indentObject(content: string, spaces = 2) {
|
|
238
|
-
if (!content) {
|
|
239
|
-
return ''
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const indent = ' '.repeat(spaces)
|
|
243
|
-
|
|
244
|
-
return content
|
|
245
|
-
.split('\n')
|
|
246
|
-
.map((line) => `${indent}${line}`)
|
|
247
|
-
.join('\n')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function buildNestedObject(entries: { path: string[]; value: string }[]): string {
|
|
251
|
-
const groups = new Map<string, { path: string[]; value: string }[]>()
|
|
252
|
-
const leaves: { key: string; value: string }[] = []
|
|
253
|
-
|
|
254
|
-
for (const entry of entries) {
|
|
255
|
-
const [head, ...tail] = entry.path
|
|
256
|
-
|
|
257
|
-
if (!head) {
|
|
258
|
-
continue
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (tail.length === 0) {
|
|
262
|
-
leaves.push({ key: head, value: entry.value })
|
|
263
|
-
continue
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
groups.set(head, [...(groups.get(head) ?? []), { path: tail, value: entry.value }])
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const lines = [
|
|
270
|
-
...leaves.map((leaf) => `${toObjectKey(leaf.key)}: ${leaf.value},`),
|
|
271
|
-
...[...groups.entries()].map(([key, groupEntries]) => {
|
|
272
|
-
return `${toObjectKey(key)}: {\n${indentObject(buildNestedObject(groupEntries), 2)}\n},`
|
|
273
|
-
}),
|
|
274
|
-
]
|
|
275
|
-
|
|
276
|
-
return lines.join('\n')
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function generateControllersRegistry() {
|
|
280
|
-
const used = new Set<string>()
|
|
281
|
-
const imports: string[] = []
|
|
282
|
-
const controllerExports: string[] = []
|
|
283
|
-
const groupLines: string[] = []
|
|
284
|
-
|
|
285
|
-
for (const group of controllerGroups) {
|
|
286
|
-
const projectEntries = getFilesFromDirs(group.dirs)
|
|
287
|
-
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
288
|
-
.filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
|
|
289
|
-
.filter(hasDefaultExport)
|
|
290
|
-
.map((filePath, index) => {
|
|
291
|
-
const baseDir = group.dirs.find((dirName) => {
|
|
292
|
-
return isFileInDir(filePath, dirName)
|
|
293
|
-
}) ?? group.dirs[0]
|
|
294
|
-
const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
|
|
295
|
-
const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
|
|
296
|
-
const exportName = toLowerCamelIdentifier(group.name === 'controller' ? [modulePath] : [group.name, modulePath], 'Controller')
|
|
297
|
-
|
|
298
|
-
return { modulePath, importName, exportName, filePath }
|
|
299
|
-
})
|
|
300
|
-
const pluginEntries = group.name === 'controller'
|
|
301
|
-
? getEnabledPluginFiles('.controller')
|
|
302
|
-
.filter(({ filePath }) => basename(filePath) !== 'index.ts')
|
|
303
|
-
.filter(({ filePath }) => hasDefaultExport(filePath))
|
|
304
|
-
.map(({ name, rootDir, filePath }, index) => {
|
|
305
|
-
const modulePath = toPluginModulePath(name, rootDir, filePath, ['.controller'])
|
|
306
|
-
const importName = toIdentifier([group.name, 'plugin', modulePath, String(index), 'Controller'])
|
|
307
|
-
const exportName = toLowerCamelIdentifier([modulePath], 'Controller')
|
|
308
|
-
|
|
309
|
-
return { modulePath, importName, exportName, filePath }
|
|
310
|
-
})
|
|
311
|
-
: []
|
|
312
|
-
const entries = [...projectEntries, ...pluginEntries]
|
|
313
|
-
.filter((entry) => {
|
|
314
|
-
if (group.name !== 'controller') {
|
|
315
|
-
return true
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const [scope] = entry.modulePath.split('/')
|
|
319
|
-
|
|
320
|
-
return !scopedControllerGroups.has(scope)
|
|
321
|
-
})
|
|
322
|
-
.filter((entry) => {
|
|
323
|
-
const key = `${group.name}:${entry.modulePath}`
|
|
324
|
-
|
|
325
|
-
if (used.has(key)) {
|
|
326
|
-
return false
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
used.add(key)
|
|
330
|
-
|
|
331
|
-
return true
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
|
|
335
|
-
controllerExports.push(...entries.map((entry) => `export const ${entry.exportName} = createController(${entry.importName})`))
|
|
336
|
-
|
|
337
|
-
if (entries.length === 0) {
|
|
338
|
-
continue
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const objectBody = buildNestedObject(entries.map((entry) => ({
|
|
342
|
-
path: entry.modulePath.split('/').map(toCamelCase),
|
|
343
|
-
value: entry.exportName,
|
|
344
|
-
})))
|
|
345
|
-
|
|
346
|
-
if (group.name === 'controller') {
|
|
347
|
-
groupLines.push(indentObject(objectBody, 2))
|
|
348
|
-
} else {
|
|
349
|
-
groupLines.push(` ${group.name}: {\n${indentObject(objectBody, 4)}\n },`)
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const helperImport = imports.length > 0
|
|
354
|
-
? `import { createController } from 'jax-hono/core/controller'\n`
|
|
355
|
-
: ''
|
|
356
|
-
const content = `${generatedHeader}
|
|
357
|
-
${helperImport}${imports.join('\n')}
|
|
358
|
-
|
|
359
|
-
${controllerExports.join('\n')}
|
|
360
|
-
|
|
361
|
-
export const controllers = {
|
|
362
|
-
${groupLines.join('\n')}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
declare module 'jax-hono/core/generated' {
|
|
366
|
-
interface JaxGenerated {
|
|
367
|
-
controllers: typeof controllers
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
`
|
|
371
|
-
|
|
372
|
-
writeGeneratedFile('controllers.generated.ts', content)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function toObjectKey(value: string) {
|
|
376
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function formatExportBlock(names: string[]) {
|
|
380
|
-
if (names.length === 0) {
|
|
381
|
-
return ''
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return `export {\n${names.map((name) => ` ${name},`).join('\n')}\n}\n`
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function createGeneratedContent(sections: string[]) {
|
|
388
|
-
return `${generatedHeader}\n${sections
|
|
389
|
-
.map((section) => section.trimEnd())
|
|
390
|
-
.filter((section) => section.trim())
|
|
391
|
-
.join('\n\n')}\n`
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function generateMiddlewaresRegistry() {
|
|
395
|
-
const files = getFilesFromDirs(['middlewares', 'middleware', 'modules'])
|
|
396
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.middleware'))
|
|
397
|
-
const imports: string[] = []
|
|
398
|
-
const entries: string[] = []
|
|
399
|
-
const exportNames = new Set<string>()
|
|
400
|
-
|
|
401
|
-
files.forEach((filePath) => {
|
|
402
|
-
const source = readFileSync(filePath, 'utf8')
|
|
403
|
-
const middlewareNames = [...source.matchAll(/\bexport\s+(?:const|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
|
|
404
|
-
.map((match) => match[1])
|
|
405
|
-
.sort((left, right) => left.localeCompare(right))
|
|
406
|
-
|
|
407
|
-
if (middlewareNames.length === 0) {
|
|
408
|
-
return
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
imports.push(`import { ${middlewareNames.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
412
|
-
middlewareNames.forEach((name) => exportNames.add(name))
|
|
413
|
-
entries.push(...middlewareNames.map((name) => {
|
|
414
|
-
const key = toMiddlewareKey(name)
|
|
415
|
-
|
|
416
|
-
return key === name
|
|
417
|
-
? ` ${name},`
|
|
418
|
-
: ` ${key}: ${name},`
|
|
419
|
-
}))
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
423
|
-
const content = createGeneratedContent([
|
|
424
|
-
imports.join('\n'),
|
|
425
|
-
exportBlock,
|
|
426
|
-
`export const middlewares = {
|
|
427
|
-
${entries.join('\n')}
|
|
428
|
-
}`,
|
|
429
|
-
])
|
|
430
|
-
|
|
431
|
-
writeGeneratedFile('middlewares.generated.ts', content)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function generateModelsRegistry() {
|
|
435
|
-
const files = getFilesFromDirs(['models', 'model', 'modules'])
|
|
436
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.model'))
|
|
437
|
-
const imports: string[] = []
|
|
438
|
-
const entries: string[] = []
|
|
439
|
-
const exportNames = new Set<string>()
|
|
440
|
-
|
|
441
|
-
files.forEach((filePath) => {
|
|
442
|
-
const source = readFileSync(filePath, 'utf8')
|
|
443
|
-
const exportedModels = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*create(?:Loose)?Model\s*\(/g)]
|
|
444
|
-
.map((match) => match[1])
|
|
445
|
-
.sort((left, right) => left.localeCompare(right))
|
|
446
|
-
|
|
447
|
-
if (exportedModels.length === 0) {
|
|
448
|
-
return
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const modelKey = source.match(/\bexport\s+const\s+modelKey\s*=\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]/)?.[1]
|
|
452
|
-
const fileModelName = toPascalCase(toModulePath(
|
|
453
|
-
filePath.includes(`${srcDir}/modules/`) ? 'modules' : basename(parse(filePath).dir),
|
|
454
|
-
filePath,
|
|
455
|
-
['.model'],
|
|
456
|
-
))
|
|
457
|
-
|
|
458
|
-
imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
459
|
-
exportedModels.forEach((name) => exportNames.add(name))
|
|
460
|
-
entries.push(...exportedModels.map((name) => {
|
|
461
|
-
const key = exportedModels.length === 1 && modelKey
|
|
462
|
-
? modelKey
|
|
463
|
-
: name === `${fileModelName}Model`
|
|
464
|
-
? fileModelName
|
|
465
|
-
: name
|
|
466
|
-
|
|
467
|
-
return key === name
|
|
468
|
-
? ` ${name},`
|
|
469
|
-
: ` ${key}: ${name},`
|
|
470
|
-
}))
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
474
|
-
|
|
475
|
-
const content = `${generatedHeader}
|
|
476
|
-
${imports.join('\n')}
|
|
477
|
-
|
|
478
|
-
${exportBlock}
|
|
479
|
-
export const models = {
|
|
480
|
-
${entries.join('\n')}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export type Models = typeof models
|
|
484
|
-
|
|
485
|
-
declare module 'jax-hono/core/generated' {
|
|
486
|
-
interface JaxGenerated {
|
|
487
|
-
models: typeof models
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
`
|
|
491
|
-
|
|
492
|
-
writeGeneratedFile('models.generated.ts', content)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function generateServicesRegistry() {
|
|
496
|
-
const files = [
|
|
497
|
-
...getFilesFromDirs(['services', 'service', 'modules'])
|
|
498
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.service'))
|
|
499
|
-
.map((filePath) => ({ filePath })),
|
|
500
|
-
...getEnabledPluginFiles('.service'),
|
|
501
|
-
]
|
|
502
|
-
const imports: string[] = []
|
|
503
|
-
const entries: string[] = []
|
|
504
|
-
const exportNames = new Set<string>()
|
|
505
|
-
|
|
506
|
-
files.forEach((item) => {
|
|
507
|
-
const { filePath } = item
|
|
508
|
-
const source = readFileSync(filePath, 'utf8')
|
|
509
|
-
const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
|
|
510
|
-
.map((match) => match[1])
|
|
511
|
-
.sort((left, right) => left.localeCompare(right))
|
|
512
|
-
const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
|
|
513
|
-
const fallbackServicePath = 'name' in item
|
|
514
|
-
? toPluginModulePath(item.name, item.rootDir, filePath, ['.service'])
|
|
515
|
-
: toModulePath('services', filePath)
|
|
516
|
-
const defaultServiceName = defaultServiceClass
|
|
517
|
-
? defaultServiceClass[1] ?? `${toPascalCase(fallbackServicePath)}Service`
|
|
518
|
-
: ''
|
|
519
|
-
const defaultImportName = defaultServiceName
|
|
520
|
-
? toIdentifier([defaultServiceName, 'Default'])
|
|
521
|
-
: ''
|
|
522
|
-
|
|
523
|
-
if (namedServiceClasses.length === 0 && !defaultServiceName) {
|
|
524
|
-
return
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (defaultServiceName) {
|
|
528
|
-
imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
|
|
529
|
-
exportNames.add(`${defaultImportName} as ${defaultServiceName}`)
|
|
530
|
-
entries.push(` ${toServiceKey(defaultServiceName)}: new ${defaultImportName}(deps),`)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (namedServiceClasses.length > 0) {
|
|
534
|
-
imports.push(`import { ${namedServiceClasses.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
535
|
-
namedServiceClasses.forEach((name) => exportNames.add(name))
|
|
536
|
-
entries.push(...namedServiceClasses.map((name) => ` ${toServiceKey(name)}: new ${name}(deps),`))
|
|
537
|
-
}
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
541
|
-
const content = `${generatedHeader}
|
|
542
|
-
${imports.join('\n')}
|
|
543
|
-
|
|
544
|
-
${exportBlock}
|
|
545
|
-
export function createServices(deps: any) {
|
|
546
|
-
return {
|
|
547
|
-
${entries.join('\n')}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
export type Services = ReturnType<typeof createServices>
|
|
552
|
-
|
|
553
|
-
declare module 'jax-hono/core/generated' {
|
|
554
|
-
interface JaxGenerated {
|
|
555
|
-
services: Services
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
`
|
|
559
|
-
|
|
560
|
-
writeGeneratedFile('services.generated.ts', content)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function generateCronsRegistry() {
|
|
564
|
-
const files = [
|
|
565
|
-
...getFilesFromDirs(['cron', 'crons'])
|
|
566
|
-
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
567
|
-
.map((filePath) => ({
|
|
568
|
-
modulePath: toModulePath(filePath.includes(`${srcDir}/crons/`) ? 'crons' : 'cron', filePath, ['.cron']),
|
|
569
|
-
filePath,
|
|
570
|
-
})),
|
|
571
|
-
...getFilesFromDirs(['modules'])
|
|
572
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.cron'))
|
|
573
|
-
.map((filePath) => ({
|
|
574
|
-
modulePath: toModulePath('modules', filePath, ['.cron']),
|
|
575
|
-
filePath,
|
|
576
|
-
})),
|
|
577
|
-
...getEnabledPluginFiles('.cron')
|
|
578
|
-
.map(({ name, rootDir, filePath }) => ({
|
|
579
|
-
modulePath: toPluginModulePath(name, rootDir, filePath, ['.cron']),
|
|
580
|
-
filePath,
|
|
581
|
-
})),
|
|
582
|
-
]
|
|
583
|
-
.filter(({ filePath }) => hasCronExport(filePath))
|
|
584
|
-
.filter((item, index, list) => {
|
|
585
|
-
return list.findIndex((row) => row.filePath === item.filePath) === index
|
|
586
|
-
})
|
|
587
|
-
.sort((left, right) => left.modulePath.localeCompare(right.modulePath))
|
|
588
|
-
|
|
589
|
-
const imports = files.map((item) => {
|
|
590
|
-
return `import * as ${toLowerCamelIdentifier([item.modulePath], 'Cron')} from '${toImportPath(item.filePath)}'`
|
|
591
|
-
})
|
|
592
|
-
const exportNames = files.map((item) => toLowerCamelIdentifier([item.modulePath], 'Cron'))
|
|
593
|
-
const entries = files.map((item) => {
|
|
594
|
-
const importName = toLowerCamelIdentifier([item.modulePath], 'Cron')
|
|
595
|
-
|
|
596
|
-
return ` { name: ${JSON.stringify(item.modulePath)}, module: ${importName} },`
|
|
597
|
-
})
|
|
598
|
-
const exportBlock = formatExportBlock(exportNames)
|
|
599
|
-
const content = createGeneratedContent([
|
|
600
|
-
imports.join('\n'),
|
|
601
|
-
exportBlock,
|
|
602
|
-
`export const cronModules = [
|
|
603
|
-
${entries.join('\n')}
|
|
604
|
-
] as const`,
|
|
605
|
-
])
|
|
606
|
-
|
|
607
|
-
writeGeneratedFile('crons.generated.ts', content)
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function hasCronExport(filePath: string) {
|
|
611
|
-
const source = readFileSync(filePath, 'utf8')
|
|
612
|
-
|
|
613
|
-
return /\bexport\s+default\b/.test(source)
|
|
614
|
-
|| /\bexport\s+const\s+(?:cron|jobs)\b/.test(source)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function findBalancedObject(source: string, startIndex: number) {
|
|
618
|
-
let depth = 0
|
|
619
|
-
let quote: string | undefined
|
|
620
|
-
let escaped = false
|
|
621
|
-
let lineComment = false
|
|
622
|
-
let blockComment = false
|
|
623
|
-
|
|
624
|
-
for (let index = startIndex; index < source.length; index += 1) {
|
|
625
|
-
const char = source[index]
|
|
626
|
-
const next = source[index + 1]
|
|
627
|
-
|
|
628
|
-
if (lineComment) {
|
|
629
|
-
if (char === '\n') {
|
|
630
|
-
lineComment = false
|
|
631
|
-
}
|
|
632
|
-
continue
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if (blockComment) {
|
|
636
|
-
if (char === '*' && next === '/') {
|
|
637
|
-
blockComment = false
|
|
638
|
-
index += 1
|
|
639
|
-
}
|
|
640
|
-
continue
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (quote) {
|
|
644
|
-
if (escaped) {
|
|
645
|
-
escaped = false
|
|
646
|
-
continue
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (char === '\\') {
|
|
650
|
-
escaped = true
|
|
651
|
-
continue
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (char === quote) {
|
|
655
|
-
quote = undefined
|
|
656
|
-
}
|
|
657
|
-
continue
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (char === '/' && next === '/') {
|
|
661
|
-
lineComment = true
|
|
662
|
-
index += 1
|
|
663
|
-
continue
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (char === '/' && next === '*') {
|
|
667
|
-
blockComment = true
|
|
668
|
-
index += 1
|
|
669
|
-
continue
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (char === '"' || char === '\'' || char === '`') {
|
|
673
|
-
quote = char
|
|
674
|
-
continue
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (char === '{') {
|
|
678
|
-
depth += 1
|
|
679
|
-
continue
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (char === '}') {
|
|
683
|
-
depth -= 1
|
|
684
|
-
|
|
685
|
-
if (depth === 0) {
|
|
686
|
-
return source.slice(startIndex, index + 1)
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return undefined
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function extractPluginConfigObject(source: string) {
|
|
695
|
-
const configCallIndex = source.indexOf('definePluginConfig')
|
|
696
|
-
const searchStart = configCallIndex >= 0 ? configCallIndex : source.indexOf('export default')
|
|
697
|
-
const objectStart = searchStart >= 0 ? source.indexOf('{', searchStart) : -1
|
|
698
|
-
|
|
699
|
-
if (objectStart < 0) {
|
|
700
|
-
return undefined
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
return findBalancedObject(source, objectStart)
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function readPluginConfig(filePath: string): PluginConfig {
|
|
707
|
-
if (!existsSync(filePath)) {
|
|
708
|
-
return {}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const objectSource = extractPluginConfigObject(readFileSync(filePath, 'utf8'))
|
|
712
|
-
|
|
713
|
-
if (!objectSource) {
|
|
714
|
-
return {}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
const value = new Function(`return (${objectSource})`)() as unknown
|
|
719
|
-
|
|
720
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
721
|
-
? (value as PluginConfig)
|
|
722
|
-
: {}
|
|
723
|
-
} catch (error) {
|
|
724
|
-
throw new Error(`Invalid plugin config ${relative(srcDir, filePath)}: ${(error as Error).message}`)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function normalizePluginPackage(packageName: string, configFile: string) {
|
|
729
|
-
if (!packageName.startsWith('./') && !packageName.startsWith('../')) {
|
|
730
|
-
return packageName
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const normalized = join(configFile, '..', packageName)
|
|
734
|
-
.replaceAll('\\', '/')
|
|
735
|
-
.replace(/\/index$/, '')
|
|
736
|
-
|
|
737
|
-
return normalized
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
741
|
-
const merged = new Map<string, Exclude<PluginConfigItem, false>>()
|
|
742
|
-
|
|
743
|
-
for (const { filePath, config } of configs) {
|
|
744
|
-
for (const [name, item] of Object.entries(config)) {
|
|
745
|
-
if (item === false) {
|
|
746
|
-
merged.delete(name)
|
|
747
|
-
continue
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const current = merged.get(name) ?? {}
|
|
751
|
-
const next = { ...current, ...item }
|
|
752
|
-
|
|
753
|
-
if (next.package) {
|
|
754
|
-
next.package = normalizePluginPackage(next.package, filePath)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
merged.set(name, next)
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return [...merged.entries()]
|
|
762
|
-
.filter((entry): entry is [string, Exclude<PluginConfigItem, false> & { package: string }] => {
|
|
763
|
-
const [, item] = entry
|
|
764
|
-
|
|
765
|
-
return item.enable === true && typeof item.package === 'string' && item.package.length > 0
|
|
766
|
-
})
|
|
767
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function getEnabledPluginConfigItems() {
|
|
771
|
-
return mergePluginConfig([
|
|
772
|
-
{ filePath: join(packageDir, 'plugins/config.ts'), config: readPluginConfig(join(packageDir, 'plugins/config.ts')) },
|
|
773
|
-
{ filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
|
|
774
|
-
])
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function resolvePluginPackageDir(packageName: string) {
|
|
778
|
-
const resolved = resolvePluginPackage(packageName)
|
|
779
|
-
|
|
780
|
-
return statSync(resolved).isDirectory()
|
|
781
|
-
? resolved
|
|
782
|
-
: dirname(resolved)
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function resolvePluginPackage(packageName: string) {
|
|
786
|
-
if (isAbsolute(packageName)) {
|
|
787
|
-
return packageName
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (packageName.startsWith('./') || packageName.startsWith('../')) {
|
|
791
|
-
return resolve(projectDir, packageName)
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if (packageName.startsWith('jax-hono/')) {
|
|
795
|
-
const packagePath = packageName.slice('jax-hono/'.length)
|
|
796
|
-
const directPath = join(packageDir, packagePath)
|
|
797
|
-
|
|
798
|
-
if (existsSync(directPath)) {
|
|
799
|
-
return directPath
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const indexPath = join(directPath, 'index.ts')
|
|
803
|
-
|
|
804
|
-
if (existsSync(indexPath)) {
|
|
805
|
-
return indexPath
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return projectRequire.resolve(packageName)
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function getEnabledPlugins(): EnabledPlugin[] {
|
|
813
|
-
return getEnabledPluginConfigItems()
|
|
814
|
-
.map(([name, item]) => {
|
|
815
|
-
const rootDir = resolvePluginPackageDir(item.package)
|
|
816
|
-
|
|
817
|
-
return {
|
|
818
|
-
name,
|
|
819
|
-
packageName: item.package,
|
|
820
|
-
rootDir,
|
|
821
|
-
}
|
|
822
|
-
})
|
|
823
|
-
.filter((plugin) => existsSync(plugin.rootDir))
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function serializePluginOptions(options: unknown) {
|
|
827
|
-
return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function generatePluginContextRegistry() {
|
|
831
|
-
const enabledPlugins = getEnabledPluginConfigItems()
|
|
832
|
-
const imports = enabledPlugins.map(([name, item]) => {
|
|
833
|
-
return `import ${toLowerCamelIdentifier([name], 'Plugin')} from ${JSON.stringify(item.package)}`
|
|
834
|
-
})
|
|
835
|
-
const exportNames = enabledPlugins.map(([name]) => toLowerCamelIdentifier([name], 'Plugin'))
|
|
836
|
-
const moduleEntries = enabledPlugins.map(
|
|
837
|
-
([name, item]) =>
|
|
838
|
-
` { name: ${JSON.stringify(name)}, module: ${toLowerCamelIdentifier([name], 'Plugin')}${serializePluginOptions(item.options)} },`,
|
|
839
|
-
)
|
|
840
|
-
const exportBlock = formatExportBlock(exportNames)
|
|
841
|
-
|
|
842
|
-
const content = createGeneratedContent([
|
|
843
|
-
imports.join('\n'),
|
|
844
|
-
exportBlock,
|
|
845
|
-
`export const plugins = {} as Record<string, unknown>
|
|
846
|
-
|
|
847
|
-
export type Plugins = typeof plugins
|
|
848
|
-
|
|
849
|
-
declare module 'jax-hono/core/generated' {
|
|
850
|
-
interface JaxGenerated {
|
|
851
|
-
plugins: Plugins
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
export const pluginModules = [
|
|
856
|
-
${moduleEntries.join('\n')}
|
|
857
|
-
] as const`,
|
|
858
|
-
])
|
|
859
|
-
|
|
860
|
-
writeGeneratedFile('plugins.generated.ts', content)
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function generateGeneratedEntry() {
|
|
864
|
-
const content = `${generatedHeader}
|
|
865
|
-
import * as configModule from './config'
|
|
866
|
-
import * as controllersModule from './controllers.generated'
|
|
867
|
-
import * as middlewaresModule from './middlewares.generated'
|
|
868
|
-
import * as modelsModule from './models.generated'
|
|
869
|
-
import * as servicesModule from './services.generated'
|
|
870
|
-
import * as cronsModule from './crons.generated'
|
|
871
|
-
import * as pluginsModule from './plugins.generated'
|
|
872
|
-
import { registerGeneratedModules } from 'jax-hono/core/generated'
|
|
873
|
-
|
|
874
|
-
registerGeneratedModules({
|
|
875
|
-
'config.ts': configModule,
|
|
876
|
-
'controllers.generated.ts': controllersModule,
|
|
877
|
-
'middlewares.generated.ts': middlewaresModule,
|
|
878
|
-
'models.generated.ts': modelsModule,
|
|
879
|
-
'services.generated.ts': servicesModule,
|
|
880
|
-
'crons.generated.ts': cronsModule,
|
|
881
|
-
'plugins.generated.ts': pluginsModule,
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
export * from 'jax-hono'
|
|
885
|
-
`
|
|
886
|
-
|
|
887
|
-
writeGeneratedFile('index.ts', content)
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
export function generateRegistry() {
|
|
891
|
-
generateControllersRegistry()
|
|
892
|
-
generateMiddlewaresRegistry()
|
|
893
|
-
generateModelsRegistry()
|
|
894
|
-
generateServicesRegistry()
|
|
895
|
-
generateCronsRegistry()
|
|
896
|
-
generatePluginContextRegistry()
|
|
897
|
-
generateGeneratedEntry()
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (import.meta.main) {
|
|
901
|
-
generateRegistry()
|
|
902
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { cwd } from 'node:process'
|
|
4
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
|
|
5
|
+
import { ensureGeneratedDir, generatedDir, packageDir, projectDir } from './generated-path'
|
|
6
|
+
|
|
7
|
+
const srcDir = join(cwd(), 'src')
|
|
8
|
+
const projectRequire = createRequire(join(projectDir, 'package.json'))
|
|
9
|
+
const moduleExtensions = new Set(['.ts', '.js', '.mjs', '.cjs'])
|
|
10
|
+
const generatedHeader = `// This file is generated by jax-hono/build/generate-registry.ts.
|
|
11
|
+
// Do not edit it directly.
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
type ControllerGroup = {
|
|
15
|
+
name: string
|
|
16
|
+
dirs: string[]
|
|
17
|
+
suffix?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type PluginConfigItem =
|
|
21
|
+
| false
|
|
22
|
+
| {
|
|
23
|
+
enable?: boolean
|
|
24
|
+
package?: string
|
|
25
|
+
options?: unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type PluginConfig = Record<string, PluginConfigItem>
|
|
29
|
+
|
|
30
|
+
type EnabledPlugin = {
|
|
31
|
+
name: string
|
|
32
|
+
packageName: string
|
|
33
|
+
rootDir: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const controllerGroups: ControllerGroup[] = [
|
|
37
|
+
{ name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
|
|
38
|
+
{ name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
|
|
39
|
+
{ name: 'open', dirs: ['open', 'controllers/open', 'controller/open', 'modules'], suffix: '.open' },
|
|
40
|
+
{ name: 'client', dirs: ['client', 'controllers/client', 'controller/client', 'modules'], suffix: '.client' },
|
|
41
|
+
{ name: 'controller', dirs: ['controllers', 'controller', 'modules'], suffix: '.controller' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const scopedControllerGroups = new Set(
|
|
45
|
+
controllerGroups
|
|
46
|
+
.filter((group) => group.name !== 'controller')
|
|
47
|
+
.map((group) => group.name),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function walkFiles(dir: string): string[] {
|
|
51
|
+
if (!existsSync(dir)) {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const files: string[] = []
|
|
56
|
+
|
|
57
|
+
for (const item of readdirSync(dir, { withFileTypes: true })) {
|
|
58
|
+
const itemPath = join(dir, item.name)
|
|
59
|
+
|
|
60
|
+
if (item.isDirectory()) {
|
|
61
|
+
files.push(...walkFiles(itemPath))
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (item.isFile() && moduleExtensions.has(parse(item.name).ext)) {
|
|
66
|
+
files.push(itemPath)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return files
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeGeneratedFile(fileName: string, content: string) {
|
|
74
|
+
const outputFile = join(generatedDir, fileName)
|
|
75
|
+
|
|
76
|
+
ensureGeneratedDir()
|
|
77
|
+
|
|
78
|
+
if (existsSync(outputFile) && readFileSync(outputFile, 'utf8') === content) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
writeFileSync(outputFile, content)
|
|
83
|
+
console.log(`Generated ${relative(projectDir, outputFile)}.`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toImportPath(filePath: string) {
|
|
87
|
+
const path = filePath
|
|
88
|
+
.replace(parse(filePath).ext, '')
|
|
89
|
+
.replaceAll('\\', '/')
|
|
90
|
+
|
|
91
|
+
return path
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toIdentifier(parts: string[]) {
|
|
95
|
+
const value = parts
|
|
96
|
+
.join('_')
|
|
97
|
+
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
98
|
+
.replace(/^([^a-zA-Z_$])/, '_$1')
|
|
99
|
+
|
|
100
|
+
return value || 'module'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toLowerCamelIdentifier(parts: string[], suffix = '') {
|
|
104
|
+
const words = parts
|
|
105
|
+
.join('/')
|
|
106
|
+
.split(/[^a-zA-Z0-9_$]+/)
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
const value = words
|
|
109
|
+
.map((word, index) => {
|
|
110
|
+
const normalized = toCamelCase(word)
|
|
111
|
+
|
|
112
|
+
return index === 0
|
|
113
|
+
? normalized.charAt(0).toLowerCase() + normalized.slice(1)
|
|
114
|
+
: toPascalCase(normalized)
|
|
115
|
+
})
|
|
116
|
+
.join('')
|
|
117
|
+
|
|
118
|
+
return toIdentifier([`${value}${suffix}`])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toCamelCase(value: string) {
|
|
122
|
+
return value.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase())
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toPascalCase(value: string) {
|
|
126
|
+
const camel = toCamelCase(value)
|
|
127
|
+
|
|
128
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toServiceKey(name: string) {
|
|
132
|
+
const baseName = name.endsWith('Service')
|
|
133
|
+
? name.slice(0, -'Service'.length)
|
|
134
|
+
: name
|
|
135
|
+
|
|
136
|
+
return baseName.replace(/^./, (char) => char.toLowerCase())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toMiddlewareKey(exportName: string) {
|
|
140
|
+
return exportName.endsWith('Middleware')
|
|
141
|
+
? exportName.slice(0, -'Middleware'.length)
|
|
142
|
+
: exportName
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function stripKnownSuffix(modulePath: string, suffixes: string[]) {
|
|
146
|
+
for (const suffix of suffixes) {
|
|
147
|
+
if (modulePath.endsWith(suffix)) {
|
|
148
|
+
return modulePath.slice(0, -suffix.length)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return modulePath
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeModulePath(modulePath: string) {
|
|
156
|
+
const parts = modulePath.split('/')
|
|
157
|
+
const last = parts.at(-1)
|
|
158
|
+
const parent = parts.at(-2)
|
|
159
|
+
|
|
160
|
+
if (!last || !parent) {
|
|
161
|
+
return modulePath
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (last === parent) {
|
|
165
|
+
return parts.slice(0, -1).join('/')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (last.startsWith(`${parent}-`) || last.startsWith(`${parent}_`)) {
|
|
169
|
+
return [...parts.slice(0, -2), last].join('/')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return modulePath
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
176
|
+
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
177
|
+
.replace(parse(filePath).ext, '')
|
|
178
|
+
.replaceAll('\\', '/')
|
|
179
|
+
const withoutIndex = modulePath.endsWith('/index')
|
|
180
|
+
? modulePath.slice(0, -'/index'.length)
|
|
181
|
+
: modulePath
|
|
182
|
+
|
|
183
|
+
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
184
|
+
|
|
185
|
+
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toPluginModulePath(pluginName: string, rootDir: string, filePath: string, suffixes: string[] = []) {
|
|
189
|
+
const modulePath = relative(rootDir, filePath)
|
|
190
|
+
.replace(parse(filePath).ext, '')
|
|
191
|
+
.replaceAll('\\', '/')
|
|
192
|
+
const withoutIndex = modulePath.endsWith('/index')
|
|
193
|
+
? modulePath.slice(0, -'/index'.length)
|
|
194
|
+
: modulePath
|
|
195
|
+
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
196
|
+
|
|
197
|
+
return normalizeModulePath(stripped ? `${pluginName}/${stripped}` : pluginName)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function uniqueSorted(values: string[]) {
|
|
201
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getFilesFromDirs(dirs: string[]) {
|
|
205
|
+
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getEnabledPluginFiles(suffix: string) {
|
|
209
|
+
return getEnabledPlugins().flatMap((plugin) => {
|
|
210
|
+
return walkFiles(plugin.rootDir)
|
|
211
|
+
.filter((filePath) => isSuffixedModuleFile(filePath, suffix))
|
|
212
|
+
.map((filePath) => ({ ...plugin, filePath }))
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isFileInDir(filePath: string, dirName: string) {
|
|
217
|
+
const rel = relative(join(srcDir, dirName), filePath)
|
|
218
|
+
|
|
219
|
+
return rel && !rel.startsWith('..')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isSuffixedModuleFile(filePath: string, suffix: string) {
|
|
223
|
+
return relative(srcDir, filePath)
|
|
224
|
+
.replace(parse(filePath).ext, '')
|
|
225
|
+
.replaceAll('\\', '/')
|
|
226
|
+
.endsWith(suffix)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isModuleFeatureFile(filePath: string, suffix: string) {
|
|
230
|
+
return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasDefaultExport(filePath: string) {
|
|
234
|
+
return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function indentObject(content: string, spaces = 2) {
|
|
238
|
+
if (!content) {
|
|
239
|
+
return ''
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const indent = ' '.repeat(spaces)
|
|
243
|
+
|
|
244
|
+
return content
|
|
245
|
+
.split('\n')
|
|
246
|
+
.map((line) => `${indent}${line}`)
|
|
247
|
+
.join('\n')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildNestedObject(entries: { path: string[]; value: string }[]): string {
|
|
251
|
+
const groups = new Map<string, { path: string[]; value: string }[]>()
|
|
252
|
+
const leaves: { key: string; value: string }[] = []
|
|
253
|
+
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const [head, ...tail] = entry.path
|
|
256
|
+
|
|
257
|
+
if (!head) {
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (tail.length === 0) {
|
|
262
|
+
leaves.push({ key: head, value: entry.value })
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
groups.set(head, [...(groups.get(head) ?? []), { path: tail, value: entry.value }])
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const lines = [
|
|
270
|
+
...leaves.map((leaf) => `${toObjectKey(leaf.key)}: ${leaf.value},`),
|
|
271
|
+
...[...groups.entries()].map(([key, groupEntries]) => {
|
|
272
|
+
return `${toObjectKey(key)}: {\n${indentObject(buildNestedObject(groupEntries), 2)}\n},`
|
|
273
|
+
}),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
return lines.join('\n')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function generateControllersRegistry() {
|
|
280
|
+
const used = new Set<string>()
|
|
281
|
+
const imports: string[] = []
|
|
282
|
+
const controllerExports: string[] = []
|
|
283
|
+
const groupLines: string[] = []
|
|
284
|
+
|
|
285
|
+
for (const group of controllerGroups) {
|
|
286
|
+
const projectEntries = getFilesFromDirs(group.dirs)
|
|
287
|
+
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
288
|
+
.filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
|
|
289
|
+
.filter(hasDefaultExport)
|
|
290
|
+
.map((filePath, index) => {
|
|
291
|
+
const baseDir = group.dirs.find((dirName) => {
|
|
292
|
+
return isFileInDir(filePath, dirName)
|
|
293
|
+
}) ?? group.dirs[0]
|
|
294
|
+
const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
|
|
295
|
+
const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
|
|
296
|
+
const exportName = toLowerCamelIdentifier(group.name === 'controller' ? [modulePath] : [group.name, modulePath], 'Controller')
|
|
297
|
+
|
|
298
|
+
return { modulePath, importName, exportName, filePath }
|
|
299
|
+
})
|
|
300
|
+
const pluginEntries = group.name === 'controller'
|
|
301
|
+
? getEnabledPluginFiles('.controller')
|
|
302
|
+
.filter(({ filePath }) => basename(filePath) !== 'index.ts')
|
|
303
|
+
.filter(({ filePath }) => hasDefaultExport(filePath))
|
|
304
|
+
.map(({ name, rootDir, filePath }, index) => {
|
|
305
|
+
const modulePath = toPluginModulePath(name, rootDir, filePath, ['.controller'])
|
|
306
|
+
const importName = toIdentifier([group.name, 'plugin', modulePath, String(index), 'Controller'])
|
|
307
|
+
const exportName = toLowerCamelIdentifier([modulePath], 'Controller')
|
|
308
|
+
|
|
309
|
+
return { modulePath, importName, exportName, filePath }
|
|
310
|
+
})
|
|
311
|
+
: []
|
|
312
|
+
const entries = [...projectEntries, ...pluginEntries]
|
|
313
|
+
.filter((entry) => {
|
|
314
|
+
if (group.name !== 'controller') {
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const [scope] = entry.modulePath.split('/')
|
|
319
|
+
|
|
320
|
+
return !scopedControllerGroups.has(scope)
|
|
321
|
+
})
|
|
322
|
+
.filter((entry) => {
|
|
323
|
+
const key = `${group.name}:${entry.modulePath}`
|
|
324
|
+
|
|
325
|
+
if (used.has(key)) {
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
used.add(key)
|
|
330
|
+
|
|
331
|
+
return true
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
|
|
335
|
+
controllerExports.push(...entries.map((entry) => `export const ${entry.exportName} = createController(${entry.importName})`))
|
|
336
|
+
|
|
337
|
+
if (entries.length === 0) {
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const objectBody = buildNestedObject(entries.map((entry) => ({
|
|
342
|
+
path: entry.modulePath.split('/').map(toCamelCase),
|
|
343
|
+
value: entry.exportName,
|
|
344
|
+
})))
|
|
345
|
+
|
|
346
|
+
if (group.name === 'controller') {
|
|
347
|
+
groupLines.push(indentObject(objectBody, 2))
|
|
348
|
+
} else {
|
|
349
|
+
groupLines.push(` ${group.name}: {\n${indentObject(objectBody, 4)}\n },`)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const helperImport = imports.length > 0
|
|
354
|
+
? `import { createController } from 'jax-hono/core/controller'\n`
|
|
355
|
+
: ''
|
|
356
|
+
const content = `${generatedHeader}
|
|
357
|
+
${helperImport}${imports.join('\n')}
|
|
358
|
+
|
|
359
|
+
${controllerExports.join('\n')}
|
|
360
|
+
|
|
361
|
+
export const controllers = {
|
|
362
|
+
${groupLines.join('\n')}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
declare module 'jax-hono/core/generated' {
|
|
366
|
+
interface JaxGenerated {
|
|
367
|
+
controllers: typeof controllers
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
`
|
|
371
|
+
|
|
372
|
+
writeGeneratedFile('controllers.generated.ts', content)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function toObjectKey(value: string) {
|
|
376
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function formatExportBlock(names: string[]) {
|
|
380
|
+
if (names.length === 0) {
|
|
381
|
+
return ''
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return `export {\n${names.map((name) => ` ${name},`).join('\n')}\n}\n`
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createGeneratedContent(sections: string[]) {
|
|
388
|
+
return `${generatedHeader}\n${sections
|
|
389
|
+
.map((section) => section.trimEnd())
|
|
390
|
+
.filter((section) => section.trim())
|
|
391
|
+
.join('\n\n')}\n`
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function generateMiddlewaresRegistry() {
|
|
395
|
+
const files = getFilesFromDirs(['middlewares', 'middleware', 'modules'])
|
|
396
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.middleware'))
|
|
397
|
+
const imports: string[] = []
|
|
398
|
+
const entries: string[] = []
|
|
399
|
+
const exportNames = new Set<string>()
|
|
400
|
+
|
|
401
|
+
files.forEach((filePath) => {
|
|
402
|
+
const source = readFileSync(filePath, 'utf8')
|
|
403
|
+
const middlewareNames = [...source.matchAll(/\bexport\s+(?:const|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
|
|
404
|
+
.map((match) => match[1])
|
|
405
|
+
.sort((left, right) => left.localeCompare(right))
|
|
406
|
+
|
|
407
|
+
if (middlewareNames.length === 0) {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
imports.push(`import { ${middlewareNames.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
412
|
+
middlewareNames.forEach((name) => exportNames.add(name))
|
|
413
|
+
entries.push(...middlewareNames.map((name) => {
|
|
414
|
+
const key = toMiddlewareKey(name)
|
|
415
|
+
|
|
416
|
+
return key === name
|
|
417
|
+
? ` ${name},`
|
|
418
|
+
: ` ${key}: ${name},`
|
|
419
|
+
}))
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
423
|
+
const content = createGeneratedContent([
|
|
424
|
+
imports.join('\n'),
|
|
425
|
+
exportBlock,
|
|
426
|
+
`export const middlewares = {
|
|
427
|
+
${entries.join('\n')}
|
|
428
|
+
}`,
|
|
429
|
+
])
|
|
430
|
+
|
|
431
|
+
writeGeneratedFile('middlewares.generated.ts', content)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function generateModelsRegistry() {
|
|
435
|
+
const files = getFilesFromDirs(['models', 'model', 'modules'])
|
|
436
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.model'))
|
|
437
|
+
const imports: string[] = []
|
|
438
|
+
const entries: string[] = []
|
|
439
|
+
const exportNames = new Set<string>()
|
|
440
|
+
|
|
441
|
+
files.forEach((filePath) => {
|
|
442
|
+
const source = readFileSync(filePath, 'utf8')
|
|
443
|
+
const exportedModels = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*create(?:Loose)?Model\s*\(/g)]
|
|
444
|
+
.map((match) => match[1])
|
|
445
|
+
.sort((left, right) => left.localeCompare(right))
|
|
446
|
+
|
|
447
|
+
if (exportedModels.length === 0) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const modelKey = source.match(/\bexport\s+const\s+modelKey\s*=\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]/)?.[1]
|
|
452
|
+
const fileModelName = toPascalCase(toModulePath(
|
|
453
|
+
filePath.includes(`${srcDir}/modules/`) ? 'modules' : basename(parse(filePath).dir),
|
|
454
|
+
filePath,
|
|
455
|
+
['.model'],
|
|
456
|
+
))
|
|
457
|
+
|
|
458
|
+
imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
459
|
+
exportedModels.forEach((name) => exportNames.add(name))
|
|
460
|
+
entries.push(...exportedModels.map((name) => {
|
|
461
|
+
const key = exportedModels.length === 1 && modelKey
|
|
462
|
+
? modelKey
|
|
463
|
+
: name === `${fileModelName}Model`
|
|
464
|
+
? fileModelName
|
|
465
|
+
: name
|
|
466
|
+
|
|
467
|
+
return key === name
|
|
468
|
+
? ` ${name},`
|
|
469
|
+
: ` ${key}: ${name},`
|
|
470
|
+
}))
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
474
|
+
|
|
475
|
+
const content = `${generatedHeader}
|
|
476
|
+
${imports.join('\n')}
|
|
477
|
+
|
|
478
|
+
${exportBlock}
|
|
479
|
+
export const models = {
|
|
480
|
+
${entries.join('\n')}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export type Models = typeof models
|
|
484
|
+
|
|
485
|
+
declare module 'jax-hono/core/generated' {
|
|
486
|
+
interface JaxGenerated {
|
|
487
|
+
models: typeof models
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
`
|
|
491
|
+
|
|
492
|
+
writeGeneratedFile('models.generated.ts', content)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function generateServicesRegistry() {
|
|
496
|
+
const files = [
|
|
497
|
+
...getFilesFromDirs(['services', 'service', 'modules'])
|
|
498
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.service'))
|
|
499
|
+
.map((filePath) => ({ filePath })),
|
|
500
|
+
...getEnabledPluginFiles('.service'),
|
|
501
|
+
]
|
|
502
|
+
const imports: string[] = []
|
|
503
|
+
const entries: string[] = []
|
|
504
|
+
const exportNames = new Set<string>()
|
|
505
|
+
|
|
506
|
+
files.forEach((item) => {
|
|
507
|
+
const { filePath } = item
|
|
508
|
+
const source = readFileSync(filePath, 'utf8')
|
|
509
|
+
const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
|
|
510
|
+
.map((match) => match[1])
|
|
511
|
+
.sort((left, right) => left.localeCompare(right))
|
|
512
|
+
const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
|
|
513
|
+
const fallbackServicePath = 'name' in item
|
|
514
|
+
? toPluginModulePath(item.name, item.rootDir, filePath, ['.service'])
|
|
515
|
+
: toModulePath('services', filePath)
|
|
516
|
+
const defaultServiceName = defaultServiceClass
|
|
517
|
+
? defaultServiceClass[1] ?? `${toPascalCase(fallbackServicePath)}Service`
|
|
518
|
+
: ''
|
|
519
|
+
const defaultImportName = defaultServiceName
|
|
520
|
+
? toIdentifier([defaultServiceName, 'Default'])
|
|
521
|
+
: ''
|
|
522
|
+
|
|
523
|
+
if (namedServiceClasses.length === 0 && !defaultServiceName) {
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (defaultServiceName) {
|
|
528
|
+
imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
|
|
529
|
+
exportNames.add(`${defaultImportName} as ${defaultServiceName}`)
|
|
530
|
+
entries.push(` ${toServiceKey(defaultServiceName)}: new ${defaultImportName}(deps),`)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (namedServiceClasses.length > 0) {
|
|
534
|
+
imports.push(`import { ${namedServiceClasses.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
535
|
+
namedServiceClasses.forEach((name) => exportNames.add(name))
|
|
536
|
+
entries.push(...namedServiceClasses.map((name) => ` ${toServiceKey(name)}: new ${name}(deps),`))
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
541
|
+
const content = `${generatedHeader}
|
|
542
|
+
${imports.join('\n')}
|
|
543
|
+
|
|
544
|
+
${exportBlock}
|
|
545
|
+
export function createServices(deps: any) {
|
|
546
|
+
return {
|
|
547
|
+
${entries.join('\n')}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export type Services = ReturnType<typeof createServices>
|
|
552
|
+
|
|
553
|
+
declare module 'jax-hono/core/generated' {
|
|
554
|
+
interface JaxGenerated {
|
|
555
|
+
services: Services
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
`
|
|
559
|
+
|
|
560
|
+
writeGeneratedFile('services.generated.ts', content)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function generateCronsRegistry() {
|
|
564
|
+
const files = [
|
|
565
|
+
...getFilesFromDirs(['cron', 'crons'])
|
|
566
|
+
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
567
|
+
.map((filePath) => ({
|
|
568
|
+
modulePath: toModulePath(filePath.includes(`${srcDir}/crons/`) ? 'crons' : 'cron', filePath, ['.cron']),
|
|
569
|
+
filePath,
|
|
570
|
+
})),
|
|
571
|
+
...getFilesFromDirs(['modules'])
|
|
572
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.cron'))
|
|
573
|
+
.map((filePath) => ({
|
|
574
|
+
modulePath: toModulePath('modules', filePath, ['.cron']),
|
|
575
|
+
filePath,
|
|
576
|
+
})),
|
|
577
|
+
...getEnabledPluginFiles('.cron')
|
|
578
|
+
.map(({ name, rootDir, filePath }) => ({
|
|
579
|
+
modulePath: toPluginModulePath(name, rootDir, filePath, ['.cron']),
|
|
580
|
+
filePath,
|
|
581
|
+
})),
|
|
582
|
+
]
|
|
583
|
+
.filter(({ filePath }) => hasCronExport(filePath))
|
|
584
|
+
.filter((item, index, list) => {
|
|
585
|
+
return list.findIndex((row) => row.filePath === item.filePath) === index
|
|
586
|
+
})
|
|
587
|
+
.sort((left, right) => left.modulePath.localeCompare(right.modulePath))
|
|
588
|
+
|
|
589
|
+
const imports = files.map((item) => {
|
|
590
|
+
return `import * as ${toLowerCamelIdentifier([item.modulePath], 'Cron')} from '${toImportPath(item.filePath)}'`
|
|
591
|
+
})
|
|
592
|
+
const exportNames = files.map((item) => toLowerCamelIdentifier([item.modulePath], 'Cron'))
|
|
593
|
+
const entries = files.map((item) => {
|
|
594
|
+
const importName = toLowerCamelIdentifier([item.modulePath], 'Cron')
|
|
595
|
+
|
|
596
|
+
return ` { name: ${JSON.stringify(item.modulePath)}, module: ${importName} },`
|
|
597
|
+
})
|
|
598
|
+
const exportBlock = formatExportBlock(exportNames)
|
|
599
|
+
const content = createGeneratedContent([
|
|
600
|
+
imports.join('\n'),
|
|
601
|
+
exportBlock,
|
|
602
|
+
`export const cronModules = [
|
|
603
|
+
${entries.join('\n')}
|
|
604
|
+
] as const`,
|
|
605
|
+
])
|
|
606
|
+
|
|
607
|
+
writeGeneratedFile('crons.generated.ts', content)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function hasCronExport(filePath: string) {
|
|
611
|
+
const source = readFileSync(filePath, 'utf8')
|
|
612
|
+
|
|
613
|
+
return /\bexport\s+default\b/.test(source)
|
|
614
|
+
|| /\bexport\s+const\s+(?:cron|jobs)\b/.test(source)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function findBalancedObject(source: string, startIndex: number) {
|
|
618
|
+
let depth = 0
|
|
619
|
+
let quote: string | undefined
|
|
620
|
+
let escaped = false
|
|
621
|
+
let lineComment = false
|
|
622
|
+
let blockComment = false
|
|
623
|
+
|
|
624
|
+
for (let index = startIndex; index < source.length; index += 1) {
|
|
625
|
+
const char = source[index]
|
|
626
|
+
const next = source[index + 1]
|
|
627
|
+
|
|
628
|
+
if (lineComment) {
|
|
629
|
+
if (char === '\n') {
|
|
630
|
+
lineComment = false
|
|
631
|
+
}
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (blockComment) {
|
|
636
|
+
if (char === '*' && next === '/') {
|
|
637
|
+
blockComment = false
|
|
638
|
+
index += 1
|
|
639
|
+
}
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (quote) {
|
|
644
|
+
if (escaped) {
|
|
645
|
+
escaped = false
|
|
646
|
+
continue
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (char === '\\') {
|
|
650
|
+
escaped = true
|
|
651
|
+
continue
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (char === quote) {
|
|
655
|
+
quote = undefined
|
|
656
|
+
}
|
|
657
|
+
continue
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (char === '/' && next === '/') {
|
|
661
|
+
lineComment = true
|
|
662
|
+
index += 1
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (char === '/' && next === '*') {
|
|
667
|
+
blockComment = true
|
|
668
|
+
index += 1
|
|
669
|
+
continue
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (char === '"' || char === '\'' || char === '`') {
|
|
673
|
+
quote = char
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (char === '{') {
|
|
678
|
+
depth += 1
|
|
679
|
+
continue
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (char === '}') {
|
|
683
|
+
depth -= 1
|
|
684
|
+
|
|
685
|
+
if (depth === 0) {
|
|
686
|
+
return source.slice(startIndex, index + 1)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return undefined
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function extractPluginConfigObject(source: string) {
|
|
695
|
+
const configCallIndex = source.indexOf('definePluginConfig')
|
|
696
|
+
const searchStart = configCallIndex >= 0 ? configCallIndex : source.indexOf('export default')
|
|
697
|
+
const objectStart = searchStart >= 0 ? source.indexOf('{', searchStart) : -1
|
|
698
|
+
|
|
699
|
+
if (objectStart < 0) {
|
|
700
|
+
return undefined
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return findBalancedObject(source, objectStart)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function readPluginConfig(filePath: string): PluginConfig {
|
|
707
|
+
if (!existsSync(filePath)) {
|
|
708
|
+
return {}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const objectSource = extractPluginConfigObject(readFileSync(filePath, 'utf8'))
|
|
712
|
+
|
|
713
|
+
if (!objectSource) {
|
|
714
|
+
return {}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const value = new Function(`return (${objectSource})`)() as unknown
|
|
719
|
+
|
|
720
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
721
|
+
? (value as PluginConfig)
|
|
722
|
+
: {}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
throw new Error(`Invalid plugin config ${relative(srcDir, filePath)}: ${(error as Error).message}`)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normalizePluginPackage(packageName: string, configFile: string) {
|
|
729
|
+
if (!packageName.startsWith('./') && !packageName.startsWith('../')) {
|
|
730
|
+
return packageName
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const normalized = join(configFile, '..', packageName)
|
|
734
|
+
.replaceAll('\\', '/')
|
|
735
|
+
.replace(/\/index$/, '')
|
|
736
|
+
|
|
737
|
+
return normalized
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
741
|
+
const merged = new Map<string, Exclude<PluginConfigItem, false>>()
|
|
742
|
+
|
|
743
|
+
for (const { filePath, config } of configs) {
|
|
744
|
+
for (const [name, item] of Object.entries(config)) {
|
|
745
|
+
if (item === false) {
|
|
746
|
+
merged.delete(name)
|
|
747
|
+
continue
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const current = merged.get(name) ?? {}
|
|
751
|
+
const next = { ...current, ...item }
|
|
752
|
+
|
|
753
|
+
if (next.package) {
|
|
754
|
+
next.package = normalizePluginPackage(next.package, filePath)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
merged.set(name, next)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return [...merged.entries()]
|
|
762
|
+
.filter((entry): entry is [string, Exclude<PluginConfigItem, false> & { package: string }] => {
|
|
763
|
+
const [, item] = entry
|
|
764
|
+
|
|
765
|
+
return item.enable === true && typeof item.package === 'string' && item.package.length > 0
|
|
766
|
+
})
|
|
767
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function getEnabledPluginConfigItems() {
|
|
771
|
+
return mergePluginConfig([
|
|
772
|
+
{ filePath: join(packageDir, 'plugins/config.ts'), config: readPluginConfig(join(packageDir, 'plugins/config.ts')) },
|
|
773
|
+
{ filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
|
|
774
|
+
])
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function resolvePluginPackageDir(packageName: string) {
|
|
778
|
+
const resolved = resolvePluginPackage(packageName)
|
|
779
|
+
|
|
780
|
+
return statSync(resolved).isDirectory()
|
|
781
|
+
? resolved
|
|
782
|
+
: dirname(resolved)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function resolvePluginPackage(packageName: string) {
|
|
786
|
+
if (isAbsolute(packageName)) {
|
|
787
|
+
return packageName
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (packageName.startsWith('./') || packageName.startsWith('../')) {
|
|
791
|
+
return resolve(projectDir, packageName)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (packageName.startsWith('jax-hono/')) {
|
|
795
|
+
const packagePath = packageName.slice('jax-hono/'.length)
|
|
796
|
+
const directPath = join(packageDir, packagePath)
|
|
797
|
+
|
|
798
|
+
if (existsSync(directPath)) {
|
|
799
|
+
return directPath
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const indexPath = join(directPath, 'index.ts')
|
|
803
|
+
|
|
804
|
+
if (existsSync(indexPath)) {
|
|
805
|
+
return indexPath
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return projectRequire.resolve(packageName)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function getEnabledPlugins(): EnabledPlugin[] {
|
|
813
|
+
return getEnabledPluginConfigItems()
|
|
814
|
+
.map(([name, item]) => {
|
|
815
|
+
const rootDir = resolvePluginPackageDir(item.package)
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
name,
|
|
819
|
+
packageName: item.package,
|
|
820
|
+
rootDir,
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
.filter((plugin) => existsSync(plugin.rootDir))
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function serializePluginOptions(options: unknown) {
|
|
827
|
+
return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function generatePluginContextRegistry() {
|
|
831
|
+
const enabledPlugins = getEnabledPluginConfigItems()
|
|
832
|
+
const imports = enabledPlugins.map(([name, item]) => {
|
|
833
|
+
return `import ${toLowerCamelIdentifier([name], 'Plugin')} from ${JSON.stringify(item.package)}`
|
|
834
|
+
})
|
|
835
|
+
const exportNames = enabledPlugins.map(([name]) => toLowerCamelIdentifier([name], 'Plugin'))
|
|
836
|
+
const moduleEntries = enabledPlugins.map(
|
|
837
|
+
([name, item]) =>
|
|
838
|
+
` { name: ${JSON.stringify(name)}, module: ${toLowerCamelIdentifier([name], 'Plugin')}${serializePluginOptions(item.options)} },`,
|
|
839
|
+
)
|
|
840
|
+
const exportBlock = formatExportBlock(exportNames)
|
|
841
|
+
|
|
842
|
+
const content = createGeneratedContent([
|
|
843
|
+
imports.join('\n'),
|
|
844
|
+
exportBlock,
|
|
845
|
+
`export const plugins = {} as Record<string, unknown>
|
|
846
|
+
|
|
847
|
+
export type Plugins = typeof plugins
|
|
848
|
+
|
|
849
|
+
declare module 'jax-hono/core/generated' {
|
|
850
|
+
interface JaxGenerated {
|
|
851
|
+
plugins: Plugins
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export const pluginModules = [
|
|
856
|
+
${moduleEntries.join('\n')}
|
|
857
|
+
] as const`,
|
|
858
|
+
])
|
|
859
|
+
|
|
860
|
+
writeGeneratedFile('plugins.generated.ts', content)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function generateGeneratedEntry() {
|
|
864
|
+
const content = `${generatedHeader}
|
|
865
|
+
import * as configModule from './config'
|
|
866
|
+
import * as controllersModule from './controllers.generated'
|
|
867
|
+
import * as middlewaresModule from './middlewares.generated'
|
|
868
|
+
import * as modelsModule from './models.generated'
|
|
869
|
+
import * as servicesModule from './services.generated'
|
|
870
|
+
import * as cronsModule from './crons.generated'
|
|
871
|
+
import * as pluginsModule from './plugins.generated'
|
|
872
|
+
import { registerGeneratedModules } from 'jax-hono/core/generated'
|
|
873
|
+
|
|
874
|
+
registerGeneratedModules({
|
|
875
|
+
'config.ts': configModule,
|
|
876
|
+
'controllers.generated.ts': controllersModule,
|
|
877
|
+
'middlewares.generated.ts': middlewaresModule,
|
|
878
|
+
'models.generated.ts': modelsModule,
|
|
879
|
+
'services.generated.ts': servicesModule,
|
|
880
|
+
'crons.generated.ts': cronsModule,
|
|
881
|
+
'plugins.generated.ts': pluginsModule,
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
export * from 'jax-hono'
|
|
885
|
+
`
|
|
886
|
+
|
|
887
|
+
writeGeneratedFile('index.ts', content)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export function generateRegistry() {
|
|
891
|
+
generateControllersRegistry()
|
|
892
|
+
generateMiddlewaresRegistry()
|
|
893
|
+
generateModelsRegistry()
|
|
894
|
+
generateServicesRegistry()
|
|
895
|
+
generateCronsRegistry()
|
|
896
|
+
generatePluginContextRegistry()
|
|
897
|
+
generateGeneratedEntry()
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (import.meta.main) {
|
|
901
|
+
generateRegistry()
|
|
902
|
+
}
|