bajo 2.16.0 → 2.18.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.
@@ -1,30 +1,32 @@
1
- import Print from '../plugin/print.js'
2
- import Log from '../app/log.js'
1
+ import Print from './print.js'
2
+ import Log from './log.js'
3
3
  import os from 'os'
4
4
  import fs from 'fs-extra'
5
5
  import lodash from 'lodash'
6
- import {
7
- buildConfigs,
8
- checkDependencies,
9
- checkNameAliases,
10
- collectHooks,
11
- run
12
- } from './base.js'
6
+ import semver from 'semver'
7
+ import aneka from 'aneka/index.js'
8
+ import outmatch from 'outmatch'
9
+ import fastGlob from 'fast-glob'
10
+ import { sprintf } from 'sprintf-js'
11
+ import dayjs from 'dayjs'
12
+ import utc from 'dayjs/plugin/utc.js'
13
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js'
14
+ import localizedFormat from 'dayjs/plugin/localizedFormat.js'
15
+ import weekOfYear from 'dayjs/plugin/weekOfYear.js'
16
+ import freeze from '../lib/freeze.js'
17
+ import findDeep from '../lib/find-deep.js'
18
+ import omitDeep from 'omit-deep'
19
+
20
+ /**
21
+ * Internal helpers called by Bajo and other classes. It should remains
22
+ * hidden and not to be imported by any program.
23
+ *
24
+ * @module Helper
25
+ */
13
26
 
14
27
  const {
15
- orderBy,
16
- isFunction,
17
- isPlainObject,
18
- map,
19
- pick,
20
- values,
21
- keys,
22
- set,
23
- get,
24
- without,
25
- uniq,
26
- camelCase,
27
- isEmpty
28
+ merge, forOwn, groupBy, find, reduce, map, trim, keys, intersection, each,
29
+ camelCase, get, orderBy, isFunction, isPlainObject, pick, values, set, without, uniq, isEmpty
28
30
  } = lodash
29
31
 
30
32
  const omitted = ['spawn', 'cwd', 'name', 'alias', 'applet', 'a', 'plugins']
@@ -49,6 +51,19 @@ const defConfig = {
49
51
  retain: 5
50
52
  }
51
53
  },
54
+ dump: {
55
+ depth: 2,
56
+ compact: false,
57
+ colors: true,
58
+ breakLength: 80,
59
+ caller: true,
60
+ frame: {
61
+ titleAllignment: 'center',
62
+ padding: 1,
63
+ margin: 1,
64
+ borderStyle: 'round'
65
+ }
66
+ },
52
67
  lang: Intl.DateTimeFormat().resolvedOptions().lang ?? 'en-US',
