bajo 1.0.11 → 1.1.1

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.
Files changed (77) hide show
  1. package/bajo/intl/en-US.json +5 -2
  2. package/bajo/intl/id.json +5 -2
  3. package/boot/class/app.js +11 -13
  4. package/boot/class/bajo-core/{helper/boot-order.js → boot-order.js} +3 -4
  5. package/boot/class/bajo-core/boot-plugins.js +19 -0
  6. package/boot/class/bajo-core/{helper/build-config.js → build-config.js} +16 -18
  7. package/boot/class/bajo-core/{helper/build-plugins.js → build-plugins.js} +13 -5
  8. package/boot/class/bajo-core/{helper/collect-config-handlers.js → collect-config-handlers.js} +2 -4
  9. package/boot/class/bajo-core.js +738 -11
  10. package/boot/class/bajo-plugin/{helper/attach-method.js → attach-method.js} +2 -2
  11. package/boot/class/bajo-plugin/{helper/collect-exit-handlers.js → collect-exit-handlers.js} +1 -1
  12. package/boot/class/bajo-plugin.js +13 -14
  13. package/boot/class/error.js +10 -2
  14. package/boot/class/log.js +9 -10
  15. package/boot/class/plugin.js +8 -12
  16. package/boot/class/print.js +16 -17
  17. package/boot/lib/create-method.js +3 -5
  18. package/boot/lib/current-loc.js +1 -1
  19. package/boot/lib/import-module.js +27 -0
  20. package/boot/lib/log-levels.js +1 -0
  21. package/boot/lib/parse-args-argv.js +2 -3
  22. package/boot/lib/read-all-configs.js +3 -6
  23. package/boot/{class/bajo-core/method → lib}/resolve-path.js +1 -1
  24. package/package.json +1 -1
  25. package/boot/class/bajo-core/helper/attach-method.js +0 -31
  26. package/boot/class/bajo-core/helper/boot-plugins.js +0 -19
  27. package/boot/class/bajo-core/method/arrange-array.js +0 -19
  28. package/boot/class/bajo-core/method/break-ns-path-from-file.js +0 -25
  29. package/boot/class/bajo-core/method/break-ns-path.js +0 -31
  30. package/boot/class/bajo-core/method/build-collections.js +0 -48
  31. package/boot/class/bajo-core/method/call-handler.js +0 -31
  32. package/boot/class/bajo-core/method/defaults-deep.js +0 -17
  33. package/boot/class/bajo-core/method/each-plugins.js +0 -57
  34. package/boot/class/bajo-core/method/envs.js +0 -7
  35. package/boot/class/bajo-core/method/extract-text.js +0 -15
  36. package/boot/class/bajo-core/method/format.js +0 -36
  37. package/boot/class/bajo-core/method/generate-id.js +0 -23
  38. package/boot/class/bajo-core/method/get-global-module-dir.js +0 -28
  39. package/boot/class/bajo-core/method/get-key-by-value.js +0 -5
  40. package/boot/class/bajo-core/method/get-method.js +0 -12
  41. package/boot/class/bajo-core/method/get-module-dir.js +0 -35
  42. package/boot/class/bajo-core/method/get-plugin-data-dir.js +0 -11
  43. package/boot/class/bajo-core/method/get-plugin-file.js +0 -21
  44. package/boot/class/bajo-core/method/get-plugin.js +0 -23
  45. package/boot/class/bajo-core/method/import-module.js +0 -27
  46. package/boot/class/bajo-core/method/import-pkg.js +0 -48
  47. package/boot/class/bajo-core/method/includes.js +0 -11
  48. package/boot/class/bajo-core/method/is-class.js +0 -7
  49. package/boot/class/bajo-core/method/is-empty-dir.js +0 -9
  50. package/boot/class/bajo-core/method/is-log-in-range.js +0 -12
  51. package/boot/class/bajo-core/method/is-set.js +0 -5
  52. package/boot/class/bajo-core/method/is-valid-app.js +0 -12
  53. package/boot/class/bajo-core/method/is-valid-plugin.js +0 -12
  54. package/boot/class/bajo-core/method/join.js +0 -20
  55. package/boot/class/bajo-core/method/log-levels.js +0 -9
  56. package/boot/class/bajo-core/method/num-unit.js +0 -11
  57. package/boot/class/bajo-core/method/paginate.js +0 -28
  58. package/boot/class/bajo-core/method/parse-object.js +0 -62
  59. package/boot/class/bajo-core/method/pascal-case.js +0 -9
  60. package/boot/class/bajo-core/method/pick.js +0 -13
  61. package/boot/class/bajo-core/method/read-config.js +0 -50
  62. package/boot/class/bajo-core/method/read-json.js +0 -14
  63. package/boot/class/bajo-core/method/round.js +0 -6
  64. package/boot/class/bajo-core/method/run-hook.js +0 -28
  65. package/boot/class/bajo-core/method/save-as-download.js +0 -19
  66. package/boot/class/bajo-core/method/sec-to-hms.js +0 -25
  67. package/boot/class/bajo-core/method/slice-string.js +0 -13
  68. package/boot/class/bajo-core/method/titleize.js +0 -24
  69. package/boot/class/bajo-core/method/white-space.js +0 -3
  70. package/boot/lib/translate.js +0 -19
  71. /package/boot/class/bajo-core/{helper/exit-handler.js → exit-handler.js} +0 -0
  72. /package/boot/class/bajo-core/{helper/run-as-applet.js → run-as-applet.js} +0 -0
  73. /package/boot/class/bajo-plugin/{helper/build-config.js → build-config.js} +0 -0
  74. /package/boot/class/bajo-plugin/{helper/check-clash.js → check-clash.js} +0 -0
  75. /package/boot/class/bajo-plugin/{helper/check-dependency.js → check-dependency.js} +0 -0
  76. /package/boot/class/bajo-plugin/{helper/collect-hooks.js → collect-hooks.js} +0 -0
  77. /package/boot/class/bajo-plugin/{helper/run.js → run.js} +0 -0
