jax-hono 1.0.14 → 1.0.16
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 +3 -1
- package/build/generate-registry.ts +259 -96
- package/core/app.ts +18 -12
- package/core/cron.ts +151 -0
- package/docs/agent.md +150 -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
package/README.md
CHANGED
|
@@ -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,14 +25,20 @@ 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' },
|
|
30
38
|
{ name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
|
|
31
39
|
{ name: 'open', dirs: ['open', 'controllers/open', 'controller/open', 'modules'], suffix: '.open' },
|
|
32
40
|
{ name: 'client', dirs: ['client', 'controllers/client', 'controller/client', 'modules'], suffix: '.client' },
|
|
33
|
-
{ name: 'controller', dirs: ['controllers', 'controller'] },
|
|
41
|
+
{ name: 'controller', dirs: ['controllers', 'controller', 'modules'], suffix: '.controller' },
|
|
34
42
|
]
|
|
35
43
|
|
|
36
44
|
const scopedControllerGroups = new Set(
|
|
@@ -126,18 +134,28 @@ function stripKnownSuffix(modulePath: string, suffixes: string[]) {
|
|
|
126
134
|
return modulePath
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
function normalizeModulePath(modulePath: string) {
|
|
130
|
-
const parts = modulePath.split('/')
|
|
131
|
-
const last = parts.at(-1)
|
|
132
|
-
const parent = parts.at(-2)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
function normalizeModulePath(modulePath: string) {
|
|
138
|
+
const parts = modulePath.split('/')
|
|
139
|
+
const last = parts.at(-1)
|
|
140
|
+
const parent = parts.at(-2)
|
|
141
|
+
|
|
142
|
+
if (!last || !parent) {
|
|
143
|
+
return modulePath
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (last === parent) {
|
|
147
|
+
return parts.slice(0, -1).join('/')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (last.startsWith(`${parent}-`) || last.startsWith(`${parent}_`)) {
|
|
151
|
+
return [...parts.slice(0, -2), last].join('/')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return modulePath
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
|
|
158
|
+
const modulePath = relative(join(srcDir, baseDir), filePath)
|
|
141
159
|
.replace(parse(filePath).ext, '')
|
|
142
160
|
.replaceAll('\\', '/')
|
|
143
161
|
const withoutIndex = modulePath.endsWith('/index')
|
|
@@ -146,16 +164,36 @@ function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []
|
|
|
146
164
|
|
|
147
165
|
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
148
166
|
|
|
149
|
-
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
150
|
-
}
|
|
167
|
+
return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function toPluginModulePath(pluginName: string, rootDir: string, filePath: string, suffixes: string[] = []) {
|
|
171
|
+
const modulePath = relative(rootDir, filePath)
|
|
172
|
+
.replace(parse(filePath).ext, '')
|
|
173
|
+
.replaceAll('\\', '/')
|
|
174
|
+
const withoutIndex = modulePath.endsWith('/index')
|
|
175
|
+
? modulePath.slice(0, -'/index'.length)
|
|
176
|
+
: modulePath
|
|
177
|
+
const stripped = stripKnownSuffix(withoutIndex, suffixes)
|
|
178
|
+
|
|
179
|
+
return normalizeModulePath(stripped ? `${pluginName}/${stripped}` : pluginName)
|
|
180
|
+
}
|
|
151
181
|
|
|
152
182
|
function uniqueSorted(values: string[]) {
|
|
153
183
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right))
|
|
154
184
|
}
|
|
155
185
|
|
|
156
|
-
function getFilesFromDirs(dirs: string[]) {
|
|
157
|
-
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
158
|
-
}
|
|
186
|
+
function getFilesFromDirs(dirs: string[]) {
|
|
187
|
+
return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getEnabledPluginFiles(suffix: string) {
|
|
191
|
+
return getEnabledPlugins().flatMap((plugin) => {
|
|
192
|
+
return walkFiles(plugin.rootDir)
|
|
193
|
+
.filter((filePath) => isSuffixedModuleFile(filePath, suffix))
|
|
194
|
+
.map((filePath) => ({ ...plugin, filePath }))
|
|
195
|
+
})
|
|
196
|
+
}
|
|
159
197
|
|
|
160
198
|
function isFileInDir(filePath: string, dirName: string) {
|
|
161
199
|
const rel = relative(join(srcDir, dirName), filePath)
|
|
@@ -170,13 +208,13 @@ function isSuffixedModuleFile(filePath: string, suffix: string) {
|
|
|
170
208
|
.endsWith(suffix)
|
|
171
209
|
}
|
|
172
210
|
|
|
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
|
-
}
|
|
211
|
+
function isModuleFeatureFile(filePath: string, suffix: string) {
|
|
212
|
+
return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasDefaultExport(filePath: string) {
|
|
216
|
+
return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
|
|
217
|
+
}
|
|
180
218
|
|
|
181
219
|
function indentObject(content: string, spaces = 2) {
|
|
182
220
|
if (!content) {
|
|
@@ -221,27 +259,39 @@ function buildNestedObject(entries: { path: string[]; value: string }[]): string
|
|
|
221
259
|
}
|
|
222
260
|
|
|
223
261
|
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
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
const used = new Set<string>()
|
|
263
|
+
const imports: string[] = []
|
|
264
|
+
const groupLines: string[] = []
|
|
265
|
+
|
|
266
|
+
for (const group of controllerGroups) {
|
|
267
|
+
const projectEntries = getFilesFromDirs(group.dirs)
|
|
268
|
+
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
269
|
+
.filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
|
|
270
|
+
.filter(hasDefaultExport)
|
|
271
|
+
.map((filePath, index) => {
|
|
272
|
+
const baseDir = group.dirs.find((dirName) => {
|
|
273
|
+
return isFileInDir(filePath, dirName)
|
|
274
|
+
}) ?? group.dirs[0]
|
|
275
|
+
const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
|
|
276
|
+
const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
|
|
277
|
+
|
|
278
|
+
return { modulePath, importName, filePath }
|
|
279
|
+
})
|
|
280
|
+
const pluginEntries = group.name === 'controller'
|
|
281
|
+
? getEnabledPluginFiles('.controller')
|
|
282
|
+
.filter(({ filePath }) => basename(filePath) !== 'index.ts')
|
|
283
|
+
.filter(({ filePath }) => hasDefaultExport(filePath))
|
|
284
|
+
.map(({ name, rootDir, filePath }, index) => {
|
|
285
|
+
const modulePath = toPluginModulePath(name, rootDir, filePath, ['.controller'])
|
|
286
|
+
const importName = toIdentifier([group.name, 'plugin', modulePath, String(index), 'Controller'])
|
|
287
|
+
|
|
288
|
+
return { modulePath, importName, filePath }
|
|
289
|
+
})
|
|
290
|
+
: []
|
|
291
|
+
const entries = [...projectEntries, ...pluginEntries]
|
|
292
|
+
.filter((entry) => {
|
|
293
|
+
if (group.name !== 'controller') {
|
|
294
|
+
return true
|
|
245
295
|
}
|
|
246
296
|
|
|
247
297
|
const [scope] = entry.modulePath.split('/')
|
|
@@ -266,16 +316,17 @@ function generateControllersRegistry() {
|
|
|
266
316
|
continue
|
|
267
317
|
}
|
|
268
318
|
|
|
269
|
-
const objectBody =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
319
|
+
const objectBody = buildNestedObject(entries.map((entry) => ({
|
|
320
|
+
path: entry.modulePath.split('/').map(toCamelCase),
|
|
321
|
+
value: `createController(${entry.importName})`,
|
|
322
|
+
})))
|
|
323
|
+
|
|
324
|
+
if (group.name === 'controller') {
|
|
325
|
+
groupLines.push(indentObject(objectBody, 2))
|
|
326
|
+
} else {
|
|
327
|
+
groupLines.push(` ${group.name}: {\n${indentObject(objectBody, 4)}\n },`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
279
330
|
|
|
280
331
|
const helperImport = imports.length > 0
|
|
281
332
|
? `import { createController } from 'jax-hono/core/controller'\n`
|
|
@@ -374,21 +425,29 @@ export type Models = typeof models
|
|
|
374
425
|
writeGeneratedFile('models.generated.ts', content)
|
|
375
426
|
}
|
|
376
427
|
|
|
377
|
-
function generateServicesRegistry() {
|
|
378
|
-
const files =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
428
|
+
function generateServicesRegistry() {
|
|
429
|
+
const files = [
|
|
430
|
+
...getFilesFromDirs(['services', 'service', 'modules'])
|
|
431
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.service'))
|
|
432
|
+
.map((filePath) => ({ filePath })),
|
|
433
|
+
...getEnabledPluginFiles('.service'),
|
|
434
|
+
]
|
|
435
|
+
const imports: string[] = []
|
|
436
|
+
const entries: string[] = []
|
|
437
|
+
|
|
438
|
+
files.forEach((item) => {
|
|
439
|
+
const { filePath } = item
|
|
440
|
+
const source = readFileSync(filePath, 'utf8')
|
|
441
|
+
const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
|
|
442
|
+
.map((match) => match[1])
|
|
443
|
+
.sort((left, right) => left.localeCompare(right))
|
|
444
|
+
const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
|
|
445
|
+
const fallbackServicePath = 'name' in item
|
|
446
|
+
? toPluginModulePath(item.name, item.rootDir, filePath, ['.service'])
|
|
447
|
+
: toModulePath('services', filePath)
|
|
448
|
+
const defaultServiceName = defaultServiceClass
|
|
449
|
+
? defaultServiceClass[1] ?? `${toPascalCase(fallbackServicePath)}Service`
|
|
450
|
+
: ''
|
|
392
451
|
const defaultImportName = defaultServiceName
|
|
393
452
|
? toIdentifier([defaultServiceName, 'Default'])
|
|
394
453
|
: ''
|
|
@@ -420,8 +479,58 @@ ${entries.join('\n')}
|
|
|
420
479
|
export type Services = ReturnType<typeof createServices>
|
|
421
480
|
`
|
|
422
481
|
|
|
423
|
-
writeGeneratedFile('services.generated.ts', content)
|
|
424
|
-
}
|
|
482
|
+
writeGeneratedFile('services.generated.ts', content)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function generateCronsRegistry() {
|
|
486
|
+
const files = [
|
|
487
|
+
...getFilesFromDirs(['cron', 'crons'])
|
|
488
|
+
.filter((filePath) => basename(filePath) !== 'index.ts')
|
|
489
|
+
.map((filePath) => ({
|
|
490
|
+
modulePath: toModulePath(filePath.includes(`${srcDir}/crons/`) ? 'crons' : 'cron', filePath, ['.cron']),
|
|
491
|
+
filePath,
|
|
492
|
+
})),
|
|
493
|
+
...getFilesFromDirs(['modules'])
|
|
494
|
+
.filter((filePath) => isModuleFeatureFile(filePath, '.cron'))
|
|
495
|
+
.map((filePath) => ({
|
|
496
|
+
modulePath: toModulePath('modules', filePath, ['.cron']),
|
|
497
|
+
filePath,
|
|
498
|
+
})),
|
|
499
|
+
...getEnabledPluginFiles('.cron')
|
|
500
|
+
.map(({ name, rootDir, filePath }) => ({
|
|
501
|
+
modulePath: toPluginModulePath(name, rootDir, filePath, ['.cron']),
|
|
502
|
+
filePath,
|
|
503
|
+
})),
|
|
504
|
+
]
|
|
505
|
+
.filter(({ filePath }) => hasCronExport(filePath))
|
|
506
|
+
.filter((item, index, list) => {
|
|
507
|
+
return list.findIndex((row) => row.filePath === item.filePath) === index
|
|
508
|
+
})
|
|
509
|
+
.sort((left, right) => left.modulePath.localeCompare(right.modulePath))
|
|
510
|
+
|
|
511
|
+
const imports = files.map((item, index) => {
|
|
512
|
+
return `import * as ${toIdentifier(['cron', item.modulePath, String(index)])} from '${toImportPath(item.filePath)}'`
|
|
513
|
+
})
|
|
514
|
+
const entries = files.map((item, index) => {
|
|
515
|
+
const importName = toIdentifier(['cron', item.modulePath, String(index)])
|
|
516
|
+
|
|
517
|
+
return ` { name: ${JSON.stringify(item.modulePath)}, module: ${importName} },`
|
|
518
|
+
})
|
|
519
|
+
const content = `${generatedHeader}${imports.length > 0 ? `${imports.join('\n')}\n\n` : ''}
|
|
520
|
+
export const cronModules = [
|
|
521
|
+
${entries.join('\n')}
|
|
522
|
+
] as const
|
|
523
|
+
`
|
|
524
|
+
|
|
525
|
+
writeGeneratedFile('crons.generated.ts', content)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function hasCronExport(filePath: string) {
|
|
529
|
+
const source = readFileSync(filePath, 'utf8')
|
|
530
|
+
|
|
531
|
+
return /\bexport\s+default\b/.test(source)
|
|
532
|
+
|| /\bexport\s+const\s+(?:cron|jobs)\b/.test(source)
|
|
533
|
+
}
|
|
425
534
|
|
|
426
535
|
function findBalancedObject(source: string, startIndex: number) {
|
|
427
536
|
let depth = 0
|
|
@@ -546,7 +655,7 @@ function normalizePluginPackage(packageName: string, configFile: string) {
|
|
|
546
655
|
return normalized
|
|
547
656
|
}
|
|
548
657
|
|
|
549
|
-
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
658
|
+
function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
|
|
550
659
|
const merged = new Map<string, Exclude<PluginConfigItem, false>>()
|
|
551
660
|
|
|
552
661
|
for (const { filePath, config } of configs) {
|
|
@@ -573,18 +682,71 @@ function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]
|
|
|
573
682
|
|
|
574
683
|
return item.enable === true && typeof item.package === 'string' && item.package.length > 0
|
|
575
684
|
})
|
|
576
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
577
|
-
}
|
|
685
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function getEnabledPluginConfigItems() {
|
|
689
|
+
return mergePluginConfig([
|
|
690
|
+
{ filePath: join(packageDir, 'plugins/config.ts'), config: readPluginConfig(join(packageDir, 'plugins/config.ts')) },
|
|
691
|
+
{ filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
|
|
692
|
+
])
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function resolvePluginPackageDir(packageName: string) {
|
|
696
|
+
const resolved = resolvePluginPackage(packageName)
|
|
697
|
+
|
|
698
|
+
return statSync(resolved).isDirectory()
|
|
699
|
+
? resolved
|
|
700
|
+
: dirname(resolved)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function resolvePluginPackage(packageName: string) {
|
|
704
|
+
if (isAbsolute(packageName)) {
|
|
705
|
+
return packageName
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (packageName.startsWith('./') || packageName.startsWith('../')) {
|
|
709
|
+
return resolve(projectDir, packageName)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (packageName.startsWith('jax-hono/')) {
|
|
713
|
+
const packagePath = packageName.slice('jax-hono/'.length)
|
|
714
|
+
const directPath = join(packageDir, packagePath)
|
|
715
|
+
|
|
716
|
+
if (existsSync(directPath)) {
|
|
717
|
+
return directPath
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const indexPath = join(directPath, 'index.ts')
|
|
721
|
+
|
|
722
|
+
if (existsSync(indexPath)) {
|
|
723
|
+
return indexPath
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return projectRequire.resolve(packageName)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function getEnabledPlugins(): EnabledPlugin[] {
|
|
731
|
+
return getEnabledPluginConfigItems()
|
|
732
|
+
.map(([name, item]) => {
|
|
733
|
+
const rootDir = resolvePluginPackageDir(item.package)
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
name,
|
|
737
|
+
packageName: item.package,
|
|
738
|
+
rootDir,
|
|
739
|
+
}
|
|
740
|
+
})
|
|
741
|
+
.filter((plugin) => existsSync(plugin.rootDir))
|
|
742
|
+
}
|
|
578
743
|
|
|
579
744
|
function serializePluginOptions(options: unknown) {
|
|
580
745
|
return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
|
|
581
746
|
}
|
|
582
747
|
|
|
583
|
-
function generatePluginContextRegistry() {
|
|
584
|
-
const enabledPlugins =
|
|
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
|
-
])
|
|
748
|
+
function generatePluginContextRegistry() {
|
|
749
|
+
const enabledPlugins = getEnabledPluginConfigItems()
|
|
588
750
|
const imports = enabledPlugins.map(([name, item]) => {
|
|
589
751
|
return `import ${toIdentifier(['plugin', name])} from ${JSON.stringify(item.package)}`
|
|
590
752
|
})
|
|
@@ -606,13 +768,14 @@ ${moduleEntries.join('\n')}
|
|
|
606
768
|
writeGeneratedFile('plugins.generated.ts', content)
|
|
607
769
|
}
|
|
608
770
|
|
|
609
|
-
export function generateRegistry() {
|
|
610
|
-
generateControllersRegistry()
|
|
611
|
-
generateMiddlewaresRegistry()
|
|
612
|
-
generateModelsRegistry()
|
|
613
|
-
generateServicesRegistry()
|
|
614
|
-
|
|
615
|
-
|
|
771
|
+
export function generateRegistry() {
|
|
772
|
+
generateControllersRegistry()
|
|
773
|
+
generateMiddlewaresRegistry()
|
|
774
|
+
generateModelsRegistry()
|
|
775
|
+
generateServicesRegistry()
|
|
776
|
+
generateCronsRegistry()
|
|
777
|
+
generatePluginContextRegistry()
|
|
778
|
+
}
|
|
616
779
|
|
|
617
780
|
if (import.meta.main) {
|
|
618
781
|
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
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Agent 维护说明
|
|
2
|
+
|
|
3
|
+
这份文档给 Codex、Claude 或其他代码代理阅读,用来说明维护 `jax-hono` 时的默认规则。
|
|
4
|
+
|
|
5
|
+
## 本地 link 规则
|
|
6
|
+
|
|
7
|
+
`jax-hono` 是用户自己维护的框架,常通过 `jax link` 链接到业务工程的 `node_modules/jax-hono`。
|
|
8
|
+
|
|
9
|
+
如果在业务工程中需要修改 `jax-hono` 行为,先检查依赖是否已经 link 到本地源码目录。不要把它当成一次性的第三方 `node_modules` 临时补丁。
|
|
10
|
+
|
|
11
|
+
在 Windows PowerShell 中,可以在业务仓库根目录查找所有 `jax-hono` 依赖:
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
Get-ChildItem -Path . -Directory -Recurse -Filter jax-hono |
|
|
15
|
+
Where-Object { $_.FullName -match '\\node_modules\\jax-hono$' } |
|
|
16
|
+
Format-List LinkType,Target,FullName
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
如果 `LinkType` 是 `Junction` 或 `SymbolicLink`,直接修改 `Target` 指向的 `jax-hono` 源码目录。
|
|
20
|
+
|
|
21
|
+
如果没有 link,先执行:
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
jax link
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
然后再次确认 link 状态,再修改框架源码。
|
|
28
|
+
|
|
29
|
+
## 修改生成器后的验证
|
|
30
|
+
|
|
31
|
+
修改 `build/generate-registry.ts`、`build/generate-config.ts` 等生成逻辑后,需要在使用方工程里重新生成。
|
|
32
|
+
|
|
33
|
+
常见命令:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
bun run setup
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
然后检查业务工程中的生成结果,例如:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
generated/controllers.generated.ts
|
|
43
|
+
generated/models.generated.ts
|
|
44
|
+
generated/services.generated.ts
|
|
45
|
+
generated/middlewares.generated.ts
|
|
46
|
+
generated/plugins.generated.ts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
如果业务仓库里有多个工程都依赖 `jax-hono`,分别查看各工程的 `package.json`,运行它们自己的 setup/build 命令。
|
|
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
|
+
|
|
96
|
+
## Controller 注册约定
|
|
97
|
+
|
|
98
|
+
生成器需要保持不同命名空间的边界清晰:
|
|
99
|
+
|
|
100
|
+
- `*.backend.ts` 注册到 `controllers.backend`
|
|
101
|
+
- `*.api.ts` 注册到 `controllers.api`
|
|
102
|
+
- `*.open.ts` 注册到 `controllers.open`
|
|
103
|
+
- `*.client.ts` 注册到 `controllers.client`
|
|
104
|
+
- `*.controller.ts` 注册到 `controllers` 根对象
|
|
105
|
+
|
|
106
|
+
`.controller.ts` 表示通用 Controller,供 backend/api/open/client 等不同路由复用,不应自动归入 `controllers.backend`。
|
|
107
|
+
|
|
108
|
+
例如:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
src/modules/oss/oss.controller.ts
|
|
112
|
+
src/modules/oss/oss-bucket.controller.ts
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
应生成:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
controllers.oss
|
|
119
|
+
controllers.ossBucket
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
如果需要后台专用 Controller,使用:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
src/modules/foo/foo.backend.ts
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
应生成:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
controllers.backend.foo
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 模块路径命名
|
|
135
|
+
|
|
136
|
+
模块内文件名如果以模块目录名开头,生成 key 时可以去掉重复前缀:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
src/modules/oss/oss.controller.ts -> controllers.oss
|
|
140
|
+
src/modules/oss/oss-bucket.controller.ts -> controllers.ossBucket
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
不要生成成:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
controllers.oss.oss
|
|
147
|
+
controllers.oss.ossBucket
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
这样业务侧调用会更自然,也更适合在不同路由层复用。
|
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
|
+
}
|