jax-hono 1.0.15 → 1.0.17
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/build/generate-registry.ts +393 -176
- package/core/app.ts +18 -12
- package/core/cron.ts +151 -0
- package/docs/agent.md +45 -0
- package/docs/jax.md +40 -4
- package/index.ts +5 -3
- package/package.json +1 -1
- package/utils/crypto.ts +9 -3
- package/utils/index.ts +5 -3
- package/utils/order.ts +5 -0
- package/utils/regexp.ts +11 -3
- package/utils/validate.ts +7 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
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'])
|
|
8
10
|
const generatedHeader = `// This file is generated by jax-hono/build/generate-registry.ts.
|
|
9
11
|
// Do not edit it directly.
|
|
10
12
|
`
|
|
@@ -23,7 +25,13 @@ type PluginConfigItem =
|
|
|
23
25
|
options?: unknown
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
type PluginConfig = Record<string, PluginConfigItem>
|
|
28
|
+
type PluginConfig = Record<string, PluginConfigItem>
|
|
29
|
+
|
|
30
|
+
type EnabledPlugin = {
|
|
31
|
+
name: string
|
|
32
|
+
packageName: string
|
|
33
|
+
rootDir: string
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
const controllerGroups: ControllerGroup[] = [
|
|
29
37
|
{ name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
|
|
@@ -83,18 +91,36 @@ function toImportPath(filePath: string) {
|
|
|
83
91
|
return path
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
function toIdentifier(parts: string[]) {
|
|
87
|
-
const value = parts
|
|
88
|
-
.join('_')
|
|
94
|
+
function toIdentifier(parts: string[]) {
|
|
95
|
+
const value = parts
|
|
96
|
+
.join('_')
|
|
89
97
|
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
90
98
|
.replace(/^([^a-zA-Z_$])/, '_$1')
|
|
91
|
-
|
|
92
|
-
return value || 'module'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
124
|
|
|
99
125
|
function toPascalCase(value: string) {
|
|
100
126
|
const camel = toCamelCase(value)
|
|
@@ -146,8 +172,8 @@ function normalizeModulePath(modulePath: string) {
|
|
|
146
172
|
return modulePath
|
|
147
173
|
}
|
|
148
174
|
|
|
149
|
-
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
150
|
-
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
175
|
+
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
176
|
+
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
151
177
|
.replace(parse(filePath).ext, '')
|
|
152
178
|
.replaceAll('\\', '/')
|
|
153
179
|
const withoutIndex = modulePath.endsWith('/index')
|
|
@@ -156,16 +182,36 @@ function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []
|
|
|
156
182
|
|
|
157
183
|
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
158
184
|
|
|
159
|
-
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
160
|
-
}
|
|
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
|
+
}
|
|
161
199
|
|
|
162
200
|
function uniqueSorted(values: string[]) {
|
|
163
201
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
164
202
|
}
|
|
165
203
|
|
|
166
|
-
function getFilesFromDirs(dirs: string[]) {
|
|
167
|
-
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
168
|
-
}
|
|
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
|
+
}
|
|
169
215
|
|
|
170
216
|
function isFileInDir(filePath: string, dirName: string) {
|
|
171
217
|
const rel = relative(join(srcDir, dirName), filePath)
|
|
@@ -230,13 +276,14 @@ function buildNestedObject(entries: { path: string[]; value: string }[]): string
|
|
|
230
276
|
return lines.join('\n')
|
|
231
277
|
}
|
|
232
278
|
|
|
233
|
-
function generateControllersRegistry() {
|
|
234
|
-
const used = new Set<string>()
|
|
235
|
-
const imports: string[] = []
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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)
|
|
240
287
|
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
241
288
|
.filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
|
|
242
289
|
.filter(hasDefaultExport)
|
|
@@ -246,12 +293,26 @@ function generateControllersRegistry() {
|
|
|
246
293
|
}) ?? group.dirs[0]
|
|
247
294
|
const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
|
|
248
295
|
const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
|
|
296
|
+
const exportName = toLowerCamelIdentifier(group.name === 'controller' ? [modulePath] : [group.name, modulePath], 'Controller')
|
|
249
297
|
|
|
250
|
-
return { modulePath, importName, filePath }
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
255
316
|
}
|
|
256
317
|
|
|
257
318
|
const [scope] = entry.modulePath.split('/')
|
|
@@ -270,81 +331,106 @@ function generateControllersRegistry() {
|
|
|
270
331
|
return true
|
|
271
332
|
})
|
|
272
333
|
|
|
273
|
-
imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
277
339
|
}
|
|
278
340
|
|
|
279
|
-
const objectBody =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
4,
|
|
285
|
-
)
|
|
286
|
-
|
|
341
|
+
const objectBody = buildNestedObject(entries.map((entry) => ({
|
|
342
|
+
path: entry.modulePath.split('/').map(toCamelCase),
|
|
343
|
+
value: entry.exportName,
|
|
344
|
+
})))
|
|
345
|
+
|
|
287
346
|
if (group.name === 'controller') {
|
|
288
|
-
groupLines.push(objectBody)
|
|
347
|
+
groupLines.push(indentObject(objectBody, 2))
|
|
289
348
|
} else {
|
|
290
|
-
groupLines.push(` ${group.name}: {\n${objectBody}\n },`)
|
|
349
|
+
groupLines.push(` ${group.name}: {\n${indentObject(objectBody, 4)}\n },`)
|
|
291
350
|
}
|
|
292
351
|
}
|
|
293
352
|
|
|
294
353
|
const helperImport = imports.length > 0
|
|
295
354
|
? `import { createController } from 'jax-hono/core/controller'\n`
|
|
296
355
|
: ''
|
|
297
|
-
const content = `${generatedHeader}
|
|
298
|
-
${helperImport}${imports.join('\n')}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
356
|
+
const content = `${generatedHeader}
|
|
357
|
+
${helperImport}${imports.join('\n')}
|
|
358
|
+
|
|
359
|
+
${controllerExports.join('\n')}
|
|
360
|
+
|
|
361
|
+
export const controllers = {
|
|
362
|
+
${groupLines.join('\n')}
|
|
302
363
|
}
|
|
303
364
|
`
|
|
304
365
|
|
|
305
366
|
writeGeneratedFile('controllers.generated.ts', content)
|
|
306
367
|
}
|
|
307
368
|
|
|
308
|
-
function toObjectKey(value: string) {
|
|
309
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
369
|
+
function toObjectKey(value: string) {
|
|
370
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function formatExportBlock(names: string[]) {
|
|
374
|
+
if (names.length === 0) {
|
|
375
|
+
return ''
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return `export {\n${names.map((name) => ` ${name},`).join('\n')}\n}\n`
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function createGeneratedContent(sections: string[]) {
|
|
382
|
+
return `${generatedHeader}\n${sections
|
|
383
|
+
.map((section) => section.trimEnd())
|
|
384
|
+
.filter((section) => section.trim())
|
|
385
|
+
.join('\n\n')}\n`
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function generateMiddlewaresRegistry() {
|
|
389
|
+
const files = getFilesFromDirs(['middlewares', 'middleware', 'modules'])
|
|
390
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.middleware'))
|
|
391
|
+
const imports: string[] = []
|
|
392
|
+
const entries: string[] = []
|
|
393
|
+
const exportNames = new Set<string>()
|
|
394
|
+
|
|
395
|
+
files.forEach((filePath) => {
|
|
396
|
+
const source = readFileSync(filePath, 'utf8')
|
|
397
|
+
const middlewareNames = [...source.matchAll(/\bexport\s+(?:const|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
|
|
398
|
+
.map((match) => match[1])
|
|
399
|
+
.sort((left, right) => left.localeCompare(right))
|
|
400
|
+
|
|
401
|
+
if (middlewareNames.length === 0) {
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
imports.push(`import { ${middlewareNames.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
406
|
+
middlewareNames.forEach((name) => exportNames.add(name))
|
|
407
|
+
entries.push(...middlewareNames.map((name) => {
|
|
408
|
+
const key = toMiddlewareKey(name)
|
|
409
|
+
|
|
410
|
+
return key === name
|
|
411
|
+
? ` ${name},`
|
|
412
|
+
: ` ${key}: ${name},`
|
|
413
|
+
}))
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
417
|
+
const content = createGeneratedContent([
|
|
418
|
+
imports.join('\n'),
|
|
419
|
+
exportBlock,
|
|
420
|
+
`export const middlewares = {
|
|
421
|
+
${entries.join('\n')}
|
|
422
|
+
}`,
|
|
423
|
+
])
|
|
424
|
+
|
|
425
|
+
writeGeneratedFile('middlewares.generated.ts', content)
|
|
426
|
+
}
|
|
342
427
|
|
|
343
428
|
function generateModelsRegistry() {
|
|
344
|
-
const files = getFilesFromDirs(['models', 'model', 'modules'])
|
|
345
|
-
.filter((filePath) => isModuleFeatureFile(filePath, '.model'))
|
|
346
|
-
const imports: string[] = []
|
|
347
|
-
const entries: string[] = []
|
|
429
|
+
const files = getFilesFromDirs(['models', 'model', 'modules'])
|
|
430
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.model'))
|
|
431
|
+
const imports: string[] = []
|
|
432
|
+
const entries: string[] = []
|
|
433
|
+
const exportNames = new Set<string>()
|
|
348
434
|
|
|
349
435
|
files.forEach((filePath) => {
|
|
350
436
|
const source = readFileSync(filePath, 'utf8')
|
|
@@ -362,25 +448,31 @@ function generateModelsRegistry() {
|
|
|
362
448
|
filePath,
|
|
363
449
|
['.model'],
|
|
364
450
|
))
|
|
365
|
-
|
|
366
|
-
imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
367
|
-
|
|
451
|
+
|
|
452
|
+
imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
453
|
+
exportedModels.forEach((name) => exportNames.add(name))
|
|
454
|
+
entries.push(...exportedModels.map((name) => {
|
|
368
455
|
const key = exportedModels.length === 1 && modelKey
|
|
369
456
|
? modelKey
|
|
370
457
|
: name === `${fileModelName}Model`
|
|
371
458
|
? fileModelName
|
|
372
459
|
: name
|
|
373
460
|
|
|
374
|
-
return
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
461
|
+
return key === name
|
|
462
|
+
? ` ${name},`
|
|
463
|
+
: ` ${key}: ${name},`
|
|
464
|
+
}))
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
468
|
+
|
|
469
|
+
const content = `${generatedHeader}
|
|
470
|
+
${imports.join('\n')}
|
|
471
|
+
|
|
472
|
+
${exportBlock}
|
|
473
|
+
export const models = {
|
|
474
|
+
${entries.join('\n')}
|
|
475
|
+
}
|
|
384
476
|
|
|
385
477
|
export type Models = typeof models
|
|
386
478
|
`
|
|
@@ -388,21 +480,30 @@ export type Models = typeof models
|
|
|
388
480
|
writeGeneratedFile('models.generated.ts', content)
|
|
389
481
|
}
|
|
390
482
|
|
|
391
|
-
function generateServicesRegistry() {
|
|
392
|
-
const files =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
483
|
+
function generateServicesRegistry() {
|
|
484
|
+
const files = [
|
|
485
|
+
...getFilesFromDirs(['services', 'service', 'modules'])
|
|
486
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.service'))
|
|
487
|
+
.map((filePath) => ({ filePath })),
|
|
488
|
+
...getEnabledPluginFiles('.service'),
|
|
489
|
+
]
|
|
490
|
+
const imports: string[] = []
|
|
491
|
+
const entries: string[] = []
|
|
492
|
+
const exportNames = new Set<string>()
|
|
493
|
+
|
|
494
|
+
files.forEach((item) => {
|
|
495
|
+
const { filePath } = item
|
|
496
|
+
const source = readFileSync(filePath, 'utf8')
|
|
497
|
+
const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
|
|
498
|
+
.map((match) => match[1])
|
|
499
|
+
.sort((left, right) => left.localeCompare(right))
|
|
500
|
+
const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
|
|
501
|
+
const fallbackServicePath = 'name' in item
|
|
502
|
+
? toPluginModulePath(item.name, item.rootDir, filePath, ['.service'])
|
|
503
|
+
: toModulePath('services', filePath)
|
|
504
|
+
const defaultServiceName = defaultServiceClass
|
|
505
|
+
? defaultServiceClass[1] ?? `${toPascalCase(fallbackServicePath)}Service`
|
|
506
|
+
: ''
|
|
406
507
|
const defaultImportName = defaultServiceName
|
|
407
508
|
? toIdentifier([defaultServiceName, 'Default'])
|
|
408
509
|
: ''
|
|
@@ -411,31 +512,89 @@ function generateServicesRegistry() {
|
|
|
411
512
|
return
|
|
412
513
|
}
|
|
413
514
|
|
|
414
|
-
if (defaultServiceName) {
|
|
415
|
-
imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
515
|
+
if (defaultServiceName) {
|
|
516
|
+
imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
|
|
517
|
+
exportNames.add(`${defaultImportName} as ${defaultServiceName}`)
|
|
518
|
+
entries.push(` ${toServiceKey(defaultServiceName)}: new ${defaultImportName}(deps),`)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (namedServiceClasses.length > 0) {
|
|
522
|
+
imports.push(`import { ${namedServiceClasses.join(', ')} } from '${toImportPath(filePath)}'`)
|
|
523
|
+
namedServiceClasses.forEach((name) => exportNames.add(name))
|
|
524
|
+
entries.push(...namedServiceClasses.map((name) => ` ${toServiceKey(name)}: new ${name}(deps),`))
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const exportBlock = formatExportBlock(Array.from(exportNames).sort((left, right) => left.localeCompare(right)))
|
|
529
|
+
const content = `${generatedHeader}
|
|
530
|
+
${imports.join('\n')}
|
|
531
|
+
|
|
532
|
+
${exportBlock}
|
|
533
|
+
export function createServices(deps: any) {
|
|
534
|
+
return {
|
|
535
|
+
${entries.join('\n')}
|
|
431
536
|
}
|
|
432
537
|
}
|
|
433
538
|
|
|
434
539
|
export type Services = ReturnType<typeof createServices>
|
|
435
540
|
`
|
|
436
541
|
|
|
437
|
-
writeGeneratedFile('services.generated.ts', content)
|
|
438
|
-
}
|
|
542
|
+
writeGeneratedFile('services.generated.ts', content)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function generateCronsRegistry() {
|
|
546
|
+
const files = [
|
|
547
|
+
...getFilesFromDirs(['cron', 'crons'])
|
|
548
|
+
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
549
|
+
.map((filePath) => ({
|
|
550
|
+
modulePath: toModulePath(filePath.includes(`${srcDir}/crons/`) ? 'crons' : 'cron', filePath, ['.cron']),
|
|
551
|
+
filePath,
|
|
552
|
+
})),
|
|
553
|
+
...getFilesFromDirs(['modules'])
|
|
554
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.cron'))
|
|
555
|
+
.map((filePath) => ({
|
|
556
|
+
modulePath: toModulePath('modules', filePath, ['.cron']),
|
|
557
|
+
filePath,
|
|
558
|
+
})),
|
|
559
|
+
...getEnabledPluginFiles('.cron')
|
|
560
|
+
.map(({ name, rootDir, filePath }) => ({
|
|
561
|
+
modulePath: toPluginModulePath(name, rootDir, filePath, ['.cron']),
|
|
562
|
+
filePath,
|
|
563
|
+
})),
|
|
564
|
+
]
|
|
565
|
+
.filter(({ filePath }) => hasCronExport(filePath))
|
|
566
|
+
.filter((item, index, list) => {
|
|
567
|
+
return list.findIndex((row) => row.filePath === item.filePath) === index
|
|
568
|
+
})
|
|
569
|
+
.sort((left, right) => left.modulePath.localeCompare(right.modulePath))
|
|
570
|
+
|
|
571
|
+
const imports = files.map((item) => {
|
|
572
|
+
return `import * as ${toLowerCamelIdentifier([item.modulePath], 'Cron')} from '${toImportPath(item.filePath)}'`
|
|
573
|
+
})
|
|
574
|
+
const exportNames = files.map((item) => toLowerCamelIdentifier([item.modulePath], 'Cron'))
|
|
575
|
+
const entries = files.map((item) => {
|
|
576
|
+
const importName = toLowerCamelIdentifier([item.modulePath], 'Cron')
|
|
577
|
+
|
|
578
|
+
return ` { name: ${JSON.stringify(item.modulePath)}, module: ${importName} },`
|
|
579
|
+
})
|
|
580
|
+
const exportBlock = formatExportBlock(exportNames)
|
|
581
|
+
const content = createGeneratedContent([
|
|
582
|
+
imports.join('\n'),
|
|
583
|
+
exportBlock,
|
|
584
|
+
`export const cronModules = [
|
|
585
|
+
${entries.join('\n')}
|
|
586
|
+
] as const`,
|
|
587
|
+
])
|
|
588
|
+
|
|
589
|
+
writeGeneratedFile('crons.generated.ts', content)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function hasCronExport(filePath: string) {
|
|
593
|
+
const source = readFileSync(filePath, 'utf8')
|
|
594
|
+
|
|
595
|
+
return /\bexport\s+default\b/.test(source)
|
|
596
|
+
|| /\bexport\s+const\s+(?:cron|jobs)\b/.test(source)
|
|
597
|
+
}
|
|
439
598
|
|
|
440
599
|
function findBalancedObject(source: string, startIndex: number) {
|
|
441
600
|
let depth = 0
|
|
@@ -560,7 +719,7 @@ function normalizePluginPackage(packageName: string, configFile: string) {
|
|
|
560
719
|
return normalized
|
|
561
720
|
}
|
|
562
721
|
|
|
563
|
-
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
722
|
+
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
564
723
|
const merged = new Map<string, Exclude<PluginConfigItem, false>>()
|
|
565
724
|
|
|
566
725
|
for (const { filePath, config } of configs) {
|
|
@@ -587,46 +746,104 @@ function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]
|
|
|
587
746
|
|
|
588
747
|
return item.enable === true && typeof item.package === 'string' && item.package.length > 0
|
|
589
748
|
})
|
|
590
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
591
|
-
}
|
|
749
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function getEnabledPluginConfigItems() {
|
|
753
|
+
return mergePluginConfig([
|
|
754
|
+
{ filePath: join(packageDir, 'plugins/config.ts'), config: readPluginConfig(join(packageDir, 'plugins/config.ts')) },
|
|
755
|
+
{ filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
|
|
756
|
+
])
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function resolvePluginPackageDir(packageName: string) {
|
|
760
|
+
const resolved = resolvePluginPackage(packageName)
|
|
761
|
+
|
|
762
|
+
return statSync(resolved).isDirectory()
|
|
763
|
+
? resolved
|
|
764
|
+
: dirname(resolved)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function resolvePluginPackage(packageName: string) {
|
|
768
|
+
if (isAbsolute(packageName)) {
|
|
769
|
+
return packageName
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (packageName.startsWith('./') || packageName.startsWith('../')) {
|
|
773
|
+
return resolve(projectDir, packageName)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (packageName.startsWith('jax-hono/')) {
|
|
777
|
+
const packagePath = packageName.slice('jax-hono/'.length)
|
|
778
|
+
const directPath = join(packageDir, packagePath)
|
|
779
|
+
|
|
780
|
+
if (existsSync(directPath)) {
|
|
781
|
+
return directPath
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const indexPath = join(directPath, 'index.ts')
|
|
785
|
+
|
|
786
|
+
if (existsSync(indexPath)) {
|
|
787
|
+
return indexPath
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return projectRequire.resolve(packageName)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function getEnabledPlugins(): EnabledPlugin[] {
|
|
795
|
+
return getEnabledPluginConfigItems()
|
|
796
|
+
.map(([name, item]) => {
|
|
797
|
+
const rootDir = resolvePluginPackageDir(item.package)
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
name,
|
|
801
|
+
packageName: item.package,
|
|
802
|
+
rootDir,
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
.filter((plugin) => existsSync(plugin.rootDir))
|
|
806
|
+
}
|
|
592
807
|
|
|
593
808
|
function serializePluginOptions(options: unknown) {
|
|
594
809
|
return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
|
|
595
810
|
}
|
|
596
811
|
|
|
597
|
-
function generatePluginContextRegistry() {
|
|
598
|
-
const enabledPlugins =
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
812
|
+
function generatePluginContextRegistry() {
|
|
813
|
+
const enabledPlugins = getEnabledPluginConfigItems()
|
|
814
|
+
const imports = enabledPlugins.map(([name, item]) => {
|
|
815
|
+
return `import ${toLowerCamelIdentifier([name], 'Plugin')} from ${JSON.stringify(item.package)}`
|
|
816
|
+
})
|
|
817
|
+
const exportNames = enabledPlugins.map(([name]) => toLowerCamelIdentifier([name], 'Plugin'))
|
|
818
|
+
const moduleEntries = enabledPlugins.map(
|
|
819
|
+
([name, item]) =>
|
|
820
|
+
` { name: ${JSON.stringify(name)}, module: ${toLowerCamelIdentifier([name], 'Plugin')}${serializePluginOptions(item.options)} },`,
|
|
821
|
+
)
|
|
822
|
+
const exportBlock = formatExportBlock(exportNames)
|
|
823
|
+
|
|
824
|
+
const content = createGeneratedContent([
|
|
825
|
+
imports.join('\n'),
|
|
826
|
+
exportBlock,
|
|
827
|
+
`export const plugins = {} as Record<string, unknown>
|
|
828
|
+
|
|
829
|
+
export type Plugins = typeof plugins
|
|
830
|
+
|
|
831
|
+
export const pluginModules = [
|
|
832
|
+
${moduleEntries.join('\n')}
|
|
833
|
+
] as const`,
|
|
834
|
+
])
|
|
835
|
+
|
|
836
|
+
writeGeneratedFile('plugins.generated.ts', content)
|
|
837
|
+
}
|
|
622
838
|
|
|
623
|
-
export function generateRegistry() {
|
|
624
|
-
generateControllersRegistry()
|
|
625
|
-
generateMiddlewaresRegistry()
|
|
626
|
-
generateModelsRegistry()
|
|
627
|
-
generateServicesRegistry()
|
|
628
|
-
|
|
629
|
-
|
|
839
|
+
export function generateRegistry() {
|
|
840
|
+
generateControllersRegistry()
|
|
841
|
+
generateMiddlewaresRegistry()
|
|
842
|
+
generateModelsRegistry()
|
|
843
|
+
generateServicesRegistry()
|
|
844
|
+
generateCronsRegistry()
|
|
845
|
+
generatePluginContextRegistry()
|
|
846
|
+
}
|
|
630
847
|
|
|
631
848
|
if (import.meta.main) {
|
|
632
849
|
generateRegistry()
|
package/core/app.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { createHono, createApi } from './hono';
|
|
|
2
2
|
import { registerCrudRoutes } from '../helpers/crud';
|
|
3
3
|
import config from '../config';
|
|
4
4
|
import type { Config } from '../config';
|
|
5
|
-
import { pluginModules } from '../generated/plugins.generated';
|
|
6
|
-
import
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
5
|
+
import { pluginModules } from '../generated/plugins.generated';
|
|
6
|
+
import { cronModules } from '../generated/crons.generated';
|
|
7
|
+
import type { models as generatedModels } from '../generated/models.generated';
|
|
8
|
+
import type { createServices as generatedCreateServices } from '../generated/services.generated';
|
|
9
|
+
import { runWithJaxContext } from './controller';
|
|
10
|
+
import { startCronJobs, stopCronJobs } from './cron';
|
|
9
11
|
import { requestLog } from '../middleware/request-log';
|
|
10
12
|
import type { Hono } from 'hono';
|
|
11
13
|
import type { JaxHono } from './hono';
|
|
@@ -65,7 +67,8 @@ export function createApp(deps: AppRuntimeDeps): JaxHono {
|
|
|
65
67
|
service: undefined as unknown as Services,
|
|
66
68
|
plugin: {} as Plugins
|
|
67
69
|
};
|
|
68
|
-
const activePlugins: ActivePlugin[] = [];
|
|
70
|
+
const activePlugins: ActivePlugin[] = [];
|
|
71
|
+
let activeCrons: ReturnType<typeof startCronJobs> = [];
|
|
69
72
|
|
|
70
73
|
ctx.service = deps.createServices(ctx);
|
|
71
74
|
app.jax = ctx;
|
|
@@ -91,16 +94,19 @@ export function createApp(deps: AppRuntimeDeps): JaxHono {
|
|
|
91
94
|
).then(async (plugins) => {
|
|
92
95
|
activePlugins.push(...plugins.filter((plugin): plugin is ActivePlugin => Boolean(plugin)));
|
|
93
96
|
|
|
94
|
-
printEnabledPlugins(activePlugins);
|
|
95
|
-
|
|
96
|
-
await Promise.all(activePlugins.map((plugin) => runPluginReadySafely(plugin, ctx)));
|
|
97
|
-
|
|
97
|
+
printEnabledPlugins(activePlugins);
|
|
98
|
+
|
|
99
|
+
await Promise.all(activePlugins.map((plugin) => runPluginReadySafely(plugin, ctx)));
|
|
100
|
+
|
|
101
|
+
activeCrons = startCronJobs(cronModules, ctx);
|
|
102
|
+
});
|
|
98
103
|
|
|
99
104
|
app.ready = ready;
|
|
100
105
|
app.close = async () => {
|
|
101
|
-
await ready.catch(() => undefined);
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
await ready.catch(() => undefined);
|
|
107
|
+
stopCronJobs(activeCrons);
|
|
108
|
+
await closePluginsSafely(activePlugins, ctx);
|
|
109
|
+
};
|
|
104
110
|
|
|
105
111
|
return app;
|
|
106
112
|
}
|
package/core/cron.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { AppContext } from './app';
|
|
2
|
+
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
|
|
5
|
+
export type CronRunContext = AppContext & {
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CronSchedule = {
|
|
10
|
+
/**
|
|
11
|
+
* Cron expression supported by Bun.cron.
|
|
12
|
+
*/
|
|
13
|
+
cron: string;
|
|
14
|
+
/**
|
|
15
|
+
* Run once immediately after the app is ready.
|
|
16
|
+
*/
|
|
17
|
+
immediate?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CronJob = CronSchedule & {
|
|
21
|
+
name?: string;
|
|
22
|
+
run: (ctx: CronRunContext) => MaybePromise<void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type CronModule =
|
|
26
|
+
| CronJob
|
|
27
|
+
| CronJob[]
|
|
28
|
+
| {
|
|
29
|
+
default?: CronJob | CronJob[];
|
|
30
|
+
cron?: CronJob | CronJob[];
|
|
31
|
+
jobs?: CronJob[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type CronModuleItem = {
|
|
35
|
+
name: string;
|
|
36
|
+
module: CronModule;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ActiveCron = {
|
|
40
|
+
name: string;
|
|
41
|
+
cron: string;
|
|
42
|
+
nextRunAt?: Date;
|
|
43
|
+
stop: () => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function defineCron(job: CronJob) {
|
|
47
|
+
return job;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function startCronJobs(items: readonly CronModuleItem[], ctx: AppContext) {
|
|
51
|
+
const active: ActiveCron[] = [];
|
|
52
|
+
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
for (const job of resolveCronJobs(item.module)) {
|
|
55
|
+
active.push(startCronJob(item.name, job, ctx));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
printCronJobs(active);
|
|
60
|
+
|
|
61
|
+
return active;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function stopCronJobs(items: readonly ActiveCron[]) {
|
|
65
|
+
for (const item of [...items].reverse()) {
|
|
66
|
+
item.stop();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveCronJobs(module: CronModule): CronJob[] {
|
|
71
|
+
if (Array.isArray(module)) {
|
|
72
|
+
return module;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isCronJob(module)) {
|
|
76
|
+
return [module];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const jobs = module.default ?? module.cron ?? module.jobs ?? [];
|
|
80
|
+
|
|
81
|
+
return Array.isArray(jobs) ? jobs : [jobs];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isCronJob(value: unknown): value is CronJob {
|
|
85
|
+
return Boolean(value && typeof value === 'object' && typeof (value as CronJob).run === 'function');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function startCronJob(moduleName: string, job: CronJob, appContext: AppContext): ActiveCron {
|
|
89
|
+
const name = job.name || moduleName;
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
let running = false;
|
|
92
|
+
|
|
93
|
+
const run = async () => {
|
|
94
|
+
if (running || controller.signal.aborted) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
running = true;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await job.run({ ...appContext, signal: controller.signal });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`[cron] ${name} failed:`, error);
|
|
104
|
+
} finally {
|
|
105
|
+
running = false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const cronJob = Bun.cron(job.cron, () => {
|
|
109
|
+
void run();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (job.immediate) {
|
|
113
|
+
void run();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
cron: job.cron,
|
|
119
|
+
nextRunAt: getNextRunAt(job.cron),
|
|
120
|
+
stop() {
|
|
121
|
+
controller.abort();
|
|
122
|
+
cronJob.stop();
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getNextRunAt(expression: string) {
|
|
128
|
+
try {
|
|
129
|
+
return Bun.cron.parse(expression) ?? undefined;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`[cron] invalid expression ${expression}:`, error);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printCronJobs(items: readonly ActiveCron[]) {
|
|
137
|
+
if (items.length === 0) {
|
|
138
|
+
console.log('[cron] enabled: none');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('[cron] enabled:');
|
|
143
|
+
|
|
144
|
+
for (const item of items) {
|
|
145
|
+
const nextRunAt = item.nextRunAt
|
|
146
|
+
? `${item.nextRunAt.toLocaleString()} (${item.nextRunAt.toISOString()})`
|
|
147
|
+
: 'unknown';
|
|
148
|
+
|
|
149
|
+
console.log(` - ${item.name} (${item.cron}) next: ${nextRunAt}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
package/docs/agent.md
CHANGED
|
@@ -48,6 +48,51 @@ generated/plugins.generated.ts
|
|
|
48
48
|
|
|
49
49
|
如果业务仓库里有多个工程都依赖 `jax-hono`,分别查看各工程的 `package.json`,运行它们自己的 setup/build 命令。
|
|
50
50
|
|
|
51
|
+
## 插件生成约定
|
|
52
|
+
|
|
53
|
+
生成器只扫描已启用插件。插件启用来源包括:
|
|
54
|
+
|
|
55
|
+
- `jax-hono/plugins/config.ts`
|
|
56
|
+
- 使用方工程的 `src/config/plugin.ts`
|
|
57
|
+
|
|
58
|
+
未启用插件不应进入生成结果,避免构建解析未启用插件的运行时依赖。
|
|
59
|
+
|
|
60
|
+
已启用插件中的约定文件会进入生成器:
|
|
61
|
+
|
|
62
|
+
- `*.controller.ts` 注册到 `controllers` 根对象
|
|
63
|
+
- `*.service.ts` 注册到 `services.generated.ts`
|
|
64
|
+
- `*.cron.ts` 注册到 `crons.generated.ts`,由框架统一启动/停止
|
|
65
|
+
|
|
66
|
+
例如启用插件 `foo` 后:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
plugins/foo/foo.controller.ts
|
|
70
|
+
plugins/foo/foo-cache.service.ts
|
|
71
|
+
plugins/foo/foo-cleanup.cron.ts
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
可生成:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
controllers.foo
|
|
78
|
+
service.fooCache
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Cron 文件可以使用 `defineCron`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { defineCron } from 'jax-hono';
|
|
85
|
+
|
|
86
|
+
export default defineCron({
|
|
87
|
+
name: 'foo-cleanup',
|
|
88
|
+
cron: '0 0 * * *',
|
|
89
|
+
immediate: true,
|
|
90
|
+
async run(ctx) {
|
|
91
|
+
// ctx.app / ctx.config / ctx.model / ctx.service / ctx.plugin / ctx.signal
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
51
96
|
## Controller 注册约定
|
|
52
97
|
|
|
53
98
|
生成器需要保持不同命名空间的边界清晰:
|
package/docs/jax.md
CHANGED
|
@@ -91,9 +91,10 @@ export default definePlugin({
|
|
|
91
91
|
- `generated/config.ts`
|
|
92
92
|
- `generated/controllers.generated.ts`
|
|
93
93
|
- `generated/middlewares.generated.ts`
|
|
94
|
-
- `generated/models.generated.ts`
|
|
95
|
-
- `generated/plugins.generated.ts`
|
|
96
|
-
- `generated/services.generated.ts`
|
|
94
|
+
- `generated/models.generated.ts`
|
|
95
|
+
- `generated/plugins.generated.ts`
|
|
96
|
+
- `generated/services.generated.ts`
|
|
97
|
+
- `generated/crons.generated.ts`
|
|
97
98
|
|
|
98
99
|
业务代码不要手动编辑 `generated` 目录。
|
|
99
100
|
|
|
@@ -259,7 +260,42 @@ Service 中推荐使用:
|
|
|
259
260
|
|
|
260
261
|
新增文件后,如果业务侧需要使用,应优先在对应子入口导出,例如 `src/jax/utils/index.ts`、`src/jax/errors/index.ts`。只有框架核心 API 才放到 `src/jax/index.ts`。
|
|
261
262
|
|
|
262
|
-
内置插件配置放在 `
|
|
263
|
+
内置插件配置放在 `plugins/config.ts`,内置插件实现放在 `plugins/*`,项目级插件通过 `src/config/plugin.ts` 显式启用或覆盖。生成器只扫描已启用插件中的约定文件,不扫描未启用插件。
|
|
264
|
+
|
|
265
|
+
已启用插件可以提供:
|
|
266
|
+
|
|
267
|
+
- `*.controller.ts`:注册到 `controllers` 根对象
|
|
268
|
+
- `*.service.ts`:注册到 `services.generated.ts`
|
|
269
|
+
- `*.cron.ts`:注册到 `crons.generated.ts`,由框架统一启动/停止
|
|
270
|
+
|
|
271
|
+
未启用插件不会进入生成结果,也不会让构建解析它的运行时依赖。
|
|
272
|
+
|
|
273
|
+
### `cron`
|
|
274
|
+
|
|
275
|
+
定时任务可以放在项目根级 `src/cron` / `src/crons` 目录,也可以放在模块或已启用插件中:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
src/cron/report.cron.ts
|
|
279
|
+
src/modules/oss/oss-trash.cron.ts
|
|
280
|
+
plugins/foo/foo-cleanup.cron.ts
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Cron 文件导出 `defineCron()`、`cron` 或 `jobs` 即可被生成器识别:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { defineCron } from 'jax-hono';
|
|
287
|
+
|
|
288
|
+
export default defineCron({
|
|
289
|
+
name: 'daily-cleanup',
|
|
290
|
+
cron: '0 0 * * *',
|
|
291
|
+
immediate: true,
|
|
292
|
+
async run(ctx) {
|
|
293
|
+
// ctx.app / ctx.config / ctx.model / ctx.service / ctx.plugin / ctx.signal
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
框架会在应用 ready 后统一启动定时任务,在 `app.close()` 时统一停止,不需要每个任务在入口文件手动声明。
|
|
263
299
|
|
|
264
300
|
## 常用命令
|
|
265
301
|
|
package/index.ts
CHANGED
|
@@ -61,9 +61,11 @@ export type { AppContext, AppPlugin } from './core/app';
|
|
|
61
61
|
export type { Config, JaxConfig } from './config';
|
|
62
62
|
export { ApiController } from './core/api-controller';
|
|
63
63
|
export { Controller } from './core/controller';
|
|
64
|
-
export { Service } from './core/service';
|
|
65
|
-
export type { ServiceContext } from './core/service';
|
|
66
|
-
export
|
|
64
|
+
export { Service } from './core/service';
|
|
65
|
+
export type { ServiceContext } from './core/service';
|
|
66
|
+
export { defineCron } from './core/cron';
|
|
67
|
+
export type { CronJob, CronRunContext, CronSchedule } from './core/cron';
|
|
68
|
+
export type { Services } from './generated/services.generated';
|
|
67
69
|
export type { ApiFailResponse, ApiResponse, ApiSuccessResponse } from './middleware/api-response';
|
|
68
70
|
|
|
69
71
|
export function createApp() {
|
package/package.json
CHANGED
package/utils/crypto.ts
CHANGED
|
@@ -4,6 +4,12 @@ export function md5(value: string) {
|
|
|
4
4
|
return createHash('md5').update(value).digest('hex');
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function createToken(byteLength = 24) {
|
|
8
|
-
return randomBytes(byteLength).toString('hex');
|
|
9
|
-
}
|
|
7
|
+
export function createToken(byteLength = 24) {
|
|
8
|
+
return randomBytes(byteLength).toString('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function nonceStr(length: number) {
|
|
12
|
+
return randomBytes(Math.ceil(length / 2))
|
|
13
|
+
.toString('hex')
|
|
14
|
+
.slice(0, length);
|
|
15
|
+
}
|
package/utils/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from "./array";
|
|
2
2
|
export * from "./async";
|
|
3
|
-
export * from "./crypto";
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
3
|
+
export * from "./crypto";
|
|
4
|
+
export * from "./order";
|
|
5
|
+
export * from "./regexp";
|
|
6
|
+
export * from "./transform";
|
|
7
|
+
export * from "./validate";
|
package/utils/order.ts
ADDED
package/utils/regexp.ts
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
* escapeRegex('price$') // => 'price\\$'
|
|
13
13
|
* escapeRegex(123) // => '123'
|
|
14
14
|
*/
|
|
15
|
-
export function escapeRegex(value: unknown) {
|
|
16
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
-
}
|
|
15
|
+
export function escapeRegex(value: unknown) {
|
|
16
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function keywordRegex(keyword?: string) {
|
|
20
|
+
if (!keyword) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return new RegExp(escapeRegex(keyword), 'i');
|
|
25
|
+
}
|