@@ -1,30 +1,757 @@
1
1
  import Plugin from './plugin.js'
2
+ import BajoPlugin from './bajo-plugin.js'
2
3
  import dayjs from '../lib/dayjs.js'
3
- import importModule from './bajo-core/method/import-module.js'
4
- import readJson from './bajo-core/method/read-json.js'
5
-
4
+ import increment from 'add-filename-increment'
5
+ import fs from 'fs-extra'
6
+ import path from 'path'
7
+ import os from 'os'
8
+ import ms from 'ms'
9
+ import dotenvParseVariables from 'dotenv-parse-variables'
10
+ import emptyDir from 'empty-dir'
6
11
  import lodash from 'lodash'
7
- const { isFunction } = lodash
12
+ import currentLoc from '../lib/current-loc.js'
13
+ import { createRequire } from 'module'
14
+ import getGlobalPath from 'get-global-path'
15
+ import { customAlphabet } from 'nanoid'
16
+ import fastGlob from 'fast-glob'
17
+ import querystring from 'querystring'
18
+ import deepFreeze from 'deep-freeze-strict'
19
+ import { sprintf } from 'sprintf-js'
20
+ import outmatch from 'outmatch'
21
+ import resolvePath from '../lib/resolve-path.js'
22
+ import importModule from '../lib/import-module.js'
23
+ import logLevels from '../lib/log-levels.js'
8
24
 
9
- async function defConfigHandler (file, opts = {}) {
10
- let mod = await importModule(file)
11
- if (isFunction(mod)) mod = await mod.call(this, opts)
12
- return mod
13
- }
25
+ const require = createRequire(import.meta.url)
26
+
27
+ const {
28
+ isFunction, words, upperFirst, map, concat, uniq, forOwn, padStart,
29
+ trim, filter, isEmpty, orderBy, pullAt, find, camelCase, isNumber,
30
+ cloneDeep, isPlainObject, isArray, isString, set, omit, keys, indexOf,
31
+ last, get, has, values, dropRight, mergeWith
32
+ } = lodash
14
33
 
