bajo 2.14.1 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ class Cache {
2
+ constructor (app) {
3
+ this.app = app
4
+ }
5
+
6
+ getRootDir = () => {
7
+ const { getPluginDataDir } = this.app.bajo
8
+ return `${getPluginDataDir('bajo')}/cache`
9
+ }
10
+
11
+ prep = (name, ttlDur = 0) => {
12
+ const { breakNsPath } = this.app.bajo
13
+ const { aneka, fs } = this.app.lib
14
+ const { ns, subNs, path } = breakNsPath(name)
15
+ ttlDur = aneka.parseDuration(ttlDur)
16
+ if (ttlDur === 0 || !subNs) return
17
+ const cacheDir = `${this.getRootDir()}/${ns}/${subNs}`
18
+ const dir = `${cacheDir}/${ttlDur}`
19
+ fs.ensureDirSync(dir)
20
+ const file = `${dir}/${path}`
21
+ return { dir, file, cacheDir }
22
+ }
23
+
24
+ load = async (name, ttlDur = 0) => {
25
+ const { fs } = this.app.lib
26
+ const { dir, file } = this.prep(name, ttlDur) ?? {}
27
+ if (!file) return
28
+ if (!fs.existsSync(file)) return
29
+ const { mtimeMs } = await fs.stat(dir)
30
+ if (Date.now() - mtimeMs > ttlDur) {
31
+ await fs.remove(dir)
32
+ return
33
+ }
34
+ let content = fs.readFileSync(file, 'utf8')
35
+ try {
36
+ if (['{', '['].includes(content[0]) && ['}', ']'].includes(content[content.length - 1])) content = JSON.parse(content)
37
+ } catch (err) {}
38
+ return content
39
+ }
40
+
41
+ save = async (name, item, ttlDur = 0) => {
42
+ const { fs } = this.app.lib
43
+ const { cloneDeep, isArray, isPlainObject } = this.app.lib._
44
+ const { dir, file } = this.prep(name, ttlDur) ?? {}
45
+ if (!file || !item) return
46
+ fs.ensureDirSync(dir)
47
+ let content = cloneDeep(item)
48
+ if (isArray(item) || isPlainObject(item)) content = JSON.stringify(content)
49
+ fs.writeFileSync(file, content, 'utf8')
50
+ }
51
+
52
+ sync = async (name, item, ttlDur = 0) => {
53
+ const content = await this.loadCache(name, ttlDur)
54
+ if (!content) await this.saveCache(name, item, ttlDur)
55
+ return content
56
+ }
57
+
58
+ _purgeItem = (name) => {
59
+ if (!this.app.bajo) return
60
+ const { fs, fastGlob } = this.app.lib
61
+ try {
62
+ if (name === '*') {
63
+ const dirs = fastGlob.globSync(`${this.getRootDir()}/*`, { onlyDirectories: true })
64
+ for (const dir of dirs) {
65
+ fs.removeSync(dir)
66
+ }
67
+ } else fs.removeSync(`${this.getRootDir()}/${name}`)
68
+ } catch (err) {}
69
+ }
70
+
71
+ purge = (name) => {
72
+ if (!this.app.bajo) return
73
+ if (name) return this._purgeItem(name)
74
+ const { fastGlob, fs } = this.app.lib
75
+ const dirs = fastGlob.globSync(`${this.getRootDir()}/*/*/*`, { onlyDirectories: true })
76
+ for (const dir of dirs) {
77
+ try {
78
+ const ttlDur = Number(dir.split('/').pop())
79
+ const { mtimeMs } = fs.statSync(dir)
80
+ if (Date.now() - mtimeMs > ttlDur) fs.removeSync(dir)
81
+ } catch (err) {}
82
+ }
83
+ }
84
+
85
+ dispose = async () => {
86
+ this.app = null
87
+ }
88
+ }
89
+
90
+ export default Cache
package/class/app.js CHANGED
@@ -2,6 +2,7 @@ import util from 'util'
2
2
  import Bajo from './bajo.js'
3
3
  import Base from './base.js'
4
4
  import { runAsApplet } from './helper/bajo.js'
5
+ import Cache from './app/cache.js'
5
6
  import Tools from './plugin/tools.js'
6
7
 
7
8
  import { outmatchNs, parseObject, lib } from './helper/app.js'