53
68
  intl: {
54
69
  supported: ['en-US', 'id'],
@@ -90,12 +105,64 @@ const defMain = `async function factory (pkgName) {
90
105
  export default factory
91
106
  `
92
107
 
108
+ export function outmatchNs (source, pattern) {
109
+ const { breakNsPath } = this.bajo
110
+ const [src, subSrc] = source.split(':')
111
+ if (!subSrc) return pattern === src
112
+ try {
113
+ const { fullNs, path } = breakNsPath(pattern)
114
+ const isMatch = outmatch(path)
115
+ return src === fullNs && isMatch(subSrc)
116
+ } catch (err) {
117
+ return false
118
+ }
119
+ }
120
+
121
+ export function parseObject (obj, options = {}) {
122
+ const me = this
123
+ const { ns = 'bajo', lang } = options
124
+ options.translator = {
125
+ lang,
126
+ prefix: 't:',
127
+ handler: val => {
128
+ const [text, ...args] = val.split('|')
129
+ args.push({ lang })
130
+ return me[ns].t(text, ...args)
131
+ }
132
+ }
133
+ return aneka.parseObject(obj, options)
134
+ }
135
+
136
+ dayjs.extend(utc)
137
+ dayjs.extend(customParseFormat)
138
+ dayjs.extend(localizedFormat)
139
+ dayjs.extend(weekOfYear)
140
+
93
141
  /**
94
- * Internal helpers called by Bajo that only used once for bootstrapping. It should remains
95
- * hidden and not to be imported by any program.
96
- *
97
- * @module Helper/Bajo
142
+ * @typedef {Object} TAppLib
143
+ * @property {Object} _ - Access to {@link https://lodash.com|lodash}
144
+ * @property {Object} fs - Access to {@link https://github.com/jprichardson/node-fs-extra|fs-extra}
145
+ * @property {Object} fastGlob - Access to {@link https://github.com/mrmlnc/fast-glob|fast-glob}
146
+ * @property {Object} sprintf - Access to {@link https://github.com/alexei/sprintf.js|sprintf}
147
+ * @property {Object} aneka - Access to {@link https://github.com/ardhi/aneka|aneka}
148
+ * @property {Object} outmatch - Access to {@link https://github.com/axtgr/outmatch|outmatch}
149
+ * @property {Object} dayjs - Access to {@link https://day.js.org|dayjs} with utc & customParseFormat plugin already applied
150
+ * @property {Object} freeze
151
+ * @property {Object} findDeep
152
+ * @see App
98
153
  */
154
+ export const lib = {
155
+ _: lodash,
156
+ fs,
157
+ fastGlob,
158
+ sprintf,
159
+ outmatch,
160
+ dayjs,
161
+ aneka,
162
+ freeze,
163
+ findDeep,
164
+ omitDeep
165
+ }
99
166
 
100
167
  /**
101
168
  * Building bajo base config. Mostly dealing with directory setups:
@@ -112,7 +179,7 @@ export async function buildBaseConfig () {
112
179
  const { defaultsDeep, textToArray, currentLoc, resolvePath } = this.app.lib.aneka
113
180
  this.config = defaultsDeep({}, this.app.argv._, this.app.envVars._)
114
181
  set(this, 'dir.base', this.app.dir)
115
- const path = currentLoc(import.meta).dir + '/../..'
182
+ const path = currentLoc(import.meta).dir + '/..'
116
183
  set(this, 'dir.pkg', resolvePath(path))
117
184
  if (get(this, 'config.dir.data')) set(this, 'dir.data', this.config.dir.data)
118
185
  if (!get(this, 'dir.data')) set(this, 'dir.data', `${this.dir.base}/data`)
@@ -278,6 +345,203 @@ export async function bootOrder () {
278
345
  freeze(this.config)
279
346
  }
280
347
 
348
+ /**
349
+ * Build configurations
350
+ *
351
+ * @async
352
+ */
353
+ export async function buildConfigs () {
354
+ this.bajo.log.debug('readConfigs')
355
+ for (const ns of this.getAllNs()) {
356
+ await this[ns].loadConfig()
357
+ this[ns].print = new Print(this[ns])
358
+ this.loadIntl(ns)
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Ensure for names and aliases to be unique and no clashes with other plugins
364
+ *
365
+ * @async
366
+ */
367
+ export async function checkNameAliases () {
368
+ this.bajo.log.debug('checkAliasNameClash')
369
+ const refs = []
370
+ for (const pkg of this.bajo.app.pluginPkgs) {
371
+ const plugin = this.bajo.app[camelCase(pkg)]
372
+ const { ns, alias } = plugin
373
+ let item = find(refs, { ns })
374
+ if (item) throw this.bajo.error('pluginNameClash%s%s%s%s', ns, pkg, item.ns, item.pkg, { code: 'BAJO_NAME_CLASH' })
375
+ item = find(refs, { alias })
376
+ if (item) throw this.bajo.error('pluginNameClash%s%s%s%s', alias, pkg, item.alias, item.pkg, { code: 'BAJO_ALIAS_CLASH' })
377
+ refs.push({ ns, alias, pkg })
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Ensure dependencies are met
383
+ *
384
+ * @async
385
+ */
386
+ export async function checkDependencies () {
387
+ const { join } = this.bajo
388
+ this.bajo.log.debug('checkDeps')
389
+ for (const pkg of this.bajo.app.pluginPkgs) {
390
+ const plugin = this.bajo.app[camelCase(pkg)]
391
+ const { ns, dependencies } = plugin
392
+ this.bajo.log.trace('- %s', ns)
393
+ const odep = reduce(dependencies, (o, k) => {
394
+ const item = map(k.split('@'), m => trim(m))
395
+ if (k[0] === '@') o['@' + item[1]] = item[2]
396
+ else o[item[0]] = item[1]
397
+ return o
398
+ }, {})
399
+ const deps = keys(odep)
400
+ if (deps.length > 0) {
401
+ if (intersection(this.bajo.app.pluginPkgs, deps).length !== deps.length) {
402
+ throw this.bajo.error('dependencyUnfulfilled%s%s', pkg, join(deps), { code: 'BAJO_DEPENDENCY' })
403
+ }
404
+ each(deps, d => {
405
+ if (!odep[d]) return
406
+ const ver = get(this.bajo.app[camelCase(d)], 'pkg.version')
407
+ if (!ver) return
408
+ if (!semver.satisfies(ver, odep[d])) {
409
+ throw this.bajo.error('semverCheckFailed%s%s', pkg, `${d}@${odep[d]}`, { code: 'BAJO_DEPENDENCY_SEMVER' })
410
+ }
411
+ })
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Collect and build hooks and push them to the bajo's hook system
418
+ *
419
+ * @async
420
+ * @fires bajo:afterCollectHooks
421
+ */
422
+ export async function collectHooks () {
423
+ const { eachPlugins, runHook, isLogInRange, importModule } = this.bajo
424
+ const { isArray, isPlainObject } = this.lib._
425
+ const me = this // "this" is "app"
426
+ me.bajo.log.trace('collecting%s', this.t('hooks'))
427
+ await eachPlugins(async function ({ dir, file }) {
428
+ let mod = await importModule(file, { asHandler: true })
429
+ if (!mod) return undefined
430
+ if (file.includes('hook.js')) mod = await mod.handler.call(this)
431
+ if (isArray(mod)) {
432
+ for (const m of mod) {
433
+ if (!isPlainObject(m)) continue
434
+ if (!m.name) throw me.bajo.error('missing%s%s', 'name', file)
435
+ if (isArray(m.name)) {
436
+ for (const name of m.name) {
437
+ me.bajo.hooks.push(merge({}, m, { name, src: this.ns }))
438
+ }
439
+ } else {
440
+ m.src = this.ns
441
+ me.bajo.hooks.push(m)
442
+ }
443
+ }
444
+ } else {
445
+ const _file = file.replace(dir + '/hook/', '').replace('.js', '')
446
+ let [names, path] = _file.split('@')
447
+ names = names.split('$').map(n => trim(n))
448
+ for (let name of names) {
449
+ name = name.split('.').map(n => camelCase(n)).join('.')
450
+ const m = merge({}, mod, { name: `${name}:${camelCase(path)}`, src: this.ns })
451
+ me.bajo.hooks.push(m)
452
+ }
453
+ }
454
+ }, { glob: ['hook/*.js', 'hook.js'], prefix: me.bajo.ns })
455
+ // for log trace purpose only
456
+ if (isLogInRange('trace')) {
457
+ const items = groupBy(me.bajo.hooks, item => item.name)
458
+ forOwn(items, (v, k) => {
459
+ const [name, path] = k.split(':')
460
+ me.bajo.log.trace('- %s:%s (%d)', name, path, v.length)
461
+ })
462
+ }
463
+
464
+ /**
465
+ * Run after hooks are collected
466
+ *
467
+ * @global
468
+ * @event bajo:afterCollectHooks
469
+ * @param {Object[]} hooks - Array of hook objects
470
+ * @see {@tutorial hook}
471
+ * @see module:Helper/Base.collectHooks
472
+ */
473
+ await runHook('bajo:afterCollectHooks', this.bajo.hooks)
474
+ me.bajo.log.debug('collected%s%d', this.t('hooks'), me.bajo.hooks.length)
475
+ }
476
+
477
+ /**
478
+ * Finally, run all plugins
479
+ *
480
+ * @async
481
+ * @fires bajo:beforeAll{method}
482
+ * @fires {ns}:before{method}
483
+ * @fires {ns}:after{method}
484
+ * @fires bajo:afterAll{method}
485
+ */
486
+ export async function run () {
487
+ const me = this
488
+ const { runHook, eachPlugins, join } = me.bajo
489
+ const { freeze } = me.lib
490
+ const methods = ['init']
491
+ if (!me.applet) methods.push('start')
492
+ for (const method of methods) {
493
+ /**
494
+ * Run before all ```{method}``` executed. Accepted ```{method}```: ```Init``` or ```Start```
495
+ *
496
+ * @global
497
+ * @event bajo:beforeAll{method}
498
+ * @param {string} method - Accepted methods: ```Init```, ```Start```
499
+ * @see module:Helper/Base.run
500
+ */
501
+ await runHook(`bajo:${camelCase(`before all ${method}`)}`)
502
+ await eachPlugins(async function () {
503
+ const { ns } = this
504
+ /**
505
+ * Run before ```{method}``` is executed within ```{ns}``` context
506
+ *
507
+ * - ```{ns}``` - namespace
508
+ * - ```{method}``` - Accepted methods: ```Init``` or ```Start```
509
+ *
510
+ * @global
511
+ * @event {ns}:before{method}
512
+ * @see module:Helper/Base.run
513
+ */
514
+ await runHook(`${ns}:${camelCase(`before ${method}`)}`)
515
+ await me[ns][method]()
516
+ /**
517
+ * Run after ```{method}``` is executed within ```{ns}``` context
518
+ *
519
+ * - ```{ns}``` - namespace
520
+ * - ```{method}``` - Accepted methods: ```Init``` or ```Start```
521
+ *
522
+ * @global
523
+ * @event {ns}:after{method}
524
+ * @see module:Helper/Base.run
525
+ */
526
+ await runHook(`${ns}:${camelCase(`after ${method}`)}`)
527
+ if (method === 'start') freeze(me[ns].config)
528
+ })
529
+ /**
530
+ * Run after all ```{method}``` executed. Accepted ```{method}```: ```Init``` or ```Start```
531
+ *
532
+ * @global
533
+ * @event bajo:afterAll{method}
534
+ * @see module:Helper/Base.run
535
+ */
536
+ await runHook(`bajo:${camelCase(`after all ${method}`)}`)
537
+ }
538
+ if (me.bajo.config.log.level === 'trace') {
539
+ let text = join(map(me.bajo.app.pluginPkgs, b => camelCase(b)))
540
+ text += ` (${me.bajo.app.pluginPkgs.length})`
541
+ me.bajo.log.trace('loadedPlugins%s', text)
542
+ } else me.bajo.log.debug('loadedPlugins%s', me.bajo.app.pluginPkgs.length)
543
+ }
544
+
281
545
  /**
282
546
  * Iterate through all plugins loaded and do:
283
547
  *
package/class/app.js CHANGED
@@ -1,16 +1,35 @@
1
1
  import util from 'util'
2
2
  import Bajo from './bajo.js'
3
3
  import Base from './base.js'
4
- import { runAsApplet } from './helper/bajo.js'
5
- import Cache from './app/cache.js'
6
- import Tools from './plugin/tools.js'
7
- import { outmatchNs, parseObject, lib } from './helper/app.js'
4
+ import Cache from './cache.js'
5
+ import Tools from './tools.js'
6
+ import Plugin from './plugin.js'
7
+ import { outmatchNs, parseObject, lib, runAsApplet } from './_helper.js'
8
8
  import { fileURLToPath } from 'url'
9
9
 
10
10
  const { camelCase, isPlainObject, get, reverse, map, last, without, set } = lib._
11
- const { pascalCase, getCallerFilename } = lib.aneka
11
+ const { pascalCase } = lib.aneka
12
12
  let unknownLangWarning = false
13
13
 
14
+ function getCallerFilename () {
15
+ const originalFunc = Error.prepareStackTrace
16
+ let callerfile
17
+
18
+ try {
19
+ const err = new Error()
20
+ Error.prepareStackTrace = (_, stack) => stack
21
+ const currentfile = err.stack.shift().getFileName()
22
+
23
+ while (err.stack.length) {
24
+ callerfile = err.stack.shift().getFileName()
25
+ if (currentfile !== callerfile) break
26
+ }
27
+ } catch (e) {}
28
+
29
+ Error.prepareStackTrace = originalFunc
30
+ return callerfile
31
+ }
32
+
14
33
  /**
15
34
  * @typedef {Object} TAppEnv
16
35
  * @property {string} dev=development
@@ -238,23 +257,132 @@ class App {
238
257
  }
239
258
 
240
259
  /**
241
- * Dumping variable on screen. Like ```console.log``` but with max 10 depth.
260
+ * Get loaded plugins
261
+ *
262
+ * @method
263
+ * @param {string[]} [nss] - Array of namespaces. If empty, it returns all loaded plugins
264
+ * @returns {TPlugin[]}
265
+ */
266
+ getPlugins = (nss) => {
267
+ const allNs = nss ?? this.getAllNs()
268
+ return allNs.map(ns => this[ns])
269
+ }
270
+
271
+ /**
272
+ * Get all plugins loaded plugins
273
+ *
274
+ * @method
275
+ * @returns {TPlugin[]}
276
+ */
277
+ getAllPlugins = () => {
278
+ return this.getPlugins()
279
+ }
280
+
281
+ /**
282
+ * Get plugin by name
283
+ *
284
+ * @method
285
+ * @param {string} name - Plugin name/namespace or alias
286
+ * @param {boolean} [silent] - If ```true```, silently return undefined even on error
287
+ * @returns {Object} Plugin object
288
+ */
289
+ getPlugin = (name, silent) => {
290
+ if (!this[name]) {
291
+ // alias?
292
+ let plugin
293
+ for (const key in this) {
294
+ const item = this[key]
295
+ if (item instanceof Plugin && (item.alias === name || item.pkgName === name)) {
296
+ plugin = item
297
+ break
298
+ }
299
+ }
300
+ if (!plugin) {
301
+ if (silent) return false
302
+ throw this.bajo.error('pluginWithNameAliasNotLoaded%s', name)
303
+ }
304
+ name = plugin.ns
305
+ }
306
+ return this[name]
307
+ }
308
+
309
+ /**
310
+ * Get plugin data directory
311
+ *
312
+ * @method
313
+ * @param {string} name - Plugin name (namespace) or alias
314
+ * @param {boolean} [ensureDir=true] - Set ```true``` (default) to ensure directory is existed
315
+ * @returns {string}
316
+ */
317
+ getPluginDataDir = (name, ensureDir = true) => {
318
+ const { fs } = this.lib
319
+ const plugin = this.getPlugin(name)
320
+ const dir = `${this.bajo.dir.data}/plugins/${plugin.ns}`
321
+ if (ensureDir) fs.ensureDirSync(dir)
322
+ return dir
323
+ }
324
+
325
+ /**
326
+ * Resolve file path from:
327
+ *
328
+ * - local/absolute file
329
+ * - TNsPath (```myPlugin:/path/to/file.txt```)
330
+ * - file under node_modules, e.g. ```myPlugin:node_modules/some-package/file.txt```
331
+ *
332
+ * @method
333
+ * @param {string} file - File path, see above for supported types
334
+ * @returns {string} Resolved file path
335
+ */
336
+ getPluginFile = (file) => {
337
+ const { currentLoc } = this.lib.aneka
338
+ const { fs } = this.lib
339
+ const { trim } = this.lib._
340
+ if (!this) return file
341
+ if (file[0] === '.') file = `${currentLoc(import.meta).dir}/${trim(file.slice(1), '/')}`
342
+ if (file.includes(':')) {
343
+ if (file.slice(1, 2) === ':') return file // windows fs
344
+ const { ns, path } = this.bajo.breakNsPath(file, false)
345
+ if (ns !== 'file' && this && this[ns] && ns.length > 1) {
346
+ file = `${this[ns].dir.pkg}${path}`
347
+ if (path.startsWith('node_modules/')) {
348
+ file = `${this[ns].dir.pkg}/${path}`
349
+ if (!fs.existsSync(file)) file = `${this[ns].dir.pkg}/../${path.slice('node_modules/'.length)}`
350
+ }
351
+ }
352
+ }
353
+ return file
354
+ }
355
+
356
+ /**
357
+ * Dumping variable on screen. Like ```console.log``` with configurable options. Useful for quick debugging and testing. You can also use it to dump variables in production without worrying about performance because it is using Bajo's built-in cache to store the result of util's inspect, so it will only be processed once for each unique variable.
358
+ *
359
+ * Any argument passed to this method will be displayed on screen.
360
+ * If the last argument is a boolean ```true```, app will quit rightaway after dumping.
361
+ *
362
+ * If you have ```bajoCli``` plugin installed, variables will be displayed in a nice box using ```boxen``` package.
363
+ * Otherwise, it will fallback to ```console.log``` with util's inspect result.
364
+ *
365
+ * To have more control on how the variable is displayed, you can set options in Bajo's config under ```dump``` key.
366
+ * See {@link Bajo#config} for details.
242
367
  *
243
368
  * @method
244
- * @param {...any} args - any arguments passed will be displayed on screen. If the last argument is a boolean 'true', app will quit rightaway
369
+ * @param {...any} args - Variables to dump
245
370
  */
246
371
  dump = (...args) => {
247
372
  let caller = getCallerFilename()
248
373
  caller = caller ? fileURLToPath(caller) : 'Unavailable'
249
- const terminate = last(args) === true
374
+ const opts = last(args)
375
+ const terminate = isPlainObject(opts) && opts.abort
250
376
  if (terminate) args.pop()
251
377
  const value = args.length === 1 ? args[0] : args
378
+ const options = { ...this.bajo.config.dump }
252
379
  if (this.boxen) {
253
- const result = util.inspect(value, { depth: 10, colors: true })
254
- const info = this.boxen(result, { title: `Caller: ${caller}`, titleAlignment: 'center', padding: 1, margin: 1, borderStyle: 'round' })
255
- console.log(info)
380
+ const result = util.inspect(value, options)
381
+ const opts = { ...this.bajo.config.dump.frame }
382
+ if (options.caller) opts.title = `Caller: ${caller}`
383
+ console.log(this.boxen(result, opts))
256
384
  } else {
257
- const result = util.inspect([caller, value], { depth: 10, colors: true })
385
+ const result = util.inspect(options.caller ? [caller, value] : value, options)
258
386
  console.log(result)
259
387
  }
260
388
  if (terminate) process.exit('1')
package/class/bajo.js CHANGED
@@ -1,3 +1,4 @@
1
+ import Tools from './tools.js'
1
2
  import Plugin from './plugin.js'
2
3
  import increment from 'add-filename-increment'
3
4
  import fs from 'fs-extra'
@@ -21,7 +22,7 @@ import {
21
22
  bootOrder,
22
23
  bootPlugins,
23
24
  exitHandler
24
- } from './helper/bajo.js'
25
+ } from './_helper.js'
25
26
 
26
27
  const require = createRequire(import.meta.url)
27
28
 
@@ -32,7 +33,7 @@ const {
32
33
  last, get, has, values, dropRight, pick
33
34
  } = lodash
34
35
 
35
- const { resolvePath, currentLoc } = aneka
36
+ const { resolvePath } = aneka
36
37
 
37
38
  /**
38
39
  * The Core. The main engine. The one and only plugin that control app's boot process and
@@ -171,7 +172,7 @@ class Bajo extends Plugin {
171
172
  [ns, subNs, subSubNs] = ns.split('.')
172
173
  if (checkNs) {
173
174
  if (!this.app[ns]) {
174
- const plugin = this.getPlugin(ns)
175
+ const plugin = this.app.getPlugin(ns)
175
176
  if (plugin) ns = plugin.ns
176
177
  }
177
178
  if (!this.app[ns]) throw this.error('unknownPluginOrNotLoaded%s')
@@ -297,7 +298,7 @@ class Bajo extends Plugin {
297
298
  callHandler = async (item, ...args) => {
298
299
  let result
299
300
  let scope = this
300
- if (item instanceof Plugin) {
301
+ if (item instanceof Tools || item instanceof Plugin) {
301
302
  scope = item
302
303
  item = args.shift()
303
304
  }
@@ -571,77 +572,6 @@ class Bajo extends Plugin {
571
572
  return resolvePath(path.dirname(dir))
572
573
  }
573
574
 
574
- /**
575
- * Get plugin data directory
576
- *
577
- * @method
578
- * @param {string} name - Plugin name (namespace) or alias
579
- * @param {boolean} [ensureDir=true] - Set ```true``` (default) to ensure directory is existed
580
- * @returns {string}
581
- */
582
- getPluginDataDir = (name, ensureDir = true) => {
583
- const plugin = this.getPlugin(name)
584
- const dir = `${this.app.bajo.dir.data}/plugins/${plugin.ns}`
585
- if (ensureDir) fs.ensureDirSync(dir)
586
- return dir
587
- }
588
-
589
- /**
590
- * Resolve file path from:
591
- *
592
- * - local/absolute file
593
- * - TNsPath (```myPlugin:/path/to/file.txt```)
594
- * - file under node_modules, e.g. ```myPlugin:node_modules/some-package/file.txt```
595
- *
596
- * @method
597
- * @param {string} file - File path, see above for supported types
598
- * @returns {string} Resolved file path
599
- */
600
- getPluginFile = (file) => {
601
- if (!this) return file
602
- if (file[0] === '.') file = `${currentLoc(import.meta).dir}/${trim(file.slice(1), '/')}`
603
- if (file.includes(':')) {
604
- if (file.slice(1, 2) === ':') return file // windows fs
605
- const { ns, path } = this.breakNsPath(file)
606
- if (ns !== 'file' && this && this.app && this.app[ns] && ns.length > 1) {
607
- file = `${this.app[ns].dir.pkg}${path}`
608
- if (path.startsWith('node_modules/')) {
609
- file = `${this.app[ns].dir.pkg}/${path}`
610
- if (!fs.existsSync(file)) file = `${this.app[ns].dir.pkg}/../${path.slice('node_modules/'.length)}`
611
- }
612
- }
613
- }
614
- return file
615
- }
616
-
617
- /**
618
- * Get plugin by name
619
- *
620
- * @method
621
- * @param {string} name - Plugin name/namespace or alias
622
- * @param {boolean} [silent] - If ```true```, silently return undefined even on error
623
- * @returns {Object} Plugin object
624
- */
625
- getPlugin = (name, silent) => {
626
- if (!this.app[name]) {
627
- // alias?
628
- let plugin
629
- for (const key in this.app) {
630
- const item = this.app[key]
631
- if (item instanceof Plugin && (item.alias === name || item.pkgName === name)) {
632
- plugin = item
633
- break
634
- }
635
- }
636
- if (!plugin) {
637
- if (silent) return false
638
- throw this.error('pluginWithNameAliasNotLoaded%s', name)
639
- }
640
- name = plugin.ns
641
- }
642
- return this.app[name]
643
- }
644
-
645
575
  /**
646
576
  * Import file/module from any loaded plugins.
647
577
  *
@@ -729,7 +659,7 @@ class Bajo extends Plugin {
729
659
  * @returns {boolean}
730
660
  */
731
661
  isEmptyDir = async (dir, filterFn) => {
732
- dir = resolvePath(this.getPluginFile(dir))
662
+ dir = resolvePath(this.app.getPluginFile(dir))
733
663
  await fs.exists(dir)
734
664
  return await emptyDir(dir, filterFn)
735
665
  }
@@ -915,7 +845,7 @@ class Bajo extends Plugin {
915
845
  await this.runHook('bajo:beforeReadConfig', file, options)
916
846
  parserOpts.readFromFile = true
917
847
  if (!ns) ns = this.ns
918
- file = resolvePath(this.getPluginFile(file))
848
+ file = resolvePath(this.app.getPluginFile(file))
919
849
  let ext = path.extname(file)
920
850
  const fname = path.dirname(file) + '/' + path.basename(file, ext)
921
851
  ext = ext.toLowerCase()
@@ -1064,8 +994,8 @@ class Bajo extends Plugin {
1064
994
  * @returns {string} Full file path
1065
995
  */
1066
996
  saveAsDownload = async (file, item, printSaved = true) => {
1067
- const { print, getPluginDataDir } = this.app.bajo
1068
- const fname = increment(`${getPluginDataDir(this.ns)}/download/${trim(file, '/')}`, { fs: true })
997
+ const { print } = this.app.bajo
998
+ const fname = increment(`${this.app.getPluginDataDir(this.ns)}/download/${trim(file, '/')}`, { fs: true })
1069
999
  const dir = path.dirname(fname)
1070
1000
  if (!fs.existsSync(dir)) fs.ensureDirSync(dir)
1071
1001
  await fs.writeFile(fname, item, 'utf8')
@@ -4,8 +4,7 @@ class Cache {
4
4
  }
5
5
 
6
6
  getRootDir = () => {
7
- const { getPluginDataDir } = this.app.bajo
8
- return `${getPluginDataDir('bajo')}/cache`
7
+ return `${this.app.getPluginDataDir('bajo')}/cache`
9
8
  }
10
9
 
11
10
  prep = (name, ttlDur = 0) => {
@@ -1,5 +1,5 @@
1
1
  import os from 'os'
2
- import logLevels from '../../lib/log-levels.js'
2
+ import logLevels from '../lib/log-levels.js'
3
3
  import chalk from 'chalk'
4
4
  import { stripVTControlCharacters } from 'node:util'
5
5
 
package/class/plugin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import lodash from 'lodash'
2
- import Err from './plugin/err.js'
2
+ import Err from './err.js'
3
3
 
4
4
  const { get, isEmpty, cloneDeep, omit, isPlainObject, camelCase } = lodash
5
5
 
@@ -43,7 +43,7 @@ async function importModule (file, { asDefaultImport = true, asHandler, noCache
43
43
  return imported
44
44
  }
45
45
 
46
- if (this) file = this.getPluginFile(file)
46
+ if (this) file = this.app.getPluginFile(file)
47
47
  if (!fs.existsSync(file)) return
48
48
  let mod = await load(file, asDefaultImport, noCache)
49
49
  if (!asHandler) return mod
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bajo",
3
- "version": "2.16.0",
3
+ "version": "2.18.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,16 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-06-12
4
+
5
+ - [2.18.0] Move plugin related methods to ```app```
6
+ - [2.18.0] Last argument of ```app.dump()``` serves as options if it a plain object with certain keys
7
+
8
+ ## 2026-06-03
9
+
10
+ - [2.17.0] Combine all helpers to ```_helper.js```
11
+ - [2.17.0] ```callHandler()``` now can accept an instance of ```Tools``` as its scope too
12
+ - [2.17.0] Bug fix in ```app.dump()```
13
+
3
14
  ## 2026-06-01
4
15
 
5
16
  - [2.16.0] Upgrade ```aneka@0.14.0```
@@ -1,73 +0,0 @@
1
- import aneka from 'aneka/index.js'
2
- import outmatch from 'outmatch'
3
- import lodash from 'lodash'
4
- import fs from 'fs-extra'
5
- import fastGlob from 'fast-glob'
6
- import { sprintf } from 'sprintf-js'
7
- import dayjs from 'dayjs'
8
- import utc from 'dayjs/plugin/utc.js'
9
- import customParseFormat from 'dayjs/plugin/customParseFormat.js'
10
- import localizedFormat from 'dayjs/plugin/localizedFormat.js'
11
- import weekOfYear from 'dayjs/plugin/weekOfYear.js'
12
- import freeze from '../../lib/freeze.js'
13
- import findDeep from '../../lib/find-deep.js'
14
- import omitDeep from 'omit-deep'
15
-
16
- dayjs.extend(utc)
17
- dayjs.extend(customParseFormat)
18
- dayjs.extend(localizedFormat)
19
- dayjs.extend(weekOfYear)
20
-
21
- /**
22
- * @typedef {Object} TAppLib
23
- * @property {Object} _ - Access to {@link https://lodash.com|lodash}
24
- * @property {Object} fs - Access to {@link https://github.com/jprichardson/node-fs-extra|fs-extra}
25
- * @property {Object} fastGlob - Access to {@link https://github.com/mrmlnc/fast-glob|fast-glob}
26
- * @property {Object} sprintf - Access to {@link https://github.com/alexei/sprintf.js|sprintf}
27
- * @property {Object} aneka - Access to {@link https://github.com/ardhi/aneka|aneka}
28
- * @property {Object} outmatch - Access to {@link https://github.com/axtgr/outmatch|outmatch}
29
- * @property {Object} dayjs - Access to {@link https://day.js.org|dayjs} with utc & customParseFormat plugin already applied
30
- * @property {Object} freeze
31
- * @property {Object} findDeep
32
- * @see App
33
- */
34
- export const lib = {
35
- _: lodash,
36
- fs,
37
- fastGlob,
38
- sprintf,
39
- outmatch,
40
- dayjs,
41
- aneka,
42
- freeze,
43
- findDeep,
44
- omitDeep
45
- }
46
-
47
- export function outmatchNs (source, pattern) {
48
- const { breakNsPath } = this.bajo
49
- const [src, subSrc] = source.split(':')
50
- if (!subSrc) return pattern === src
51
- try {
52
- const { fullNs, path } = breakNsPath(pattern)
53
- const isMatch = outmatch(path)
54
- return src === fullNs && isMatch(subSrc)
55
- } catch (err) {
56
- return false
57
- }
58
- }
59
-
60
- export function parseObject (obj, options = {}) {
61
- const me = this
62
- const { ns = 'bajo', lang } = options
63
- options.translator = {
64
- lang,
65
- prefix: 't:',
66
- handler: val => {
67
- const [text, ...args] = val.split('|')
68
- args.push({ lang })
69
- return me[ns].t(text, ...args)
70
- }
71
- }
72
- return aneka.parseObject(obj, options)
73
- }
@@ -1,222 +0,0 @@
1
- import semver from 'semver'
2
- import lodash from 'lodash'
3
- import Print from '../plugin/print.js'
4
-
5
- const {
6
- merge,
7
- forOwn,
8
- groupBy,
9
- find,
10
- reduce,
11
- map,
12
- trim,
13
- keys,
14
- intersection,
15
- each,
16
- camelCase,
17
- get
18
- } = lodash
19
-
20
- /**
21
- * Internal helpers called by Bajo & plugins that only used once for bootstrapping purpose.
22
- * It should remains hidden and not to be imported by any program.
23
- *
24
- * @module Helper/Base
25
- */
26
-
27
- /**
28
- * Build configurations
29
- *
30
- * @async
31
- */
32
- export async function buildConfigs () {
33
- this.bajo.log.debug('readConfigs')
34
- for (const ns of this.getAllNs()) {
35
- await this[ns].loadConfig()
36
- this[ns].print = new Print(this[ns])
37
- this.loadIntl(ns)
38
- }
39
- }
40
-
41
- /**
42
- * Ensure for names and aliases to be unique and no clashes with other plugins
43
- *
44
- * @async
45
- */
46
- export async function checkNameAliases () {
47
- this.bajo.log.debug('checkAliasNameClash')
48
- const refs = []
49
- for (const pkg of this.bajo.app.pluginPkgs) {
50
- const plugin = this.bajo.app[camelCase(pkg)]
51
- const { ns, alias } = plugin
52
- let item = find(refs, { ns })
53
- if (item) throw this.bajo.error('pluginNameClash%s%s%s%s', ns, pkg, item.ns, item.pkg, { code: 'BAJO_NAME_CLASH' })
54
- item = find(refs, { alias })
55
- if (item) throw this.bajo.error('pluginNameClash%s%s%s%s', alias, pkg, item.alias, item.pkg, { code: 'BAJO_ALIAS_CLASH' })
56
- refs.push({ ns, alias, pkg })
57
- }
58
- }
59
-
60
- /**
61
- * Ensure dependencies are met
62
- *
63
- * @async
64
- */
65
- export async function checkDependencies () {
66
- const { join } = this.bajo
67
- this.bajo.log.debug('checkDeps')
68
- for (const pkg of this.bajo.app.pluginPkgs) {
69
- const plugin = this.bajo.app[camelCase(pkg)]
70
- const { ns, dependencies } = plugin
71
- this.bajo.log.trace('- %s', ns)
72
- const odep = reduce(dependencies, (o, k) => {
73
- const item = map(k.split('@'), m => trim(m))
74
- if (k[0] === '@') o['@' + item[1]] = item[2]
75
- else o[item[0]] = item[1]
76
- return o
77
- }, {})
78
- const deps = keys(odep)
79
- if (deps.length > 0) {
80
- if (intersection(this.bajo.app.pluginPkgs, deps).length !== deps.length) {
81
- throw this.bajo.error('dependencyUnfulfilled%s%s', pkg, join(deps), { code: 'BAJO_DEPENDENCY' })
82
- }
83
- each(deps, d => {
84
- if (!odep[d]) return
85
- const ver = get(this.bajo.app[camelCase(d)], 'pkg.version')
86
- if (!ver) return
87
- if (!semver.satisfies(ver, odep[d])) {
88
- throw this.bajo.error('semverCheckFailed%s%s', pkg, `${d}@${odep[d]}`, { code: 'BAJO_DEPENDENCY_SEMVER' })
89
- }
90
- })
91
- }
92
- }
93
- }
94
-
95
- /**
96
- * Collect and build hooks and push them to the bajo's hook system
97
- *
98
- * @async
99
- * @fires bajo:afterCollectHooks
100
- */
101
- export async function collectHooks () {
102
- const { eachPlugins, runHook, isLogInRange, importModule } = this.bajo
103
- const { isArray, isPlainObject } = this.lib._
104
- const me = this // "this" is "app"
105
- me.bajo.log.trace('collecting%s', this.t('hooks'))
106
- await eachPlugins(async function ({ dir, file }) {
107
- let mod = await importModule(file, { asHandler: true })
108
- if (!mod) return undefined
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
- }
132
- }
133
- }, { glob: ['hook/*.js', 'hook.js'], prefix: me.bajo.ns })
134
- // for log trace purpose only
135
- if (isLogInRange('trace')) {
136
- const items = groupBy(me.bajo.hooks, item => item.name)
137
- forOwn(items, (v, k) => {
138
- const [name, path] = k.split(':')
139
- me.bajo.log.trace('- %s:%s (%d)', name, path, v.length)
140
- })
141
- }
142
-
143
- /**
144
- * Run after hooks are collected
145
- *
146
- * @global
147
- * @event bajo:afterCollectHooks
148
- * @param {Object[]} hooks - Array of hook objects
149
- * @see {@tutorial hook}
150
- * @see module:Helper/Base.collectHooks
151
- */
152
- await runHook('bajo:afterCollectHooks', this.bajo.hooks)
153
- me.bajo.log.debug('collected%s%d', this.t('hooks'), me.bajo.hooks.length)
154
- }
155
-
156
- /**
157
- * Finally, run all plugins
158
- *
159
- * @async
160
- * @fires bajo:beforeAll{method}
161
- * @fires {ns}:before{method}
162
- * @fires {ns}:after{method}
163
- * @fires bajo:afterAll{method}
164
- */
165
- export async function run () {
166
- const me = this
167
- const { runHook, eachPlugins, join } = me.bajo
168
- const { freeze } = me.lib
169
- const methods = ['init']
170
- if (!me.applet) methods.push('start')
171
- for (const method of methods) {
172
- /**
173
- * Run before all ```{method}``` executed. Accepted ```{method}```: ```Init``` or ```Start```
174
- *
175
- * @global
176
- * @event bajo:beforeAll{method}
177
- * @param {string} method - Accepted methods: ```Init```, ```Start```
178
- * @see module:Helper/Base.run
179
- */
180
- await runHook(`bajo:${camelCase(`before all ${method}`)}`)
181
- await eachPlugins(async function () {
182
- const { ns } = this
183
- /**
184
- * Run before ```{method}``` is executed within ```{ns}``` context
185
- *
186
- * - ```{ns}``` - namespace
187
- * - ```{method}``` - Accepted methods: ```Init``` or ```Start```
188
- *
189
- * @global
190
- * @event {ns}:before{method}
191
- * @see module:Helper/Base.run
192
- */
193
- await runHook(`${ns}:${camelCase(`before ${method}`)}`)
194
- await me[ns][method]()
195
- /**
196
- * Run after ```{method}``` is executed within ```{ns}``` context
197
- *
198
- * - ```{ns}``` - namespace
199
- * - ```{method}``` - Accepted methods: ```Init``` or ```Start```
200
- *
201
- * @global
202
- * @event {ns}:after{method}
203
- * @see module:Helper/Base.run
204
- */
205
- await runHook(`${ns}:${camelCase(`after ${method}`)}`)
206
- if (method === 'start') freeze(me[ns].config)
207
- })
208
- /**
209
- * Run after all ```{method}``` executed. Accepted ```{method}```: ```Init``` or ```Start```
210
- *
211
- * @global
212
- * @event bajo:afterAll{method}
213
- * @see module:Helper/Base.run
214
- */
215
- await runHook(`bajo:${camelCase(`after all ${method}`)}`)
216
- }
217
- if (me.bajo.config.log.level === 'trace') {
218
- let text = join(map(me.bajo.app.pluginPkgs, b => camelCase(b)))
219
- text += ` (${me.bajo.app.pluginPkgs.length})`
220
- me.bajo.log.trace('loadedPlugins%s', text)
221
- } else me.bajo.log.debug('loadedPlugins%s', me.bajo.app.pluginPkgs.length)
222
- }
File without changes
File without changes
File without changes