15
34
  class BajoCore extends Plugin {
16
35
  constructor (app) {
17
36
  super('bajo', app)
18
37
  this.runAt = new Date()
19
38
  this.mainNs = 'main'
39
+ this.lib._ = lodash
40
+ this.lib.fs = fs
41
+ this.lib.fastGlob = fastGlob
42
+ this.lib.sprintf = sprintf
43
+ this.lib.outmatch = outmatch
20
44
  this.lib.dayjs = dayjs
45
+ this.lib.BajoPlugin = BajoPlugin
21
46
  this.applets = []
22
47
  this.pluginPkgs = []
23
48
  this.pluginNames = []
24
49
  this.configHandlers = [
25
- { ext: '.js', readHandler: defConfigHandler },
26
- { ext: '.json', readHandler: readJson }
50
+ { ext: '.js', readHandler: this._defConfigHandler },
51
+ { ext: '.json', readHandler: this.readJson }
27
52
  ]
53
+ this.whiteSpace = [' ', '\t', '\n', '\r']
54
+ this.logLevels = logLevels
55
+ this.envs = { dev: 'development', staging: 'staging', prod: 'production' }
56
+ }
57
+
58
+ async _defConfigHandler (file, opts = {}) {
59
+ let mod = await importModule(file)
60
+ if (isFunction(mod)) mod = await mod.call(this, opts)
61
+ return mod
62
+ }
63
+
64
+ resolvePath = (item, asFileUrl) => {
65
+ return resolvePath(item, asFileUrl)
66
+ }
67
+
68
+ freeze = (o, shallow) => {
69
+ if (shallow) Object.freeze(o)
70
+ else deepFreeze(o)
71
+ }
72
+
73
+ setImmediate = async () => {
74
+ return new Promise((resolve) => {
75
+ setImmediate(() => resolve())
76
+ })
77
+ }
78
+
79
+ arrangeArray = (inputs, trimItem = true) => {
80
+ const first = []
81
+ const last = []
82
+
83
+ const items = filter(inputs, item => {
84
+ if (trimItem) item = trim(item)
85
+ if (item[0] === '^') first.push(item.slice(1))
86
+ else if (item[0] === '$') last.push(item.slice(1))
87
+ return !['^', '$'].includes(item[0])
88
+ })
89
+ items.unshift(...first)
90
+ items.push(...last)
91
+ return items
92
+ }
93
+
94
+ breakNsPathFromFile = ({ file, dir, baseNs, suffix = '', getType } = {}) => {
95
+ let item = file.replace(dir + suffix, '')
96
+ let type
97
+ if (getType) {
98
+ const items = item.split('/')
99
+ type = items.shift()
100
+ item = items.join('/')
101
+ }
102
+ item = item.slice(0, item.length - path.extname(item).length)
103
+ let [name, _path] = item.split('@')
104
+ if (!_path) {
105
+ _path = name
106
+ name = baseNs
107
+ }
108
+ _path = camelCase(_path)
109
+ const names = map(name.split('.'), n => camelCase(n))
110
+ const [ns, subNs] = names
111
+ return { ns, subNs, path: _path, fullNs: names.join('.'), type }
112
+ }
113
+
114
+ breakNsPath = (item = '', defaultNs = 'bajo', checkNs = true) => {
115
+ let [ns, ...path] = item.split(':')
116
+ let subNs
117
+ let subSubNs
118
+ path = path.join(':')
119
+ if (path.startsWith('//')) return { ns: undefined, path: item } // for: http:// etc
120
+ if (isEmpty(path)) {
121
+ path = ns
122
+ ns = defaultNs
123
+ }
124
+ [ns, subNs, subSubNs] = ns.split('.')
125
+ if (checkNs) {
126
+ if (!this.app[ns]) {
127
+ const plugin = this.getPlugin(ns)
128
+ if (plugin) ns = plugin.name
129
+ }
130
+ if (!this.app[ns]) throw this.error('unknownPluginOrNotLoaded%s')
131
+ }
132
+ const fullPath = path
133
+ let qs
134
+ [path, qs] = path.split('?')
135
+ qs = querystring.parse(qs) ?? {}
136
+ return { ns, path, subNs, subSubNs, qs, fullPath }
137
+ }
138
+
139
+ buildCollections = async (options = {}) => {
140
+ let { ns, handler, dupChecks = [], container, useDefaultName } = options
141
+ useDefaultName = useDefaultName ?? true
142
+ if (!ns) ns = this.name
143
+ const cfg = this.app[ns].getConfig()
144
+ let items = get(cfg, container, [])
145
+ if (!isArray(items)) items = [items]
146
+ this.app[ns].log.trace('collecting%s', this.app[ns].print.write(container))
147
+ await this.runHook(`${ns}:${camelCase('beforeBuildCollection')}`, container)
148
+ const deleted = []
149
+ for (const index in items) {
150
+ const item = items[index]
151
+ if (useDefaultName) {
152
+ if (!has(item, 'name')) {
153
+ if (find(items, { name: 'default' })) throw this.app[ns].error('collExists%s', 'default')
154
+ else item.name = 'default'
155
+ }
156
+ }
157
+ this.app[ns].log.trace('- %s', item.name)
158
+ const result = await handler.call(this.app[ns], { item, index, cfg })
159
+ if (result) items[index] = result
160
+ else if (result === false) deleted.push(index)
161
+ if (this.app.bajo.applet && item.skipOnTool && !deleted.includes(index)) deleted.push(index)
162
+ }
163
+ if (deleted.length > 0) pullAt(items, deleted)
164
+
165
+ // check for duplicity
166
+ for (const c of items) {
167
+ for (const d of dupChecks) {
168
+ if (isFunction(d)) await d.call(this.app[ns], c, items)
169
+ else {
170
+ const checker = set({}, d, c[d])
171
+ const match = filter(items, checker)
172
+ if (match.length > 1) this.app[ns].fatal('oneOrMoreSharedTheSame%s%s', container, this.join(dupChecks.filter(i => !isFunction(i))))
173
+ }
174
+ }
175
+ }
176
+ await this.runHook(`${ns}:${camelCase('afterBuildCollection')}`, container)
177
+ this.app[ns].log.debug('collected%s%d', this.app[ns].print.write(container), items.length)
178
+ return items
179
+ }
180
+
181
+ callHandler = async (item, ...args) => {
182
+ let result
183
+ let scope = this
184
+ if (item instanceof BajoPlugin) {
185
+ scope = item
186
+ item = args.shift()
187
+ }
188
+ const bajo = scope.app.bajo
189
+ if (isString(item)) {
190
+ if (item.startsWith('applet:') && bajo.applets.length > 0) {
191
+ const [, ns, path] = item.split(':')
192
+ const applet = find(bajo.applets, a => (a.ns === ns || a.alias === ns))
193
+ if (applet) result = await bajo.runApplet(applet, path, ...args)
194
+ } else {
195
+ const method = bajo.getMethod(item)
196
+ if (method) result = await method(...args)
197
+ }
198
+ } else if (isFunction(item)) {
199
+ result = await item.call(scope, ...args)
200
+ } else if (isPlainObject(item) && item.handler) {
201
+ result = await item.handler.call(scope, ...args)
202
+ }
203
+ return result
204
+ }
205
+
206
+ defaultsDeep = (...args) => {
207
+ const output = {}
208
+ args.reverse().forEach(function (item) {
209
+ mergeWith(output, item, function (objectValue, sourceValue) {
210
+ return isArray(sourceValue) ? sourceValue : undefined
211
+ })
212
+ })
213
+ return output
214
+ }
215
+
216
+ eachPlugins = async (handler, options = {}) => {
217
+ if (typeof options === 'string') options = { glob: options }
218
+ const result = {}
219
+ const pluginPkgs = cloneDeep(this.app.bajo.pluginPkgs) ?? []
220
+ const { glob, useBajo, prefix = '', noUnderscore = true, returnItems } = options
221
+ if (useBajo) pluginPkgs.unshift('bajo')
222
+ for (const pkgName of pluginPkgs) {
223
+ const ns = camelCase(pkgName)
224
+ const config = this.app[ns].config
225
+ const alias = this.app[ns].alias
226
+ let r
227
+ if (glob) {
228
+ const base = prefix === '' ? this.app[ns].dir.pkg : `${this.app[ns].dir.pkg}/${prefix}`
229
+ let opts = isString(glob) ? { pattern: [glob] } : glob
230
+ let pattern = opts.pattern ?? []
231
+ if (isString(pattern)) pattern = [pattern]
232
+ opts = omit(opts, ['pattern'])
233
+ for (const i in pattern) {
234
+ if (!path.isAbsolute(pattern[i])) pattern[i] = `${base}/${pattern[i]}`
235
+ }
236
+ const files = await fastGlob(pattern, opts)
237
+ for (const f of files) {
238
+ if (path.basename(f)[0] === '_' && noUnderscore) continue
239
+ const resp = await handler.call(this.app[ns], { ns, pkgName, config, alias, file: f, dir: base })
240
+ if (resp === false) break
241
+ else if (resp === undefined) continue
242
+ else {
243
+ result[ns] = result[ns] ?? {}
244
+ result[ns][f] = resp
245
+ }
246
+ }
247
+ } else {
248
+ r = await handler.call(this.app[ns], { ns, pkgName, config, dir: this.app[ns].dir.pkg, alias })
249
+ if (r === false) break
250
+ else if (r === undefined) continue
251
+ else result[ns] = r
252
+ }
253
+ }
254
+ if (returnItems) {
255
+ const data = []
256
+ for (const r in result) {
257
+ for (const f in result[r]) {
258
+ data.push(result[r][f])
259
+ }
260
+ }
261
+ return data
262
+ }
263
+ return result
264
+ }
265
+
266
+ extractText = (text, patternStart, patternEnd) => {
267
+ let result = ''
268
+ const open = text.indexOf(patternStart)
269
+ if (open > -1) {
270
+ text = text.slice(open + patternStart.length)
271
+ const close = text.indexOf(patternEnd)
272
+ if (close > -1) {
273
+ result = text.slice(0, close)
274
+ }
275
+ }
276
+ const pattern = `${patternStart}${result}${patternEnd}`
277
+ return { result, pattern }
278
+ }
279
+
280
+ format = (value, type, options = {}) => {
281
+ const { format } = this.config.intl
282
+ const { emptyValue = format.emptyValue } = options
283
+ const lang = options.lang ?? this.config.lang
284
+ if ([undefined, null, ''].includes(value)) return emptyValue
285
+ if (type === 'auto') {
286
+ if (value instanceof Date) type = 'datetime'
287
+ }
288
+ if (['integer', 'smallint'].includes(type)) {
289
+ value = parseInt(value)
290
+ if (isNaN(value)) return emptyValue
291
+ const setting = this.defaultsDeep(options.integer, format.integer)
292
+ return new Intl.NumberFormat(lang, setting).format(value)
293
+ }
294
+ if (['float', 'double'].includes(type)) {
295
+ value = parseFloat(value)
296
+ if (isNaN(value)) return emptyValue
297
+ if (this.app.bajoSpatial && options.latitude) return this.app.bajoSpatial.latToDms(value)
298
+ if (this.app.bajoSpatial && options.longitude) return this.app.bajoSpatial.lngToDms(value)
299
+ const setting = this.defaultsDeep(options.float, format.float)
300
+ return new Intl.NumberFormat(lang, setting).format(value)
301
+ }
302
+ if (['datetime', 'date'].includes(type)) {
303
+ const setting = this.defaultsDeep(options[type], format[type])
304
+ return new Intl.DateTimeFormat(lang, setting).format(new Date(value))
305
+ }
306
+ if (['time'].includes(type)) {
307
+ const setting = this.defaultsDeep(options.time, format.time)
308
+ return new Intl.DateTimeFormat(lang, setting).format(new Date(`1970-01-01T${value}Z`))
309
+ }
310
+ if (['array'].includes(type)) return value.join(', ')
311
+ if (['object'].includes(type)) return JSON.stringify(value)
312
+ return value
313
+ }
314
+
315
+ generateId = (options = {}) => {
316
+ let type
317
+ if (options === true) options = 'alpha'
318
+ if (options === 'int') {
319
+ type = options
320
+ options = { pattern: '0123456789', length: 15 }
321
+ } else if (options === 'alpha') {
322
+ type = options
323
+ options = { pattern: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', length: 15 }
324
+ }
325
+ let { pattern, length = 13, returnInstance } = options
326
+ pattern = pattern ?? 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
327
+ if (options.case === 'lower') pattern = pattern.toLowerCase()
328
+ else if (options.case === 'upper') pattern = pattern.toUpperCase()
329
+ const nid = customAlphabet(pattern, length)
330
+ if (returnInstance) return nid
331
+ const value = nid()
332
+ return type === 'int' ? parseInt(value) : value
333
+ }
334
+
335
+ getGlobalModuleDir = (pkgName, silent = true) => {
336
+ let nodeModulesDir = process.env.BAJO_GLOBAL_MODULE_DIR
337
+ if (!nodeModulesDir) {
338
+ const npmPath = getGlobalPath('npm')
339
+ if (!npmPath) {
340
+ if (silent) return
341
+ throw this.error('cantLocateNpmGlobalDir', { code: 'BAJO_CANT_LOCATE_NPM_GLOBAL_DIR' })
342
+ }
343
+ nodeModulesDir = dropRight(resolvePath(npmPath).split('/'), 1).join('/')
344
+ process.env.BAJO_GLOBAL_MODULE_DIR = nodeModulesDir
345
+ }
346
+ if (!pkgName) return nodeModulesDir
347
+ const dir = `${nodeModulesDir}/${pkgName}`
348
+ if (!fs.existsSync(dir)) {
349
+ if (silent) return
350
+ throw this.error('cantLocateGlobalDir%s', pkgName, { code: 'BAJO_CANT_LOCATE_MODULE_GLOBAL_DIR' })
351
+ }
352
+ return dir
353
+ }
354
+
355
+ getKeyByValue = (object, value) => {
356
+ return Object.keys(object).find(key => object[key] === value)
357
+ }
358
+
359
+ getMethod = (name = '', thrown = true) => {
360
+ const { ns, path } = this.breakNsPath(name)
361
+ const method = get(this.app, `${ns}.${path}`)
362
+ if (method && isFunction(method)) return method
363
+ if (thrown) throw this.error('cantFindMethod%s', name)
364
+ }
365
+
366
+ findDeep = (item, paths) => {
367
+ let dir
368
+ for (const p of paths) {
369
+ const d = `${p}/${item}`
370
+ if (fs.existsSync(d)) {
371
+ dir = d
372
+ break
373
+ }
374
+ }
375
+ return dir
376
+ }
377
+
378
+ getModuleDir = (pkgName, base) => {
379
+ if (pkgName === 'main') return resolvePath(process.env.BAJOCWD)
380
+ if (base === 'main') base = process.env.BAJOCWD
381
+ else if (this && this.app && this.app[base]) base = this.app[base].pkgName
382
+ const pkgPath = pkgName + '/package.json'
383
+ const paths = require.resolve.paths(pkgPath)
384
+ const gdir = this.getGlobalModuleDir()
385
+ paths.unshift(gdir)
386
+ paths.unshift(resolvePath(path.join(process.env.BAJOCWD, 'node_modules')))
387
+ let dir = this.findDeep(pkgPath, paths)
388
+ if (base && !dir) dir = this.findDeep(`${base}/node_modules/${pkgPath}`, paths)
389
+ if (!dir) return null
390
+ return resolvePath(path.dirname(dir))
391
+ }
392
+
393
+ getPluginDataDir = (name, ensureDir = true) => {
394
+ const { getPlugin } = this.app.bajo
395
+ const plugin = getPlugin(name)
396
+ const dir = `${this.app.bajo.dir.data}/plugins/${plugin.name}`
397
+ if (ensureDir) fs.ensureDirSync(dir)
398
+ return dir
399
+ }
400
+
401
+ getPluginFile = (file) => {
402
+ if (!this) return file
403
+ if (file[0] === '.') file = `${currentLoc(import.meta).dir}/${trim(file.slice(1), '/')}`
404
+ if (file.includes(':')) {
405
+ if (file.slice(1, 2) === ':') return file // windows fs
406
+ const { ns, path } = this.breakNsPath(file)
407
+ if (ns !== 'file' && this && this.app && this.app[ns] && ns.length > 1) {
408
+ file = `${this.app[ns].dir.pkg}${path}`
409
+ }
410
+ }
411
+ return file
412
+ }
413
+
414
+ getPlugin = (name, silent) => {
415
+ if (!this.app[name]) {
416
+ // alias?
417
+ let plugin
418
+ for (const key in this.app) {
419
+ const item = this.app[key]
420
+ if (item instanceof BajoPlugin && (item.alias === name || item.pkgName === name)) {
421
+ plugin = item
422
+ break
423
+ }
424
+ }
425
+ if (!plugin) {
426
+ if (silent) return false
427
+ throw this.error('pluginWithNameAliasNotLoaded%s', name)
428
+ }
429
+ name = plugin.name
430
+ }
431
+ return this.app[name]
432
+ }
433
+
434
+ importModule = async (file, { asDefaultImport, asHandler, noCache } = {}) => {
435
+ return await importModule.call(this, file, { asDefaultImport, asHandler, noCache })
436
+ }
437
+
438
+ importPkg = async (...pkgs) => {
439
+ const result = {}
440
+ const notFound = []
441
+ let opts = { returnDefault: true, thrownNotFound: false }
442
+ if (isPlainObject(last(pkgs))) {
443
+ opts = this.defaultsDeep(pkgs.pop(), opts)
444
+ }
445
+ for (const pkg of pkgs) {
446
+ const { ns, path: name } = this.breakNsPath(pkg)
447
+ const dir = this.getModuleDir(name, ns)
448
+ if (!dir) {
449
+ notFound.push(pkg)
450
+ continue
451
+ }
452
+ const p = this.readJson(`${dir}/package.json`, opts.thrownNotFound)
453
+ const mainFileOrg = dir + '/' + (p.main ?? get(p, 'exports.default', 'index.js'))
454
+ let mainFile = resolvePath(mainFileOrg, os.platform() === 'win32')
455
+ if (isEmpty(path.extname(mainFile))) {
456
+ if (fs.existsSync(`${mainFileOrg}/index.js`)) mainFile += '/index.js'
457
+ else mainFile += '.js'
458
+ }
459
+ if (opts.noCache) mainFile += `?_=${Date.now()}`
460
+ let mod = await import(mainFile)
461
+ if (opts.returnDefault && has(mod, 'default')) {
462
+ mod = mod.default
463
+ if (opts.returnDefault && has(mod, 'default')) mod = mod.default
464
+ }
465
+ result[name] = mod
466
+ }
467
+ if (notFound.length > 0) throw this.error('cantFind%s', this.join(notFound))
468
+ if (pkgs.length === 1) return result[keys(result)[0]]
469
+ if (opts.asObject) return result
470
+ return values(result)
471
+ }
472
+
473
+ includes = (matcher = [], array = []) => {
474
+ if (typeof matcher === 'string') matcher = [matcher]
475
+ let found = false
476
+ for (const m of matcher) {
477
+ found = array.includes(m)
478
+ if (found) break
479
+ }
480
+ return found
481
+ }
482
+
483
+ isClass = (item) => {
484
+ return typeof item === 'function' &&
485
+ Object.prototype.hasOwnProperty.call(item, 'prototype') &&
486
+ !Object.prototype.hasOwnProperty.call(item, 'arguments')
487
+ }
488
+
489
+ isEmptyDir = async (dir) => {
490
+ await fs.exists(dir)
491
+ return await emptyDir(dir)
492
+ }
493
+
494
+ isLogInRange = (level) => {
495
+ const levels = keys(this.logLevels)
496
+ const logLevel = indexOf(levels, this.app.bajo.config.log.level)
497
+ return indexOf(levels, level) >= logLevel
498
+ }
499
+
500
+ isSet = (input) => {
501
+ return ![null, undefined].includes(input)
502
+ }
503
+
504
+ isValidApp = (dir) => {
505
+ if (!dir) dir = process.env.BAJOCWD
506
+ dir = resolvePath(dir)
507
+ const hasMainDir = fs.existsSync(`${dir}/main/plugin`)
508
+ const hasPackageJson = fs.existsSync(`${dir}/package.json`)
509
+ return hasMainDir && hasPackageJson
510
+ }
511
+
512
+ isValidPlugin = (dir) => {
513
+ if (!dir) dir = process.env.BAJOCWD
514
+ dir = resolvePath(dir)
515
+ const hasPluginDir = fs.existsSync(`${dir}/plugin`)
516
+ const hasPackageJson = fs.existsSync(`${dir}/package.json`)
517
+ return hasPluginDir && hasPackageJson
518
+ }
519
+
520
+ join = (array, sep) => {
521
+ const translate = val => {
522
+ if (this && this.print) return this.print.write(val).toLowerCase()
523
+ return val
524
+ }
525
+ if (array.length === 0) return translate('none')
526
+ if (array.length === 1) return array[0]
527
+ if (this.isSet(sep) && !isPlainObject(sep)) return array.join(sep)
528
+ let { separator = ', ', joiner = 'and' } = sep ?? {}
529
+ joiner = translate(joiner)
530
+ const last = (array.pop() ?? '').trim()
531
+ return array.map(a => (a + '').trim()).join(separator) + ` ${joiner} ${last}`
532
+ }
533
+
534
+ numUnit = (value = '', defUnit = '') => {
535
+ const num = value.match(/\d+/g)
536
+ const unit = value.match(/[a-zA-Z]+/g)
537
+ return `${num[0]}${isEmpty(unit) ? defUnit : unit[0]}`
538
+ }
539
+
540
+ paginate = (collection, { page = 1, limit = 25, sort } = {}) => {
541
+ const count = collection.length
542
+ const offset = (page - 1) * limit
543
+ const fields = []
544
+ const dirs = []
545
+ if (isPlainObject(sort)) {
546
+ forOwn(sort, (v, k) => {
547
+ fields.push(k)
548
+ dirs.push(v < 0 ? 'desc' : 'asc')
549
+ })
550
+ }
551
+ if (!isEmpty(fields)) collection = orderBy(collection, fields, dirs)
552
+ const data = collection.slice(offset, offset + limit)
553
+
554
+ return {
555
+ data,
556
+ page,
557
+ limit,
558
+ count,
559
+ pages: Math.ceil(collection.length / limit)
560
+ }
561
+ }
562
+
563
+ parseDur = (val) => {
564
+ return isNumber(val) ? val : ms(val)
565
+ }
566
+
567
+ parseDt = (val) => {
568
+ const dt = this.lib.dayjs(val)
569
+ if (!dt.isValid()) throw this.error('dtUnparsable%s', val)
570
+ return dt.toDate()
571
+ }
572
+
573
+ parseObject = (input, { silent = true, parseValue = false, lang, ns } = {}) => {
574
+ const statics = ['*']
575
+ let obj = cloneDeep(input)
576
+ const keys = Object.keys(obj)
577
+ const me = this
578
+ const mutated = []
579
+ keys.forEach(k => {
580
+ const v = obj[k]
581
+ if (isPlainObject(v)) obj[k] = this.parseObject(v)
582
+ else if (isArray(v)) {
583
+ v.forEach((i, idx) => {
584
+ if (isPlainObject(i)) obj[k][idx] = this.parseObject(i)
585
+ else if (statics.includes(i)) obj[k][idx] = i
586
+ else if (parseValue) obj[k][idx] = dotenvParseVariables(set({}, 'item', obj[k][idx]), { assignToProcessEnv: false }).item
587
+ if (isArray(obj[k][idx])) obj[k][idx] = obj[k][idx].map(item => typeof item === 'string' ? item.trim() : item)
588
+ })
589
+ } else if (this.isSet(v)) {
590
+ try {
591
+ if (statics.includes(v)) obj[k] = v
592
+ else if (k.startsWith('t:') && isString(v)) {
593
+ const newK = k.slice(2)
594
+ if (lang) {
595
+ const scope = ns ? me.app[ns] : me
596
+ const [text, ...args] = v.split('|')
597
+ obj[newK] = scope.print.write(text, ...args, { lang })
598
+ } else obj[newK] = v
599
+ mutated.push(k)
600
+ } else if (parseValue) {
601
+ obj[k] = dotenvParseVariables(set({}, 'item', v), { assignToProcessEnv: false }).item
602
+ if (isArray(obj[k])) obj[k] = obj[k].map(item => typeof item === 'string' ? item.trim() : item)
603
+ }
604
+ if (k.slice(-3) === 'Dur') obj[k] = this.parseDur(v)
605
+ if (k.slice(-2) === 'Dt') obj[k] = this.parseDt(v)
606
+ } catch (err) {
607
+ obj[k] = undefined
608
+ if (!silent) throw err
609
+ }
610
+ }
611
+ })
612
+ if (mutated.length > 0) obj = omit(obj, mutated)
613
+ return obj
614
+ }
615
+
616
+ pascalCase = (text) => {
617
+ return upperFirst(camelCase(text))
618
+ }
619
+
620
+ pick = (obj, items, excludeUnset) => {
621
+ const result = {}
622
+ for (const item of items) {
623
+ const [k, nk] = item.split(':')
624
+ if (excludeUnset && !this.isSet(obj[k])) continue
625
+ result[nk ?? k] = obj[k]
626
+ }
627
+ return result
628
+ }
629
+
630
+ readConfig = async (file, { ns, pattern, globOptions = {}, ignoreError, defValue = {}, opts = {} } = {}) => {
631
+ if (!ns) ns = this.name
632
+ file = resolvePath(this.getPluginFile(file))
633
+ let ext = path.extname(file)
634
+ const fname = path.dirname(file) + '/' + path.basename(file, ext)
635
+ ext = ext.toLowerCase()
636
+ if (['.mjs', '.js'].includes(ext)) {
637
+ const { readHandler } = find(this.app.bajo.configHandlers, { ext })
638
+ return this.parseObject(await readHandler.call(this.app[ns], file, opts))
639
+ }
640
+ if (ext === '.json') return await this.readJson(file)
641
+ if (!['', '.*'].includes(ext)) {
642
+ const item = find(this.app.bajo.configHandlers, { ext })
643
+ if (!item) {
644
+ if (!ignoreError) throw this.error('cantParse%s', file, { code: 'BAJO_CONFIG_NO_PARSER' })
645
+ return this.parseObject(defValue)
646
+ }
647
+ return this.parseObject(await item.readHandler.call(this.app[ns], file, opts))
648
+ }
649
+ const item = pattern ?? `${fname}.{${map(map(this.app.bajo.configHandlers, 'ext'), k => k.slice(1)).join(',')}}`
650
+ const files = await fastGlob(item, globOptions)
651
+ if (files.length === 0) {
652
+ if (!ignoreError) throw this.error('noConfigFileFound', { code: 'BAJO_CONFIG_FILE_NOT_FOUND' })
653
+ return this.parseObject(defValue)
654
+ }
655
+ let config = defValue
656
+ for (const f of files) {
657
+ const ext = path.extname(f).toLowerCase()
658
+ const item = find(this.app.bajo.configHandlers, { ext })
659
+ if (!item) {
660
+ if (!ignoreError) throw this.error('cantParse%s', f, { code: 'BAJO_CONFIG_NO_PARSER' })
661
+ continue
662
+ }
663
+ config = await item.readHandler.call(this.app[ns], f, opts)
664
+ if (!isEmpty(config)) break
665
+ }
666
+ return this.parseObject(config)
667
+ }
668
+
669
+ readJson = (file, thrownNotFound = false) => {
670
+ if (isPlainObject(thrownNotFound)) thrownNotFound = false
671
+ if (!fs.existsSync(file) && thrownNotFound) throw this.error('notFound%s%s', this.print.write('file'), file)
672
+ let resp
673
+ try {
674
+ resp = fs.readFileSync(file, 'utf8')
675
+ } catch (err) {}
676
+ if (isEmpty(resp)) return resp
677
+ return this.parseObject(JSON.parse(resp))
678
+ }
679
+
680
+ round = (val, scale = 0) => {
681
+ scale = scale <= 0 ? 1 : 10 ** scale
682
+ return Math.round(val * scale) / scale
683
+ }
684
+
685
+ runHook = async (hookName, ...args) => {
686
+ const [ns, path] = (hookName ?? '').split(':')
687
+ let fns = filter(this.app.bajo.hooks, { ns, path })
688
+ if (isEmpty(fns)) return
689
+ fns = orderBy(fns, ['level'])
690
+ const results = []
691
+ const removed = []
692
+ for (const i in fns) {
693
+ const fn = fns[i]
694
+ const scope = this.app[fn.src]
695
+ const res = await fn.handler.call(scope, ...args)
696
+ results.push({
697
+ hook: hookName,
698
+ resp: res
699
+ })
700
+ if (path.startsWith('once')) removed.push(i)
701
+ if (this.config.log.traceHook) scope.log.trace('hookExecuted%s', hookName)
702
+ }
703
+ if (removed.length > 0) pullAt(this.app.bajo.hooks, removed)
704
+
705
+ return results
706
+ }
707
+
708
+ saveAsDownload = async (file, obj, printSaved = true) => {
709
+ const { print, getPluginDataDir } = this.app.bajo
710
+ const plugin = this.name
711
+ const fname = increment(`${getPluginDataDir(plugin)}/${trim(file, '/')}`, { fs: true })
712
+ const dir = path.dirname(fname)
713
+ if (!fs.existsSync(dir)) fs.ensureDirSync(dir)
714
+ await fs.writeFile(fname, obj, 'utf8')
715
+ if (printSaved) print.succeed('savedAs%s', path.resolve(fname), { skipSilence: true })
716
+ return fname
717
+ }
718
+
719
+ // based on: https://stackoverflow.com/questions/1322732/convert-seconds-to-hh-mm-ss-with-javascript
720
+ secToHms = (secs, ms) => {
721
+ let remain
722
+ if (ms) {
723
+ remain = secs % 1000
724
+ secs = Math.floor(secs / 1000)
725
+ }
726
+ const secNum = parseInt(secs, 10)
727
+ const hours = Math.floor(secNum / 3600)
728
+ const minutes = Math.floor(secNum / 60) % 60
729
+ const seconds = secNum % 60
730
+
731
+ let hms = [hours, minutes, seconds]
732
+ .map(v => v < 10 ? '0' + v : v)
733
+ .filter((v, i) => v !== '00' || i > 0)
734
+ .join(':')
735
+ if (ms) hms += '+' + padStart(remain, 3, '0')
736
+ return hms
737
+ }
738
+
739
+ titleize = (text, { ignores = [], replacement = {} } = {}) => {
740
+ const defIgnores = ['or', 'and', 'of', 'with']
741
+ const replacer = {}
742
+ forOwn(replacement, (v, k) => {
743
+ const id = this.generateId('int')
744
+ replacer[id] = k
745
+ text = text.replace(k, ` ${id} `)
746
+ })
747
+ return map(words(text), t => {
748
+ forOwn(replacer, (v, k) => {
749
+ if (k === t) t = replacement[replacer[k]]
750
+ })
751
+ ignores = uniq(concat(ignores, defIgnores))
752
+ if (ignores.includes(t)) return t
753
+ return upperFirst(t)
754
+ }).join(' ')
28
755
  }
29
756
  }
30
757