@@ -196,6 +197,8 @@ class App {
196
197
  */
197
198
  this.envVars = {}
198
199
 
200
+ this.cache = new Cache(this)
201
+
199
202
  if (!options.cwd) options.cwd = process.cwd()
200
203
  const l = last(process.argv)
201
204
  if (l.startsWith('--cwd')) {
@@ -278,6 +281,9 @@ class App {
278
281
  this.applet = this.envVars._.applet ?? this.argv._.applet
279
282
  await this.bajo.runHook('bajo:beforeBoot')
280
283
  await this.bajo.init()
284
+ // cache
285
+ this.cache.purge()
286
+ setInterval(this.cache.purge, this.bajo.config.cache.purgeIntvDur)
281
287
  // boot complete
282
288
  const elapsed = new Date() - this.runAt
283
289
  this.bajo.log.debug('bootCompleted%s', secToHms(elapsed, true))
package/class/bajo.js CHANGED
@@ -341,23 +341,20 @@ class Bajo extends Plugin {
341
341
  * @returns {any}
342
342
  */
343
343
  eachPlugins = async (handler, options = {}) => {
344
- if (typeof options === 'string') options = { glob: options }
345
- const { glob, useBajo, prefix = '', noUnderscore = true, returnItems } = options
344
+ if (isString(options)) options = { glob: options }
345
+ const { glob = [], useBajo, prefix = '', noUnderscore = true, returnItems, opts = {} } = options
346
+ const globs = isString(glob) ? [glob] : [...glob]
346
347
  const pluginPkgs = useBajo ? [...cloneDeep(this.app.pluginPkgs), 'bajo'] : this.app.pluginPkgs
347
348
  const result = {}
348
349
  for (const pkgName of pluginPkgs) {
349
350
  const ns = camelCase(pkgName)
350
351
  let r
351
- if (glob) {
352
+ if (globs.length > 0) {
352
353
  const base = prefix === '' ? `${this.app[ns].dir.pkg}/extend` : `${this.app[ns].dir.pkg}/extend/${prefix}`
353
- let opts = isString(glob) ? { pattern: [glob] } : glob
354
- let pattern = opts.pattern ?? []
355
- if (isString(pattern)) pattern = [pattern]
356
- opts = omit(opts, ['pattern'])
357
- for (const i in pattern) {
358
- if (!path.isAbsolute(pattern[i])) pattern[i] = `${base}/${pattern[i]}`
359
- }
360
- const files = await fastGlob(pattern, opts)
354
+ const patterns = globs.map(glob => {
355
+ return !path.isAbsolute(glob) ? `${base}/${glob}` : glob
356
+ })
357
+ const files = await fastGlob.glob(patterns, opts)
361
358
  for (const f of files) {
362
359
  if (path.basename(f)[0] === '_' && noUnderscore) continue
363
360
  const resp = await handler.call(this.app[ns], { file: f, dir: base })
@@ -850,7 +847,7 @@ class Bajo extends Plugin {
850
847
  const { parseObject } = this.app.lib
851
848
  const { defaultsDeep } = this.app.lib.aneka
852
849
  const { uniq, isString, isArray, findIndex, isPlainObject, merge } = this.app.lib._
853
- let { ns, baseNs, extend, checkOverride, merge: merged, pattern, ignoreError = true, defValue = {}, parserOpts = {}, globOpts = {}, handler } = options
850
+ let { ns, baseNs, extend, checkOverride, merge: merged, pattern, ignoreError = true, defValue = {}, parserOpts = {}, globOpts = {}, handler, cache = {} } = options
854
851
 
855
852
  const getParseOptsArgs = (opts, orig) => {
856
853
  opts.parserOpts = opts.parserOpts ?? {}
@@ -866,6 +863,7 @@ class Bajo extends Plugin {
866
863
  let orig = parseObject(obj)
867
864
  if (!baseNs || extend === false) {
868
865
  await this.runHook('bajo:afterReadConfig', file, orig, options)
866
+ if (cache.name) await this.app.cache.save(cache.name, orig, cache.ttlDur)
869
867
  return orig
870
868
  }
871
869
  const { suffix = '', keys = [] } = options
@@ -907,9 +905,13 @@ class Bajo extends Plugin {
907
905
  let result = isArray(orig) ? [...orig, ...ext] : binder({}, keys.length > 0 ? pick(ext, keys) : ext, orig)
908
906
  if (handler) result = await this.callHandler(this.app[ns], handler, result)
909
907
  await this.runHook('bajo:afterReadConfig', file, result, options)
908
+ if (cache.name) await this.app.cache.save(cache.name, result, cache.ttlDur)
910
909
  return result
911
910
  }
912
911
 
912
+ let result
913
+ if (cache.name) result = await this.app.cache.load(cache.name, cache.ttlDur)
914
+ if (result) return result
913
915
  await this.runHook('bajo:beforeReadConfig', file, options)
914
916
  parserOpts.readFromFile = true
915
917
  if (!ns) ns = this.ns
@@ -999,19 +1001,19 @@ class Bajo extends Plugin {
999
1001
  * @param {string} path - Base path to start looking config files
1000
1002
  * @returns {Object}
1001
1003
  */
1002
- readAllConfigs = async (path) => {
1004
+ readAllConfigs = async (path, options) => {
1003
1005
  const { defaultsDeep } = this.app.lib.aneka
1004
1006
  let cfg = {}
1005
1007
  let ext = {}
1006
1008
  // default config file
1007
1009
  try {
1008
- cfg = await this.readConfig(`${path}.*`)
1010
+ cfg = await this.readConfig(`${path}.*`, options)
1009
1011
  } catch (err) {
1010
1012
  if (['BAJO_CONFIG_NO_PARSER'].includes(err.code)) throw err
1011
1013
  }
1012
1014
  // env based config file
1013
1015
  try {
1014
- ext = await this.readConfig(`${path}-${this.config.env}.*`)
1016
+ ext = await this.readConfig(`${path}-${this.config.env}.*`, options)
1015
1017
  } catch (err) {
1016
1018
  if (!['BAJO_CONFIG_FILE_NOT_FOUND'].includes(err.code)) throw err
1017
1019
  }
@@ -1028,15 +1030,7 @@ class Bajo extends Plugin {
1028
1030
  * @returns {Array} Array of hook execution results
1029
1031
  */
1030
1032
  runHook = async (hookName, ...args) => {
1031
- let ns
1032
- let path
1033
- let subNs
1034
- try {
1035
- ({ ns, subNs, path } = this.breakNsPath(hookName ?? ''))
1036
- } catch (err) {
1037
- return
1038
- }
1039
- let fns = filter(this.hooks, { ns, subNs, path })
1033
+ let fns = filter(this.hooks, { name: hookName })
1040
1034
  if (isEmpty(fns)) return []
1041
1035
  fns = orderBy(fns, ['level'])
1042
1036
  const results = []
@@ -69,7 +69,11 @@ const defConfig = {
69
69
  id: 'metric'
70
70
  }
71
71
  },
72
- exitHandler: true
72
+ exitHandler: true,
73
+ cache: {
74
+ purge: [],
75
+ purgeIntvDur: '5m'
76
+ }
73
77
  }
74
78
 
75
79
  const defMain = `async function factory (pkgName) {
@@ -203,9 +207,9 @@ export async function collectConfigHandlers () {
203
207
  */
204
208
  export async function buildExtConfig () {
205
209
  // config merging
206
- const { defaultsDeep } = this.app.lib.aneka
210
+ const { defaultsDeep, includes } = this.app.lib.aneka
207
211
  const { parseObject, omitDeep } = this.app.lib
208
- const { isEmpty, get } = this.app.lib._
212
+ const { isEmpty, get, isString, without } = this.app.lib._
209
213
 
210
214
  let resp = get(this, `app.options.config.${this.ns}`, {})
211
215
  if (isEmpty(resp)) resp = await this.readAllConfigs(`${this.dir.data}/config/${this.ns}`)
@@ -231,6 +235,16 @@ export async function buildExtConfig () {
231
235
  this.config.exitHandler = false
232
236
  }
233
237
  if (this.config.runtime.noWarning) process.removeAllListeners('warning')
238
+ if (isString(this.config.cache.purge)) this.config.cache.purge = [this.config.cache.purge]
239
+ this.config.cache.purge = without(this.config.cache.purge, '', null, undefined)
240
+ if (this.config.cache.purge.length > 0) {
241
+ if (includes(['all', '*'], this.config.cache.purge)) this.app.cache.purge('*')
242
+ else {
243
+ for (const name of this.config.cache.purge) {
244
+ this.app.cache.purge(name)
245
+ }
246
+ }
247
+ }
234
248
  this.app.log = new Log(this.app)
235
249
  this.log.trace('dataDir%s', this.dir.data)
236
250
  this.log.debug('configHandlers%s', this.join(exts))
@@ -100,29 +100,43 @@ export async function checkDependencies () {
100
100
  */
101
101
  export async function collectHooks () {
102
102
  const { eachPlugins, runHook, isLogInRange, importModule } = this.bajo
103
+ const { isArray, isPlainObject } = this.lib._
103
104
  const me = this // "this" is "app"
104
105
  me.bajo.log.trace('collecting%s', this.t('hooks'))
105
106
  await eachPlugins(async function ({ dir, file }) {
106
- const mod = await importModule(file, { asHandler: true })
107
+ let mod = await importModule(file, { asHandler: true })
107
108
  if (!mod) return undefined
108
- const _file = file.replace(dir + '/hook/', '').replace('.js', '')
109
- let [names, path] = _file.split('@')
110
- names = names.split('$').map(n => trim(n))
111
- for (const name of names) {
112
- let [ns, subNs, subSubNs] = name.split('.').map(n => camelCase(n))
113
- if (subSubNs) subNs = `${subNs}.${subSubNs}`
114
- const m = merge({}, mod, { ns, subNs, path: camelCase(path), src: this.ns })
115
- me.bajo.hooks.push(m)
109
+ if (file.includes('hook.js')) mod = await mod.handler.call(this)
110
+ if (isArray(mod)) {
111
+ for (const m of mod) {
112
+ if (!isPlainObject(m)) continue
113
+ if (!m.name) throw me.bajo.error('missing%s%s', 'name', file)
114
+ if (isArray(m.name)) {
115
+ for (const name of m.name) {
116
+ me.bajo.hooks.push(merge({}, m, { name, src: this.ns }))
117
+ }
118
+ } else {
119
+ m.src = this.ns
120
+ me.bajo.hooks.push(m)
121
+ }
122
+ }
123
+ } else {
124
+ const _file = file.replace(dir + '/hook/', '').replace('.js', '')
125
+ let [names, path] = _file.split('@')
126
+ names = names.split('$').map(n => trim(n))
127
+ for (let name of names) {
128
+ name = name.split('.').map(n => camelCase(n)).join('.')
129
+ const m = merge({}, mod, { name: `${name}:${camelCase(path)}`, src: this.ns })
130
+ me.bajo.hooks.push(m)
131
+ }
116
132
  }
117
- }, { glob: 'hook/**/*.js', prefix: me.bajo.ns })
133
+ }, { glob: ['hook/*.js', 'hook.js'], prefix: me.bajo.ns })
118
134
  // for log trace purpose only
119
135
  if (isLogInRange('trace')) {
120
- const items = groupBy(me.bajo.hooks, item => item.ns + (item.subNs ? `.${item.subNs}` : ''))
136
+ const items = groupBy(me.bajo.hooks, item => item.name)
121
137
  forOwn(items, (v, k) => {
122
- const hooks = groupBy(v, 'path')
123
- forOwn(hooks, (v1, k1) => {
124
- me.bajo.log.trace('- %s:%s (%d)', k, k1, v1.length)
125
- })
138
+ const [name, path] = k.split(':')
139
+ me.bajo.log.trace('- %s:%s (%d)', name, path, v.length)
126
140
  })
127
141
  }
128
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bajo",
3
- "version": "2.14.1",
3
+ "version": "2.15.0",
4
4
  "description": "The ultimate framework for whipping up massive apps in no time",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-28
4
+
5
+ - [2.15.0] Add built-in cache feature through ```app.cache``` instance
6
+ - [2.15.0] Bug fix in ```eachPlugins()```
7
+ - [2.15.0] Add cache feature to ```readConfig()```
8
+ - [2.15.0] Add feature to read many hooks as one file
9
+
3
10
  ## 2026-05-22
4
11
 
5
12
  - [2.14.1] Bug fix in ```helper.buildBaseConfig()```