jax-hono 1.0.13 → 1.0.14
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 +17 -17
- package/bin/jax.js +66 -66
- package/bin/jax.ts +9 -9
- package/build/build.ts +19 -19
- package/build/generate-config.ts +116 -116
- package/build/generate-registry.ts +619 -619
- package/build/generated-path.ts +57 -57
- package/config.ts +76 -76
- package/core/api-controller.ts +290 -294
- package/core/app.ts +199 -199
- package/core/controller.ts +86 -86
- package/core/hono.ts +64 -64
- package/core/service.ts +46 -46
- package/docs/jax.md +272 -272
- package/helpers/config.ts +23 -23
- package/helpers/crud.ts +27 -34
- package/helpers/index.ts +4 -4
- package/helpers/route.ts +7 -7
- package/index.ts +116 -116
- package/middleware/api-response.ts +44 -44
- package/middleware/jwt.ts +90 -90
- package/middleware/request-log.ts +3 -3
- package/package.json +124 -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 +11 -11
- package/types/config.ts +9 -9
- package/types/context.ts +10 -10
- package/types/index.ts +2 -2
- package/utils/array.ts +7 -7
- package/utils/async.ts +47 -47
- package/utils/crypto.ts +9 -9
- package/utils/index.ts +5 -5
- package/utils/regexp.ts +17 -17
- package/utils/transform.ts +3 -3
|
@@ -1,619 +1,619 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { cwd } from 'node:process'
|
|
3
|
-
import { basename, join, parse, relative } from 'node:path'
|
|
4
|
-
import { ensureGeneratedDir, generatedDir, packageDir, projectDir } from './generated-path'
|
|
5
|
-
|
|
6
|
-
const srcDir = join(cwd(), 'src')
|
|
7
|
-
const moduleExtensions = new Set(['.ts', '.js', '.mjs', '.cjs'])
|
|
8
|
-
const generatedHeader = `// This file is generated by jax-hono/build/generate-registry.ts.
|
|
9
|
-
// Do not edit it directly.
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
type ControllerGroup = {
|
|
13
|
-
name: string
|
|
14
|
-
dirs: string[]
|
|
15
|
-
suffix?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type PluginConfigItem =
|
|
19
|
-
| false
|
|
20
|
-
| {
|
|
21
|
-
enable?: boolean
|
|
22
|
-
package?: string
|
|
23
|
-
options?: unknown
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type PluginConfig = Record<string, PluginConfigItem>
|
|
27
|
-
|
|
28
|
-
const controllerGroups: ControllerGroup[] = [
|
|
29
|
-
{ name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
|
|
30
|
-
{ name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
|
|
31
|
-
{ name: 'open', dirs: ['open', 'controllers/open', 'controller/open', 'modules'], suffix: '.open' },
|
|
32
|
-
{ name: 'client', dirs: ['client', 'controllers/client', 'controller/client', 'modules'], suffix: '.client' },
|
|
33
|
-
{ name: 'controller', dirs: ['controllers', 'controller'] },
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
const scopedControllerGroups = new Set(
|
|
37
|
-
controllerGroups
|
|
38
|
-
.filter((group) => group.name !== 'controller')
|
|
39
|
-
.map((group) => group.name),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
function walkFiles(dir: string): string[] {
|
|
43
|
-
if (!existsSync(dir)) {
|
|
44
|
-
return []
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const files: string[] = []
|
|
48
|
-
|
|
49
|
-
for (const item of readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
-
const itemPath = join(dir, item.name)
|
|
51
|
-
|
|
52
|
-
if (item.isDirectory()) {
|
|
53
|
-
files.push(...walkFiles(itemPath))
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (item.isFile() && moduleExtensions.has(parse(item.name).ext)) {
|
|
58
|
-
files.push(itemPath)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return files
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function writeGeneratedFile(fileName: string, content: string) {
|
|
66
|
-
const outputFile = join(generatedDir, fileName)
|
|
67
|
-
|
|
68
|
-
ensureGeneratedDir()
|
|
69
|
-
|
|
70
|
-
if (existsSync(outputFile) && readFileSync(outputFile, 'utf8') === content) {
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
writeFileSync(outputFile, content)
|
|
75
|
-
console.log(`Generated ${relative(projectDir, outputFile)}.`)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function toImportPath(filePath: string) {
|
|
79
|
-
const path = filePath
|
|
80
|
-
.replace(parse(filePath).ext, '')
|
|
81
|
-
.replaceAll('\\', '/')
|
|
82
|
-
|
|
83
|
-
return path
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function toIdentifier(parts: string[]) {
|
|
87
|
-
const value = parts
|
|
88
|
-
.join('_')
|
|
89
|
-
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
90
|
-
.replace(/^([^a-zA-Z_$])/, '_$1')
|
|
91
|
-
|
|
92
|
-
return value || 'module'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function toCamelCase(value: string) {
|
|
96
|
-
return value.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase())
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function toPascalCase(value: string) {
|
|
100
|
-
const camel = toCamelCase(value)
|
|
101
|
-
|
|
102
|
-
return camel.charAt(0).toUpperCase() + camel.slice(1)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function toServiceKey(name: string) {
|
|
106
|
-
const baseName = name.endsWith('Service')
|
|
107
|
-
? name.slice(0, -'Service'.length)
|
|
108
|
-
: name
|
|
109
|
-
|
|
110
|
-
return baseName.replace(/^./, (char) => char.toLowerCase())
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function toMiddlewareKey(exportName: string) {
|
|
114
|
-
return exportName.endsWith('Middleware')
|
|
115
|
-
? exportName.slice(0, -'Middleware'.length)
|
|
116
|
-
: exportName
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function stripKnownSuffix(modulePath: string, suffixes: string[]) {
|
|
120
|
-
for (const suffix of suffixes) {
|
|
121
|
-
if (modulePath.endsWith(suffix)) {
|
|
122
|
-
return modulePath.slice(0, -suffix.length)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return modulePath
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function normalizeModulePath(modulePath: string) {
|
|
130
|
-
const parts = modulePath.split('/')
|
|
131
|
-
const last = parts.at(-1)
|
|
132
|
-
const parent = parts.at(-2)
|
|
133
|
-
|
|
134
|
-
return last && parent && last === parent
|
|
135
|
-
? parts.slice(0, -1).join('/')
|
|
136
|
-
: modulePath
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
140
|
-
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
141
|
-
.replace(parse(filePath).ext, '')
|
|
142
|
-
.replaceAll('\\', '/')
|
|
143
|
-
const withoutIndex = modulePath.endsWith('/index')
|
|
144
|
-
? modulePath.slice(0, -'/index'.length)
|
|
145
|
-
: modulePath
|
|
146
|
-
|
|
147
|
-
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
148
|
-
|
|
149
|
-
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function uniqueSorted(values: string[]) {
|
|
153
|
-
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function getFilesFromDirs(dirs: string[]) {
|
|
157
|
-
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isFileInDir(filePath: string, dirName: string) {
|
|
161
|
-
const rel = relative(join(srcDir, dirName), filePath)
|
|
162
|
-
|
|
163
|
-
return rel && !rel.startsWith('..')
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function isSuffixedModuleFile(filePath: string, suffix: string) {
|
|
167
|
-
return relative(srcDir, filePath)
|
|
168
|
-
.replace(parse(filePath).ext, '')
|
|
169
|
-
.replaceAll('\\', '/')
|
|
170
|
-
.endsWith(suffix)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function isModuleFeatureFile(filePath: string, suffix: string) {
|
|
174
|
-
return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function hasDefaultExport(filePath: string) {
|
|
178
|
-
return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function indentObject(content: string, spaces = 2) {
|
|
182
|
-
if (!content) {
|
|
183
|
-
return ''
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const indent = ' '.repeat(spaces)
|
|
187
|
-
|
|
188
|
-
return content
|
|
189
|
-
.split('\n')
|
|
190
|
-
.map((line) => `${indent}${line}`)
|
|
191
|
-
.join('\n')
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function buildNestedObject(entries: { path: string[]; value: string }[]): string {
|
|
195
|
-
const groups = new Map<string, { path: string[]; value: string }[]>()
|
|
196
|
-
const leaves: { key: string; value: string }[] = []
|
|
197
|
-
|
|
198
|
-
for (const entry of entries) {
|
|
199
|
-
const [head, ...tail] = entry.path
|
|
200
|
-
|
|
201
|
-
if (!head) {
|
|
202
|
-
continue
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (tail.length === 0) {
|
|
206
|
-
leaves.push({ key: head, value: entry.value })
|
|
207
|
-
continue
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
groups.set(head, [...(groups.get(head) ?? []), { path: tail, value: entry.value }])
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const lines = [
|
|
214
|
-
...leaves.map((leaf) => `${toObjectKey(leaf.key)}: ${leaf.value},`),
|
|
215
|
-
...[...groups.entries()].map(([key, groupEntries]) => {
|
|
216
|
-
return `${toObjectKey(key)}: {\n${indentObject(buildNestedObject(groupEntries), 2)}\n},`
|
|
217
|
-
}),
|
|
218
|
-
]
|
|
219
|
-
|
|
220
|
-
return lines.join('\n')
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function generateControllersRegistry() {
|
|
224
|
-
const used = new Set<string>()
|
|
225
|
-
const imports: string[] = []
|
|
226
|
-
const groupLines: string[] = []
|
|
227
|
-
|
|
228
|
-
for (const group of controllerGroups) {
|
|
229
|
-
const entries = getFilesFromDirs(group.dirs)
|
|
230
|
-
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
231
|
-
.filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
|
|
232
|
-
.filter(hasDefaultExport)
|
|
233
|
-
.map((filePath, index) => {
|
|
234
|
-
const baseDir = group.dirs.find((dirName) => {
|
|
235
|
-
return isFileInDir(filePath, dirName)
|
|
236
|
-
}) ?? group.dirs[0]
|
|
237
|
-
const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
|
|
238
|
-
const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
|
|
239
|
-
|
|
240
|
-
return { modulePath, importName, filePath }
|
|
241
|
-
})
|
|
242
|
-
.filter((entry) => {
|
|
243
|
-
if (group.name !== 'controller') {
|
|
244
|
-
return true
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const [scope] = entry.modulePath.split('/')
|
|
248
|
-
|
|
249
|
-
return !scopedControllerGroups.has(scope)
|
|
250
|
-
})
|
|
251
|
-
.filter((entry) => {
|
|
252
|
-
const key = `${group.name}:${entry.modulePath}`
|
|
253
|
-
|
|
254
|
-
if (used.has(key)) {
|
|
255
|
-
return false
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
used.add(key)
|
|
259
|
-
|
|
260
|
-
return true
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
|
|
264
|
-
|
|
265
|
-
if (entries.length === 0) {
|
|
266
|
-
continue
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const objectBody = indentObject(
|
|
270
|
-
buildNestedObject(entries.map((entry) => ({
|
|
271
|
-
path: entry.modulePath.split('/').map(toCamelCase),
|
|
272
|
-
value: `createController(${entry.importName})`,
|
|
273
|
-
}))),
|
|
274
|
-
4,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
groupLines.push(` ${group.name}: {\n${objectBody}\n },`)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const helperImport = imports.length > 0
|
|
281
|
-
? `import { createController } from 'jax-hono/core/controller'\n`
|
|
282
|
-
: ''
|
|
283
|
-
const content = `${generatedHeader}
|
|
284
|
-
${helperImport}${imports.join('\n')}
|
|
285
|
-
|
|
286
|
-
export const controllers = {
|
|
287
|
-
${groupLines.join('\n')}
|
|
288
|
-
}
|
|
289
|
-
`
|
|
290
|
-
|
|
291
|
-
writeGeneratedFile('controllers.generated.ts', content)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function toObjectKey(value: string) {
|
|
295
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function generateMiddlewaresRegistry() {
|
|
299
|
-
const files = getFilesFromDirs(['middlewares', 'middleware', 'modules'])
|
|
300
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.middleware'))
|
|
301
|
-
const imports: string[] = []
|
|
302
|
-
const entries: string[] = []
|
|
303
|
-
|
|
304
|
-
files.forEach((filePath, index) => {
|
|
305
|
-
const source = readFileSync(filePath, 'utf8')
|
|
306
|
-
const exportNames = [...source.matchAll(/\bexport\s+(?:const|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
|
|
307
|
-
.map((match) => match[1])
|
|
308
|
-
.sort((left, right) => left.localeCompare(right))
|
|
309
|
-
|
|
310
|
-
if (exportNames.length === 0) {
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
imports.push(`import { ${exportNames.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
315
|
-
entries.push(...exportNames.map((name) => ` ${toMiddlewareKey(name)}: ${name},`))
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
const content = `${generatedHeader}
|
|
319
|
-
${imports.join('\n')}
|
|
320
|
-
|
|
321
|
-
export const middlewares = {
|
|
322
|
-
${entries.join('\n')}
|
|
323
|
-
}
|
|
324
|
-
`
|
|
325
|
-
|
|
326
|
-
writeGeneratedFile('middlewares.generated.ts', content)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function generateModelsRegistry() {
|
|
330
|
-
const files = getFilesFromDirs(['models', 'model', 'modules'])
|
|
331
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.model'))
|
|
332
|
-
const imports: string[] = []
|
|
333
|
-
const entries: string[] = []
|
|
334
|
-
|
|
335
|
-
files.forEach((filePath) => {
|
|
336
|
-
const source = readFileSync(filePath, 'utf8')
|
|
337
|
-
const exportedModels = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*create(?:Loose)?Model\s*\(/g)]
|
|
338
|
-
.map((match) => match[1])
|
|
339
|
-
.sort((left, right) => left.localeCompare(right))
|
|
340
|
-
|
|
341
|
-
if (exportedModels.length === 0) {
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const modelKey = source.match(/\bexport\s+const\s+modelKey\s*=\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]/)?.[1]
|
|
346
|
-
const fileModelName = toPascalCase(toModulePath(
|
|
347
|
-
filePath.includes(`${srcDir}/modules/`) ? 'modules' : basename(parse(filePath).dir),
|
|
348
|
-
filePath,
|
|
349
|
-
['.model'],
|
|
350
|
-
))
|
|
351
|
-
|
|
352
|
-
imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
353
|
-
entries.push(...exportedModels.map((name) => {
|
|
354
|
-
const key = exportedModels.length === 1 && modelKey
|
|
355
|
-
? modelKey
|
|
356
|
-
: name === `${fileModelName}Model`
|
|
357
|
-
? fileModelName
|
|
358
|
-
: name
|
|
359
|
-
|
|
360
|
-
return ` ${key}: ${name},`
|
|
361
|
-
}))
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
const content = `${generatedHeader}
|
|
365
|
-
${imports.join('\n')}
|
|
366
|
-
|
|
367
|
-
export const models = {
|
|
368
|
-
${entries.join('\n')}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export type Models = typeof models
|
|
372
|
-
`
|
|
373
|
-
|
|
374
|
-
writeGeneratedFile('models.generated.ts', content)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function generateServicesRegistry() {
|
|
378
|
-
const files = getFilesFromDirs(['services', 'service', 'modules'])
|
|
379
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.service'))
|
|
380
|
-
const imports: string[] = []
|
|
381
|
-
const entries: string[] = []
|
|
382
|
-
|
|
383
|
-
files.forEach((filePath) => {
|
|
384
|
-
const source = readFileSync(filePath, 'utf8')
|
|
385
|
-
const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
|
|
386
|
-
.map((match) => match[1])
|
|
387
|
-
.sort((left, right) => left.localeCompare(right))
|
|
388
|
-
const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
|
|
389
|
-
const defaultServiceName = defaultServiceClass
|
|
390
|
-
? defaultServiceClass[1] ?? `${toPascalCase(toModulePath('services', filePath))}Service`
|
|
391
|
-
: ''
|
|
392
|
-
const defaultImportName = defaultServiceName
|
|
393
|
-
? toIdentifier([defaultServiceName, 'Default'])
|
|
394
|
-
: ''
|
|
395
|
-
|
|
396
|
-
if (namedServiceClasses.length === 0 && !defaultServiceName) {
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (defaultServiceName) {
|
|
401
|
-
imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
|
|
402
|
-
entries.push(` ${toServiceKey(defaultServiceName)}: new ${defaultImportName}(deps),`)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (namedServiceClasses.length > 0) {
|
|
406
|
-
imports.push(`import { ${namedServiceClasses.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
407
|
-
entries.push(...namedServiceClasses.map((name) => ` ${toServiceKey(name)}: new ${name}(deps),`))
|
|
408
|
-
}
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
const content = `${generatedHeader}
|
|
412
|
-
${imports.join('\n')}
|
|
413
|
-
|
|
414
|
-
export function createServices(deps: any) {
|
|
415
|
-
return {
|
|
416
|
-
${entries.join('\n')}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export type Services = ReturnType<typeof createServices>
|
|
421
|
-
`
|
|
422
|
-
|
|
423
|
-
writeGeneratedFile('services.generated.ts', content)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function findBalancedObject(source: string, startIndex: number) {
|
|
427
|
-
let depth = 0
|
|
428
|
-
let quote: string | undefined
|
|
429
|
-
let escaped = false
|
|
430
|
-
let lineComment = false
|
|
431
|
-
let blockComment = false
|
|
432
|
-
|
|
433
|
-
for (let index = startIndex; index < source.length; index += 1) {
|
|
434
|
-
const char = source[index]
|
|
435
|
-
const next = source[index + 1]
|
|
436
|
-
|
|
437
|
-
if (lineComment) {
|
|
438
|
-
if (char === '\n') {
|
|
439
|
-
lineComment = false
|
|
440
|
-
}
|
|
441
|
-
continue
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (blockComment) {
|
|
445
|
-
if (char === '*' && next === '/') {
|
|
446
|
-
blockComment = false
|
|
447
|
-
index += 1
|
|
448
|
-
}
|
|
449
|
-
continue
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (quote) {
|
|
453
|
-
if (escaped) {
|
|
454
|
-
escaped = false
|
|
455
|
-
continue
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (char === '\\') {
|
|
459
|
-
escaped = true
|
|
460
|
-
continue
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (char === quote) {
|
|
464
|
-
quote = undefined
|
|
465
|
-
}
|
|
466
|
-
continue
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (char === '/' && next === '/') {
|
|
470
|
-
lineComment = true
|
|
471
|
-
index += 1
|
|
472
|
-
continue
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (char === '/' && next === '*') {
|
|
476
|
-
blockComment = true
|
|
477
|
-
index += 1
|
|
478
|
-
continue
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (char === '"' || char === '\'' || char === '`') {
|
|
482
|
-
quote = char
|
|
483
|
-
continue
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (char === '{') {
|
|
487
|
-
depth += 1
|
|
488
|
-
continue
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (char === '}') {
|
|
492
|
-
depth -= 1
|
|
493
|
-
|
|
494
|
-
if (depth === 0) {
|
|
495
|
-
return source.slice(startIndex, index + 1)
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return undefined
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function extractPluginConfigObject(source: string) {
|
|
504
|
-
const configCallIndex = source.indexOf('definePluginConfig')
|
|
505
|
-
const searchStart = configCallIndex >= 0 ? configCallIndex : source.indexOf('export default')
|
|
506
|
-
const objectStart = searchStart >= 0 ? source.indexOf('{', searchStart) : -1
|
|
507
|
-
|
|
508
|
-
if (objectStart < 0) {
|
|
509
|
-
return undefined
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
return findBalancedObject(source, objectStart)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function readPluginConfig(filePath: string): PluginConfig {
|
|
516
|
-
if (!existsSync(filePath)) {
|
|
517
|
-
return {}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const objectSource = extractPluginConfigObject(readFileSync(filePath, 'utf8'))
|
|
521
|
-
|
|
522
|
-
if (!objectSource) {
|
|
523
|
-
return {}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
const value = new Function(`return (${objectSource})`)() as unknown
|
|
528
|
-
|
|
529
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
530
|
-
? (value as PluginConfig)
|
|
531
|
-
: {}
|
|
532
|
-
} catch (error) {
|
|
533
|
-
throw new Error(`Invalid plugin config ${relative(srcDir, filePath)}: ${(error as Error).message}`)
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function normalizePluginPackage(packageName: string, configFile: string) {
|
|
538
|
-
if (!packageName.startsWith('./') && !packageName.startsWith('../')) {
|
|
539
|
-
return packageName
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const normalized = join(configFile, '..', packageName)
|
|
543
|
-
.replaceAll('\\', '/')
|
|
544
|
-
.replace(/\/index$/, '')
|
|
545
|
-
|
|
546
|
-
return normalized
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
550
|
-
const merged = new Map<string, Exclude<PluginConfigItem, false>>()
|
|
551
|
-
|
|
552
|
-
for (const { filePath, config } of configs) {
|
|
553
|
-
for (const [name, item] of Object.entries(config)) {
|
|
554
|
-
if (item === false) {
|
|
555
|
-
merged.delete(name)
|
|
556
|
-
continue
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const current = merged.get(name) ?? {}
|
|
560
|
-
const next = { ...current, ...item }
|
|
561
|
-
|
|
562
|
-
if (next.package) {
|
|
563
|
-
next.package = normalizePluginPackage(next.package, filePath)
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
merged.set(name, next)
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return [...merged.entries()]
|
|
571
|
-
.filter((entry): entry is [string, Exclude<PluginConfigItem, false> & { package: string }] => {
|
|
572
|
-
const [, item] = entry
|
|
573
|
-
|
|
574
|
-
return item.enable === true && typeof item.package === 'string' && item.package.length > 0
|
|
575
|
-
})
|
|
576
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function serializePluginOptions(options: unknown) {
|
|
580
|
-
return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function generatePluginContextRegistry() {
|
|
584
|
-
const enabledPlugins = mergePluginConfig([
|
|
585
|
-
{ filePath: join(packageDir, 'plugins/config.ts'), config: readPluginConfig(join(packageDir, 'plugins/config.ts')) },
|
|
586
|
-
{ filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
|
|
587
|
-
])
|
|
588
|
-
const imports = enabledPlugins.map(([name, item]) => {
|
|
589
|
-
return `import ${toIdentifier(['plugin', name])} from ${JSON.stringify(item.package)}`
|
|
590
|
-
})
|
|
591
|
-
const moduleEntries = enabledPlugins.map(
|
|
592
|
-
([name, item]) =>
|
|
593
|
-
` { name: ${JSON.stringify(name)}, module: ${toIdentifier(['plugin', name])}${serializePluginOptions(item.options)} },`,
|
|
594
|
-
)
|
|
595
|
-
|
|
596
|
-
const content = `${generatedHeader}${imports.length > 0 ? `${imports.join('\n')}\n\n` : ''}
|
|
597
|
-
export const plugins = {} as Record<string, unknown>
|
|
598
|
-
|
|
599
|
-
export type Plugins = typeof plugins
|
|
600
|
-
|
|
601
|
-
export const pluginModules = [
|
|
602
|
-
${moduleEntries.join('\n')}
|
|
603
|
-
] as const
|
|
604
|
-
`
|
|
605
|
-
|
|
606
|
-
writeGeneratedFile('plugins.generated.ts', content)
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export function generateRegistry() {
|
|
610
|
-
generateControllersRegistry()
|
|
611
|
-
generateMiddlewaresRegistry()
|
|
612
|
-
generateModelsRegistry()
|
|
613
|
-
generateServicesRegistry()
|
|
614
|
-
generatePluginContextRegistry()
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (import.meta.main) {
|
|
618
|
-
generateRegistry()
|
|
619
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { cwd } from 'node:process'
|
|
3
|
+
import { basename, join, parse, relative } from 'node:path'
|
|
4
|
+
import { ensureGeneratedDir, generatedDir, packageDir, projectDir } from './generated-path'
|
|
5
|
+
|
|
6
|
+
const srcDir = join(cwd(), 'src')
|
|
7
|
+
const moduleExtensions = new Set(['.ts', '.js', '.mjs', '.cjs'])
|
|
8
|
+
const generatedHeader = `// This file is generated by jax-hono/build/generate-registry.ts.
|
|
9
|
+
// Do not edit it directly.
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
type ControllerGroup = {
|
|
13
|
+
name: string
|
|
14
|
+
dirs: string[]
|
|
15
|
+
suffix?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PluginConfigItem =
|
|
19
|
+
| false
|
|
20
|
+
| {
|
|
21
|
+
enable?: boolean
|
|
22
|
+
package?: string
|
|
23
|
+
options?: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type PluginConfig = Record<string, PluginConfigItem>
|
|
27
|
+
|
|
28
|
+
const controllerGroups: ControllerGroup[] = [
|
|
29
|
+
{ name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
|
|
30
|
+
{ name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
|
|
31
|
+
{ name: 'open', dirs: ['open', 'controllers/open', 'controller/open', 'modules'], suffix: '.open' },
|
|
32
|
+
{ name: 'client', dirs: ['client', 'controllers/client', 'controller/client', 'modules'], suffix: '.client' },
|
|
33
|
+
{ name: 'controller', dirs: ['controllers', 'controller'] },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const scopedControllerGroups = new Set(
|
|
37
|
+
controllerGroups
|
|
38
|
+
.filter((group) => group.name !== 'controller')
|
|
39
|
+
.map((group) => group.name),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
function walkFiles(dir: string): string[] {
|
|
43
|
+
if (!existsSync(dir)) {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const files: string[] = []
|
|
48
|
+
|
|
49
|
+
for (const item of readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const itemPath = join(dir, item.name)
|
|
51
|
+
|
|
52
|
+
if (item.isDirectory()) {
|
|
53
|
+
files.push(...walkFiles(itemPath))
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (item.isFile() && moduleExtensions.has(parse(item.name).ext)) {
|
|
58
|
+
files.push(itemPath)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return files
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeGeneratedFile(fileName: string, content: string) {
|
|
66
|
+
const outputFile = join(generatedDir, fileName)
|
|
67
|
+
|
|
68
|
+
ensureGeneratedDir()
|
|
69
|
+
|
|
70
|
+
if (existsSync(outputFile) && readFileSync(outputFile, 'utf8') === content) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
writeFileSync(outputFile, content)
|
|
75
|
+
console.log(`Generated ${relative(projectDir, outputFile)}.`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toImportPath(filePath: string) {
|
|
79
|
+
const path = filePath
|
|
80
|
+
.replace(parse(filePath).ext, '')
|
|
81
|
+
.replaceAll('\\', '/')
|
|
82
|
+
|
|
83
|
+
return path
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toIdentifier(parts: string[]) {
|
|
87
|
+
const value = parts
|
|
88
|
+
.join('_')
|
|
89
|
+
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
90
|
+
.replace(/^([^a-zA-Z_$])/, '_$1')
|
|
91
|
+
|
|
92
|
+
return value || 'module'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toCamelCase(value: string) {
|
|
96
|
+
return value.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toPascalCase(value: string) {
|
|
100
|
+
const camel = toCamelCase(value)
|
|
101
|
+
|
|
102
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toServiceKey(name: string) {
|
|
106
|
+
const baseName = name.endsWith('Service')
|
|
107
|
+
? name.slice(0, -'Service'.length)
|
|
108
|
+
: name
|
|
109
|
+
|
|
110
|
+
return baseName.replace(/^./, (char) => char.toLowerCase())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toMiddlewareKey(exportName: string) {
|
|
114
|
+
return exportName.endsWith('Middleware')
|
|
115
|
+
? exportName.slice(0, -'Middleware'.length)
|
|
116
|
+
: exportName
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stripKnownSuffix(modulePath: string, suffixes: string[]) {
|
|
120
|
+
for (const suffix of suffixes) {
|
|
121
|
+
if (modulePath.endsWith(suffix)) {
|
|
122
|
+
return modulePath.slice(0, -suffix.length)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return modulePath
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeModulePath(modulePath: string) {
|
|
130
|
+
const parts = modulePath.split('/')
|
|
131
|
+
const last = parts.at(-1)
|
|
132
|
+
const parent = parts.at(-2)
|
|
133
|
+
|
|
134
|
+
return last && parent && last === parent
|
|
135
|
+
? parts.slice(0, -1).join('/')
|
|
136
|
+
: modulePath
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
140
|
+
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
141
|
+
.replace(parse(filePath).ext, '')
|
|
142
|
+
.replaceAll('\\', '/')
|
|
143
|
+
const withoutIndex = modulePath.endsWith('/index')
|
|
144
|
+
? modulePath.slice(0, -'/index'.length)
|
|
145
|
+
: modulePath
|
|
146
|
+
|
|
147
|
+
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
148
|
+
|
|
149
|
+
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function uniqueSorted(values: string[]) {
|
|
153
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getFilesFromDirs(dirs: string[]) {
|
|
157
|
+
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isFileInDir(filePath: string, dirName: string) {
|
|
161
|
+
const rel = relative(join(srcDir, dirName), filePath)
|
|
162
|
+
|
|
163
|
+
return rel && !rel.startsWith('..')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isSuffixedModuleFile(filePath: string, suffix: string) {
|
|
167
|
+
return relative(srcDir, filePath)
|
|
168
|
+
.replace(parse(filePath).ext, '')
|
|
169
|
+
.replaceAll('\\', '/')
|
|
170
|
+
.endsWith(suffix)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isModuleFeatureFile(filePath: string, suffix: string) {
|
|
174
|
+
return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasDefaultExport(filePath: string) {
|
|
178
|
+
return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function indentObject(content: string, spaces = 2) {
|
|
182
|
+
if (!content) {
|
|
183
|
+
return ''
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const indent = ' '.repeat(spaces)
|
|
187
|
+
|
|
188
|
+
return content
|
|
189
|
+
.split('\n')
|
|
190
|
+
.map((line) => `${indent}${line}`)
|
|
191
|
+
.join('\n')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildNestedObject(entries: { path: string[]; value: string }[]): string {
|
|
195
|
+
const groups = new Map<string, { path: string[]; value: string }[]>()
|
|
196
|
+
const leaves: { key: string; value: string }[] = []
|
|
197
|
+
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
const [head, ...tail] = entry.path
|
|
200
|
+
|
|
201
|
+
if (!head) {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (tail.length === 0) {
|
|
206
|
+
leaves.push({ key: head, value: entry.value })
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
groups.set(head, [...(groups.get(head) ?? []), { path: tail, value: entry.value }])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lines = [
|
|
214
|
+
...leaves.map((leaf) => `${toObjectKey(leaf.key)}: ${leaf.value},`),
|
|
215
|
+
...[...groups.entries()].map(([key, groupEntries]) => {
|
|
216
|
+
return `${toObjectKey(key)}: {\n${indentObject(buildNestedObject(groupEntries), 2)}\n},`
|
|
217
|
+
}),
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
return lines.join('\n')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function generateControllersRegistry() {
|
|
224
|
+
const used = new Set<string>()
|
|
225
|
+
const imports: string[] = []
|
|
226
|
+
const groupLines: string[] = []
|
|
227
|
+
|
|
228
|
+
for (const group of controllerGroups) {
|
|
229
|
+
const entries = getFilesFromDirs(group.dirs)
|
|
230
|
+
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
231
|
+
.filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
|
|
232
|
+
.filter(hasDefaultExport)
|
|
233
|
+
.map((filePath, index) => {
|
|
234
|
+
const baseDir = group.dirs.find((dirName) => {
|
|
235
|
+
return isFileInDir(filePath, dirName)
|
|
236
|
+
}) ?? group.dirs[0]
|
|
237
|
+
const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
|
|
238
|
+
const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
|
|
239
|
+
|
|
240
|
+
return { modulePath, importName, filePath }
|
|
241
|
+
})
|
|
242
|
+
.filter((entry) => {
|
|
243
|
+
if (group.name !== 'controller') {
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const [scope] = entry.modulePath.split('/')
|
|
248
|
+
|
|
249
|
+
return !scopedControllerGroups.has(scope)
|
|
250
|
+
})
|
|
251
|
+
.filter((entry) => {
|
|
252
|
+
const key = `${group.name}:${entry.modulePath}`
|
|
253
|
+
|
|
254
|
+
if (used.has(key)) {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
used.add(key)
|
|
259
|
+
|
|
260
|
+
return true
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
|
|
264
|
+
|
|
265
|
+
if (entries.length === 0) {
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const objectBody = indentObject(
|
|
270
|
+
buildNestedObject(entries.map((entry) => ({
|
|
271
|
+
path: entry.modulePath.split('/').map(toCamelCase),
|
|
272
|
+
value: `createController(${entry.importName})`,
|
|
273
|
+
}))),
|
|
274
|
+
4,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
groupLines.push(` ${group.name}: {\n${objectBody}\n },`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const helperImport = imports.length > 0
|
|
281
|
+
? `import { createController } from 'jax-hono/core/controller'\n`
|
|
282
|
+
: ''
|
|
283
|
+
const content = `${generatedHeader}
|
|
284
|
+
${helperImport}${imports.join('\n')}
|
|
285
|
+
|
|
286
|
+
export const controllers = {
|
|
287
|
+
${groupLines.join('\n')}
|
|
288
|
+
}
|
|
289
|
+
`
|
|
290
|
+
|
|
291
|
+
writeGeneratedFile('controllers.generated.ts', content)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function toObjectKey(value: string) {
|
|
295
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function generateMiddlewaresRegistry() {
|
|
299
|
+
const files = getFilesFromDirs(['middlewares', 'middleware', 'modules'])
|
|
300
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.middleware'))
|
|
301
|
+
const imports: string[] = []
|
|
302
|
+
const entries: string[] = []
|
|
303
|
+
|
|
304
|
+
files.forEach((filePath, index) => {
|
|
305
|
+
const source = readFileSync(filePath, 'utf8')
|
|
306
|
+
const exportNames = [...source.matchAll(/\bexport\s+(?:const|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
|
|
307
|
+
.map((match) => match[1])
|
|
308
|
+
.sort((left, right) => left.localeCompare(right))
|
|
309
|
+
|
|
310
|
+
if (exportNames.length === 0) {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
imports.push(`import { ${exportNames.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
315
|
+
entries.push(...exportNames.map((name) => ` ${toMiddlewareKey(name)}: ${name},`))
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const content = `${generatedHeader}
|
|
319
|
+
${imports.join('\n')}
|
|
320
|
+
|
|
321
|
+
export const middlewares = {
|
|
322
|
+
${entries.join('\n')}
|
|
323
|
+
}
|
|
324
|
+
`
|
|
325
|
+
|
|
326
|
+
writeGeneratedFile('middlewares.generated.ts', content)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function generateModelsRegistry() {
|
|
330
|
+
const files = getFilesFromDirs(['models', 'model', 'modules'])
|
|
331
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.model'))
|
|
332
|
+
const imports: string[] = []
|
|
333
|
+
const entries: string[] = []
|
|
334
|
+
|
|
335
|
+
files.forEach((filePath) => {
|
|
336
|
+
const source = readFileSync(filePath, 'utf8')
|
|
337
|
+
const exportedModels = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*create(?:Loose)?Model\s*\(/g)]
|
|
338
|
+
.map((match) => match[1])
|
|
339
|
+
.sort((left, right) => left.localeCompare(right))
|
|
340
|
+
|
|
341
|
+
if (exportedModels.length === 0) {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const modelKey = source.match(/\bexport\s+const\s+modelKey\s*=\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]/)?.[1]
|
|
346
|
+
const fileModelName = toPascalCase(toModulePath(
|
|
347
|
+
filePath.includes(`${srcDir}/modules/`) ? 'modules' : basename(parse(filePath).dir),
|
|
348
|
+
filePath,
|
|
349
|
+
['.model'],
|
|
350
|
+
))
|
|
351
|
+
|
|
352
|
+
imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
353
|
+
entries.push(...exportedModels.map((name) => {
|
|
354
|
+
const key = exportedModels.length === 1 && modelKey
|
|
355
|
+
? modelKey
|
|
356
|
+
: name === `${fileModelName}Model`
|
|
357
|
+
? fileModelName
|
|
358
|
+
: name
|
|
359
|
+
|
|
360
|
+
return ` ${key}: ${name},`
|
|
361
|
+
}))
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const content = `${generatedHeader}
|
|
365
|
+
${imports.join('\n')}
|
|
366
|
+
|
|
367
|
+
export const models = {
|
|
368
|
+
${entries.join('\n')}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export type Models = typeof models
|
|
372
|
+
`
|
|
373
|
+
|
|
374
|
+
writeGeneratedFile('models.generated.ts', content)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function generateServicesRegistry() {
|
|
378
|
+
const files = getFilesFromDirs(['services', 'service', 'modules'])
|
|
379
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.service'))
|
|
380
|
+
const imports: string[] = []
|
|
381
|
+
const entries: string[] = []
|
|
382
|
+
|
|
383
|
+
files.forEach((filePath) => {
|
|
384
|
+
const source = readFileSync(filePath, 'utf8')
|
|
385
|
+
const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
|
|
386
|
+
.map((match) => match[1])
|
|
387
|
+
.sort((left, right) => left.localeCompare(right))
|
|
388
|
+
const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
|
|
389
|
+
const defaultServiceName = defaultServiceClass
|
|
390
|
+
? defaultServiceClass[1] ?? `${toPascalCase(toModulePath('services', filePath))}Service`
|
|
391
|
+
: ''
|
|
392
|
+
const defaultImportName = defaultServiceName
|
|
393
|
+
? toIdentifier([defaultServiceName, 'Default'])
|
|
394
|
+
: ''
|
|
395
|
+
|
|
396
|
+
if (namedServiceClasses.length === 0 && !defaultServiceName) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (defaultServiceName) {
|
|
401
|
+
imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
|
|
402
|
+
entries.push(` ${toServiceKey(defaultServiceName)}: new ${defaultImportName}(deps),`)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (namedServiceClasses.length > 0) {
|
|
406
|
+
imports.push(`import { ${namedServiceClasses.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
407
|
+
entries.push(...namedServiceClasses.map((name) => ` ${toServiceKey(name)}: new ${name}(deps),`))
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const content = `${generatedHeader}
|
|
412
|
+
${imports.join('\n')}
|
|
413
|
+
|
|
414
|
+
export function createServices(deps: any) {
|
|
415
|
+
return {
|
|
416
|
+
${entries.join('\n')}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export type Services = ReturnType<typeof createServices>
|
|
421
|
+
`
|
|
422
|
+
|
|
423
|
+
writeGeneratedFile('services.generated.ts', content)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function findBalancedObject(source: string, startIndex: number) {
|
|
427
|
+
let depth = 0
|
|
428
|
+
let quote: string | undefined
|
|
429
|
+
let escaped = false
|
|
430
|
+
let lineComment = false
|
|
431
|
+
let blockComment = false
|
|
432
|
+
|
|
433
|
+
for (let index = startIndex; index < source.length; index += 1) {
|
|
434
|
+
const char = source[index]
|
|
435
|
+
const next = source[index + 1]
|
|
436
|
+
|
|
437
|
+
if (lineComment) {
|
|
438
|
+
if (char === '\n') {
|
|
439
|
+
lineComment = false
|
|
440
|
+
}
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (blockComment) {
|
|
445
|
+
if (char === '*' && next === '/') {
|
|
446
|
+
blockComment = false
|
|
447
|
+
index += 1
|
|
448
|
+
}
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (quote) {
|
|
453
|
+
if (escaped) {
|
|
454
|
+
escaped = false
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (char === '\\') {
|
|
459
|
+
escaped = true
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (char === quote) {
|
|
464
|
+
quote = undefined
|
|
465
|
+
}
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (char === '/' && next === '/') {
|
|
470
|
+
lineComment = true
|
|
471
|
+
index += 1
|
|
472
|
+
continue
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (char === '/' && next === '*') {
|
|
476
|
+
blockComment = true
|
|
477
|
+
index += 1
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (char === '"' || char === '\'' || char === '`') {
|
|
482
|
+
quote = char
|
|
483
|
+
continue
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (char === '{') {
|
|
487
|
+
depth += 1
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (char === '}') {
|
|
492
|
+
depth -= 1
|
|
493
|
+
|
|
494
|
+
if (depth === 0) {
|
|
495
|
+
return source.slice(startIndex, index + 1)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return undefined
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function extractPluginConfigObject(source: string) {
|
|
504
|
+
const configCallIndex = source.indexOf('definePluginConfig')
|
|
505
|
+
const searchStart = configCallIndex >= 0 ? configCallIndex : source.indexOf('export default')
|
|
506
|
+
const objectStart = searchStart >= 0 ? source.indexOf('{', searchStart) : -1
|
|
507
|
+
|
|
508
|
+
if (objectStart < 0) {
|
|
509
|
+
return undefined
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return findBalancedObject(source, objectStart)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function readPluginConfig(filePath: string): PluginConfig {
|
|
516
|
+
if (!existsSync(filePath)) {
|
|
517
|
+
return {}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const objectSource = extractPluginConfigObject(readFileSync(filePath, 'utf8'))
|
|
521
|
+
|
|
522
|
+
if (!objectSource) {
|
|
523
|
+
return {}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const value = new Function(`return (${objectSource})`)() as unknown
|
|
528
|
+
|
|
529
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
530
|
+
? (value as PluginConfig)
|
|
531
|
+
: {}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
throw new Error(`Invalid plugin config ${relative(srcDir, filePath)}: ${(error as Error).message}`)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function normalizePluginPackage(packageName: string, configFile: string) {
|
|
538
|
+
if (!packageName.startsWith('./') && !packageName.startsWith('../')) {
|
|
539
|
+
return packageName
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const normalized = join(configFile, '..', packageName)
|
|
543
|
+
.replaceAll('\\', '/')
|
|
544
|
+
.replace(/\/index$/, '')
|
|
545
|
+
|
|
546
|
+
return normalized
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
550
|
+
const merged = new Map<string, Exclude<PluginConfigItem, false>>()
|
|
551
|
+
|
|
552
|
+
for (const { filePath, config } of configs) {
|
|
553
|
+
for (const [name, item] of Object.entries(config)) {
|
|
554
|
+
if (item === false) {
|
|
555
|
+
merged.delete(name)
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const current = merged.get(name) ?? {}
|
|
560
|
+
const next = { ...current, ...item }
|
|
561
|
+
|
|
562
|
+
if (next.package) {
|
|
563
|
+
next.package = normalizePluginPackage(next.package, filePath)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
merged.set(name, next)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return [...merged.entries()]
|
|
571
|
+
.filter((entry): entry is [string, Exclude<PluginConfigItem, false> & { package: string }] => {
|
|
572
|
+
const [, item] = entry
|
|
573
|
+
|
|
574
|
+
return item.enable === true && typeof item.package === 'string' && item.package.length > 0
|
|
575
|
+
})
|
|
576
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function serializePluginOptions(options: unknown) {
|
|
580
|
+
return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function generatePluginContextRegistry() {
|
|
584
|
+
const enabledPlugins = mergePluginConfig([
|
|
585
|
+
{ filePath: join(packageDir, 'plugins/config.ts'), config: readPluginConfig(join(packageDir, 'plugins/config.ts')) },
|
|
586
|
+
{ filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
|
|
587
|
+
])
|
|
588
|
+
const imports = enabledPlugins.map(([name, item]) => {
|
|
589
|
+
return `import ${toIdentifier(['plugin', name])} from ${JSON.stringify(item.package)}`
|
|
590
|
+
})
|
|
591
|
+
const moduleEntries = enabledPlugins.map(
|
|
592
|
+
([name, item]) =>
|
|
593
|
+
` { name: ${JSON.stringify(name)}, module: ${toIdentifier(['plugin', name])}${serializePluginOptions(item.options)} },`,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
const content = `${generatedHeader}${imports.length > 0 ? `${imports.join('\n')}\n\n` : ''}
|
|
597
|
+
export const plugins = {} as Record<string, unknown>
|
|
598
|
+
|
|
599
|
+
export type Plugins = typeof plugins
|
|
600
|
+
|
|
601
|
+
export const pluginModules = [
|
|
602
|
+
${moduleEntries.join('\n')}
|
|
603
|
+
] as const
|
|
604
|
+
`
|
|
605
|
+
|
|
606
|
+
writeGeneratedFile('plugins.generated.ts', content)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function generateRegistry() {
|
|
610
|
+
generateControllersRegistry()
|
|
611
|
+
generateMiddlewaresRegistry()
|
|
612
|
+
generateModelsRegistry()
|
|
613
|
+
generateServicesRegistry()
|
|
614
|
+
generatePluginContextRegistry()
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (import.meta.main) {
|
|
618
|
+
generateRegistry()
|
|
619
|
+
}
|