bajo 2.22.0 → 2.24.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.
Files changed (108) hide show
  1. package/.jsdoc.conf.json +2 -1
  2. package/class/app.js +220 -107
  3. package/class/bajo.js +258 -177
  4. package/class/base.js +85 -24
  5. package/class/cache.js +8 -16
  6. package/class/err.js +31 -10
  7. package/class/log.js +46 -38
  8. package/class/plugin.js +53 -30
  9. package/class/print.js +18 -15
  10. package/class/tools.js +6 -5
  11. package/docs/App.html +51 -1
  12. package/docs/Bajo.html +1 -1
  13. package/docs/Base.html +34 -1
  14. package/docs/Cache.html +1 -1
  15. package/docs/Err.html +2 -2
  16. package/docs/Log.html +2 -2
  17. package/docs/Main.html +3 -0
  18. package/docs/Plugin.html +1 -1
  19. package/docs/Print.html +1 -1
  20. package/docs/Tools.html +1 -1
  21. package/docs/class_app.js.html +222 -109
  22. package/docs/class_bajo.js.html +260 -179
  23. package/docs/class_base.js.html +87 -26
  24. package/docs/class_cache.js.html +10 -18
  25. package/docs/class_err.js.html +33 -12
  26. package/docs/class_log.js.html +48 -40
  27. package/docs/class_plugin.js.html +55 -32
  28. package/docs/class_print.js.html +20 -17
  29. package/docs/class_tools.js.html +8 -7
  30. package/docs/data/search.json +1 -1
  31. package/docs/extend_bajo_hook.js.html +289 -0
  32. package/docs/global.html +22 -1
  33. package/docs/index.html +1 -1
  34. package/docs/index.js.html +53 -16
  35. package/docs/lib_applet-hook.js.html +328 -0
  36. package/docs/lib_config.js.html +167 -0
  37. package/docs/lib_helper.js.html +186 -192
  38. package/docs/module-Applet.html +3 -0
  39. package/docs/module-Helper-Main.html +3 -0
  40. package/docs/module-Helper.html +1 -8
  41. package/docs/module-Hook.html +1 -1
  42. package/docs/tutorial-01_WELCOME.html +3 -0
  43. package/docs/tutorial-02_GETTING_STARTED.html +125 -0
  44. package/docs/tutorial-03_USER_GUIDE.html +54 -0
  45. package/docs/tutorial-04_DEV_GUIDE.html +178 -0
  46. package/docs/tutorial-05_ECOSYSTEM.html +3 -0
  47. package/extend/bajo/intl/en-US.json +2 -1
  48. package/extend/bajo/intl/id.json +2 -1
  49. package/index.js +51 -14
  50. package/lib/applet-hook.js +325 -0
  51. package/lib/config.js +164 -0
  52. package/lib/helper.js +184 -190
  53. package/package.json +6 -2
  54. package/test/00-global-hooks.test.js +44 -0
  55. package/test/e2e/_run.js +19 -0
  56. package/test/e2e/e2e-applet-process.test.js +66 -0
  57. package/test/e2e/e2e-boot-process.test.js +62 -0
  58. package/test/e2e/e2e-hooks-process.test.js +68 -0
  59. package/test/integration/_setup.js +55 -0
  60. package/test/integration/aspect-01-boot-instance.test.js +24 -0
  61. package/test/integration/aspect-02-plugin-loading.test.js +24 -0
  62. package/test/integration/aspect-03-config-merge.test.js +27 -0
  63. package/test/integration/aspect-04-intl-loading.test.js +29 -0
  64. package/test/integration/aspect-05-cache-roundtrip.test.js +26 -0
  65. package/test/integration/aspect-06-log-file-save.test.js +28 -0
  66. package/test/integration/aspect-07-plugin-data-dir.test.js +25 -0
  67. package/test/integration/aspect-08-plugin-lookup.test.js +25 -0
  68. package/test/integration/aspect-09-config-formats.test.js +26 -0
  69. package/test/integration/aspect-10-hooks.test.js +38 -0
  70. package/test/unit/app.test.js +115 -0
  71. package/test/unit/applet-hook.test.js +10 -0
  72. package/test/unit/bajo.test.js +151 -0
  73. package/test/unit/base.test.js +90 -0
  74. package/test/unit/cache.test.js +89 -0
  75. package/test/unit/config.test.js +14 -0
  76. package/test/unit/err.test.js +57 -0
  77. package/test/unit/helper.test.js +116 -0
  78. package/test/unit/index.test.js +40 -0
  79. package/test/unit/log.test.js +84 -0
  80. package/test/unit/plugin.test.js +95 -0
  81. package/test/unit/print.test.js +72 -0
  82. package/test/unit/tools.test.js +29 -0
  83. package/wiki/CHANGES.md +9 -0
  84. package/wiki/tutorials/01_WELCOME.md +18 -0
  85. package/wiki/tutorials/02_GETTING_STARTED.md +366 -0
  86. package/wiki/tutorials/03_USER_GUIDE.md +262 -0
  87. package/wiki/tutorials/04_DEV_GUIDE.md +386 -0
  88. package/wiki/tutorials/05_ECOSYSTEM.md +95 -0
  89. package/wiki/tutorials/tutorials.json +17 -0
  90. package/lib/hook.js +0 -228
  91. package/test/app.test.js +0 -183
  92. package/test/bajo.test.js +0 -124
  93. package/test/base.test.js +0 -75
  94. package/test/cache.test.js +0 -94
  95. package/test/e2e.test.js +0 -137
  96. package/test/err.test.js +0 -73
  97. package/test/helper.test.js +0 -39
  98. package/test/import-module.test.js +0 -138
  99. package/test/integration.test.js +0 -218
  100. package/test/log.test.js +0 -119
  101. package/test/plugin.test.js +0 -116
  102. package/test/print.test.js +0 -100
  103. package/test/tools.test.js +0 -38
  104. package/wiki/CONFIG.md +0 -36
  105. package/wiki/DEV_GUIDE.md +0 -3
  106. package/wiki/ECOSYSTEM.md +0 -93
  107. package/wiki/GETTING-STARTED.md +0 -356
  108. package/wiki/USER-GUIDE.md +0 -256
package/lib/helper.js CHANGED
@@ -7,12 +7,14 @@ import semver from 'semver'
7
7
  import aneka from 'aneka/index.js'
8
8
  import outmatch from 'outmatch'
9
9
  import fastGlob from 'fast-glob'
10
+ import * as readline from 'readline/promises'
10
11
  import { sprintf } from 'sprintf-js'
11
12
  import dayjs from 'dayjs'
12
13
  import utc from 'dayjs/plugin/utc.js'
13
14
  import customParseFormat from 'dayjs/plugin/customParseFormat.js'
14
15
  import localizedFormat from 'dayjs/plugin/localizedFormat.js'
15
16
  import weekOfYear from 'dayjs/plugin/weekOfYear.js'
17
+ import defConfig from './config.js'
16
18
  const { resolvePath } = aneka
17
19
  const {
18
20
  isFunction, isPlainObject, cloneDeep, merge, forOwn, groupBy, find, reduce, map,
@@ -21,74 +23,40 @@ const {
21
23
  } = lodash
22
24
 
23
25
  /**
26
+ * # Helper
27
+ *
24
28
  * Internal helpers called by Bajo and other classes. It should remains
25
- * hidden and not to be imported by any program.
29
+ * hidden and not to be imported by any program. It listed here for documentation purpose only.
26
30
  *
27
31
  * @module Helper
28
32
  */
29
33
 
30
34
  const omitted = ['spawn', 'cwd', 'name', 'alias', 'applet', 'a', 'plugins']
31
35
 
32
- const defConfig = {
33
- env: 'dev',
34
- runtime: {
35
- noWarning: false
36
- },
37
- log: {
38
- timeTaken: false,
39
- dateFormat: 'YYYY-MM-DDTHH:mm:ss.SSS',
40
- useUtc: false,
41
- pretty: false,
42
- applet: false,
43
- traceHook: false,
44
- save: false,
45
- rotation: {
46
- cycle: 'none', // none, daily, weekly, monthly
47
- compressOld: true,
48
- byPlugin: false,
49
- retain: 5
50
- }
51
- },
52
- dump: {
53
- depth: 2,
54
- compact: false,
55
- colors: true,
56
- breakLength: 80,
57
- caller: true,
58
- frame: {
59
- titleAllignment: 'center',
60
- padding: 1,
61
- margin: 1,
62
- borderStyle: 'round'
63
- }
64
- },
65
- lang: Intl.DateTimeFormat().resolvedOptions().lang ?? 'en-US',
66
- intl: {
67
- supported: ['en-US', 'id'],
68
- fallback: 'en-US',
69
- lookupOrder: [],
70
- format: {
71
- emptyValue: '',
72
- datetime: { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC' },
73
- date: { dateStyle: 'medium', timeZone: 'UTC' },
74
- time: { timeStyle: 'short', timeZone: 'UTC' },
75
- float: { maximumFractionDigits: 2 },
76
- double: { maximumFractionDigits: 5 },
77
- smallint: {},
78
- integer: {}
79
- },
80
- unitSys: {
81
- 'en-US': 'imperial',
82
- id: 'metric'
83
- }
84
- },
85
- exitHandler: true,
86
- cache: {
87
- purge: [],
88
- purgeIntvDur: '5m'
89
- }
90
- }
91
-
36
+ /**
37
+ * Main plugin class. This is the special plugin created by Bajo during the boot process, solely made
38
+ * for you to write your own code for your application. It behaves like any other plugin, with a few differences:
39
+ *
40
+ * - It always there, can't be disabled nor removed, reside in the app's root directory named `main`.
41
+ * If it's folder is missing, Bajo will create it during the boot process with the default template and settings.
42
+ * - You don't have to put this plugin in your `.plugin` file or `package.json` file,
43
+ * it is always present and always loaded as the **last** plugin to load.
44
+ * - You write your app the same way you would do it with plugin:
45
+ * - by creating properties & methods within your class
46
+ * - by extending other plugins inside `/main/extend/{pluginNs}` folder
47
+ * - by tapping hooks inside `/main/extend/bajo/hook` folder or `/main/extend/bajo/hook.js` file
48
+ * - you even have your own config file in `{dataDir}/config/main.{ext}`
49
+ *
50
+ * Why it has to be a plugin? Because Bajo is a plugin-based framework, and everything is a plugin.
51
+ * This brings some flexibilities, e.g. when someday you think your main plugin become too big,
52
+ * just turn it into a separate plugin and move it to its own package with almost no code changes at all.
53
+ *
54
+ * For more on this, see {@link Plugin} and {@link module:Hook}
55
+ *
56
+ * @class
57
+ * @global
58
+ * @name Main
59
+ */
92
60
  const defMain = `async function factory (pkgName) {
93
61
  const me = this
94
62
 
@@ -103,7 +71,19 @@ const defMain = `async function factory (pkgName) {
103
71
  export default factory
104
72
  `
105
73
 
106
- function outmatchNs (source, pattern) {
74
+ /**
75
+ * Outmacth with support for scoped source and pattern. Scoped source/pattern is a source/pattern that has a plugin's namespace prefix,
76
+ * separated by `:` symbol.
77
+ *
78
+ * This function is usefull for matching a source/pattern that is scoped to a specific plugin's namespace.
79
+ * For example, if you have a source `myPlugin:foo/bar` and a pattern `myPlugin:foo/*`,
80
+ * this function will return `true` because the source matches the pattern within the same namespace.
81
+ *
82
+ * @param {string} source - Scoped source to match against the pattern. It should be in the format of `ns:path`
83
+ * @param {string} pattern - Scoped pattern to match against the source. It should be in the format of `ns:path`
84
+ * @returns {boolean} Returns `true` if the source matches the pattern, otherwise `false`.
85
+ */
86
+ export function outmatchNs (source, pattern) {
107
87
  const { breakNsPath } = this.bajo
108
88
  const [src, subSrc] = source.split(':')
109
89
  if (!subSrc) return pattern === src
@@ -116,7 +96,20 @@ function outmatchNs (source, pattern) {
116
96
  }
117
97
  }
118
98
 
119
- function parseObject (obj, options = {}) {
99
+ /**
100
+ * Parse object and normalize their values.
101
+ *
102
+ * Support automatic value translation for keys with `t:` prefix. The keys are then
103
+ * converted to the one without the `t:` prefix, and the original key is removed from the object.
104
+ *
105
+ * See {@link https://ardhi.github.io/aneka/global.html#parseObject|aneka.parseObject()} for more details.
106
+ *
107
+ * @method
108
+ * @param {*} obj - The object to parse and normalize.
109
+ * @param {object} [options={}] - Options for parsing and translation.
110
+ * @returns {object} The parsed and normalized object.
111
+ */
112
+ export function parseObject (obj, options = {}) {
120
113
  const me = this
121
114
  const { ns = 'bajo', lang } = options
122
115
  options.translator = {
@@ -137,6 +130,15 @@ dayjs.extend(localizedFormat)
137
130
  dayjs.extend(weekOfYear)
138
131
 
139
132
  /**
133
+ * Most commonly used libraries by Bajo and its plugins. They are already imported and ready to use,
134
+ * so you don't have to import them again in your plugin.
135
+ *
136
+ * Example:
137
+ * ```javascript
138
+ * const { fs, dayjs } = this.app.lib
139
+ * fs.ensureDirSync('/path/to/dir')
140
+ * const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
141
+ * ```
140
142
  * @typedef {Object} TLib
141
143
  * @memberof App
142
144
  * @property {Object} _ Access to {@link https://lodash.com|lodash}.
@@ -146,8 +148,11 @@ dayjs.extend(weekOfYear)
146
148
  * @property {Object} aneka Access to {@link https://github.com/ardhi/aneka|aneka}.
147
149
  * @property {Object} outmatch Access to {@link https://github.com/axtgr/outmatch|outmatch}.
148
150
  * @property {Object} dayjs Access to {@link https://day.js.org|dayjs} with utc & customParseFormat plugin already applied.
149
- * @property {Object} freeze
150
- * @property {Object} findDeep
151
+ * @property {Object} freeze Freeze object. See {@link module:Helper.freeze|freeze} for more details.
152
+ * @property {Object} findDeep Deep file file in an array of files. See {@link module:Helper.findDeep|findDeep} for more details.
153
+ * @property {Object} outmatchNs Like outmatch, but support scoped source & pattern. See {@link module:Helper.outmatchNs|outmatchNs} for more details.
154
+ * @property {Object} parseObject Parse object and normalize their values. Also support translation. See {@link module:Helper.parseObject|parseObject} for more details.
155
+ * @property {Object} [anekaSpatial] Access to {@link https://ardhi.github.io/bajo-spatial|aneka-spatial} helpers if `bajoSpatial` plugin is loaded.
151
156
  * @see App
152
157
  */
153
158
  export const lib = {
@@ -164,27 +169,49 @@ export const lib = {
164
169
  parseObject
165
170
  }
166
171
 
172
+ export async function ask (question, ...args) {
173
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
174
+ let answer
175
+ try {
176
+ answer = await rl.question(this.t(question, ...args))
177
+ } catch (err) {
178
+ console.error(err)
179
+ } finally {
180
+ rl.close()
181
+ }
182
+ return answer
183
+ }
184
+
167
185
  /**
168
- * Building bajo base config. Mostly dealing with directory setups:
169
- * - determine base directory
170
- * - check whether data directory is valid. If not exist, create one inside app dir
171
- * - ensure data config directory is there
172
- * - ensure tmp dir is there
173
- * - read the list of plugins from ```.plugins``` file
186
+ * Build base configuration object for Bajo. This function is called by Bajo during the {@link Bajo#init|initialization process}:
187
+ * - it sets up the base directory,
188
+ * - it checks the validity of the data directory,
189
+ * - it ensures the temporary directory exists,
190
+ * - it reads the list of used plugins from the `package.json`, `.plugins` file or command line options.
174
191
  *
175
192
  * @async
176
193
  * @method
177
194
  */
178
195
  export async function buildBaseConfig () {
179
- // dirs
180
196
  const { defaultsDeep, textToArray, currentLoc, resolvePath } = this.app.lib.aneka
181
- this.config = defaultsDeep({}, this.app.argv._, this.app.envVars._)
197
+ // dirs
198
+ this.config = defaultsDeep({}, this.app.envVars._, this.app.argv._)
182
199
  set(this, 'dir.base', this.app.dir)
183
200
  const path = currentLoc(import.meta).dir + '/..'
184
201
  set(this, 'dir.pkg', resolvePath(path))
185
202
  if (get(this, 'config.dir.data')) set(this, 'dir.data', this.config.dir.data)
186
203
  if (!get(this, 'dir.data')) set(this, 'dir.data', `${this.dir.base}/data`)
187
204
  this.dir.data = resolvePath(this.dir.data)
205
+ if (!fs.existsSync(this.dir.data)) {
206
+ console.error("Data directory does not exist: '%s'", this.dir.data)
207
+ let answer
208
+ do {
209
+ answer = await ask.call(this, 'Create one now? (y/n) ', this.dir.data)
210
+ answer = answer.trim().toLowerCase()
211
+ } while (!['y', 'n'].includes(answer))
212
+ if (answer === 'n') this.app.exit(true, 'Aborted')
213
+ fs.ensureDirSync(this.dir.data)
214
+ }
188
215
  fs.ensureDirSync(`${this.dir.data}/config`)
189
216
  if (!this.dir.tmp) {
190
217
  this.dir.tmp = `${resolvePath(os.tmpdir())}/${this.ns}`
@@ -192,8 +219,8 @@ export async function buildBaseConfig () {
192
219
  }
193
220
  this.pkg = await this.getPkgInfo()
194
221
  let pluginPkgs = this.app.pluginPkgs
222
+ // collect list of plugins
195
223
  if (isEmpty(pluginPkgs)) {
196
- // collect list of plugins
197
224
  const mainPkg = await this.getPkgInfo(this.app.dir)
198
225
  pluginPkgs = get(mainPkg, 'bajo.plugins', [])
199
226
  if (isEmpty(pluginPkgs)) {
@@ -208,12 +235,12 @@ export async function buildBaseConfig () {
208
235
  }
209
236
 
210
237
  /**
211
- * Building all plugins:
212
- * - load from app's pluginPkgs
213
- * - iterate through the list and build related plugins
214
- * - making sure main plugin is there. If not, create from template
215
- * - attach these plugins to the app instance
238
+ * Load, build and sanitize all plugins defined in `package.json` or `.plugins` file, and add them to the app's plugin list.
239
+ * It will also make sure the `main` plugin is there too, if not, it will be created with the default template and settings.
216
240
  *
241
+ * Finally, it will loop through all loaded plugins and its config and language files.
242
+ *
243
+ * This function is called by Bajo during the {@link Bajo#init|initialization process}.
217
244
  * @async
218
245
  */
219
246
  export async function buildPlugins () {
@@ -241,12 +268,17 @@ export async function buildPlugins () {
241
268
  this.app.addPlugin(plugin, ClassDef)
242
269
  this.log.trace('- ' + pkg)
243
270
  }
271
+ // load all plugins' config & language files
272
+ for (const ns of this.app.getAllNs()) {
273
+ await this.app[ns].loadConfig()
274
+ this.app[ns].print = new Print(this.app[ns])
275
+ this.app.loadIntl(ns)
276
+ }
244
277
  this.log.debug('buildPluginsComplete')
245
278
  }
246
279
 
247
280
  /**
248
- * Collect all config handlers, including the one provided by plugins
249
- *
281
+ * Collect all plugins' config handlers and push them to the app's {@link App#configHandlers|configHandlers} array.
250
282
  * @async
251
283
  */
252
284
  export async function collectConfigHandlers () {
@@ -267,14 +299,17 @@ export async function collectConfigHandlers () {
267
299
  }
268
300
 
269
301
  /**
270
- * Bajo extra config:
271
- * - reading config file
272
- * - merge config with arguments & environments values
273
- * - Set environment (```dev``` or ```prod```)
302
+ * Building Bajo configuration object. Called after all config handlers are collected,
303
+ * this function will read config files written in any of config handlers' supported formats:
304
+ * - if runtime configuration object is provided (the one passed to the app constructor), it will be
305
+ * used as the base. Otherwise, files are read from the `{dataDir}/config`
306
+ * - then it will be merged with the configuration coming from the command line arguments and environment variables
307
+ * - doing some normalization and validation works, including setting the language, environment, log level, and cache purge settings
308
+ * - and setting up the app's log instance with the proper log level
274
309
  *
275
310
  * @async
276
311
  */
277
- export async function buildExtConfig () {
312
+ export async function buildConfig () {
278
313
  // config merging
279
314
  const { defaultsDeep, includes } = this.app.lib.aneka
280
315
  const { parseObject } = this.app.lib
@@ -320,7 +355,8 @@ export async function buildExtConfig () {
320
355
  }
321
356
 
322
357
  /**
323
- * Setup plugins boot orders by reading plugin's ```.bootorder``` file if provided.
358
+ * Determine the boot order of plugins based on their `bajo.bootorder` property in their `package.json` file.
359
+ * If not provided, the plugin will be booted in the order they are listed in the `package.json` file or `.plugins` file.
324
360
  *
325
361
  * @async
326
362
  */
@@ -348,50 +384,36 @@ export async function bootOrder () {
348
384
  }
349
385
 
350
386
  /**
351
- * Build configurations
352
- *
353
- * @async
354
- */
355
- export async function buildConfigs () {
356
- this.bajo.log.debug('readConfigs')
357
- for (const ns of this.getAllNs()) {
358
- await this[ns].loadConfig()
359
- this[ns].print = new Print(this[ns])
360
- this.loadIntl(ns)
361
- }
362
- }
363
-
364
- /**
365
- * Ensure for names and aliases to be unique and no clashes with other plugins
366
- *
387
+ * Ensure the uniqueness of all plugins' name and alias. If there is a clash, an error will be thrown.
367
388
  * @async
368
389
  */
369
390
  export async function checkNameAliases () {
370
- this.bajo.log.debug('checkAliasNameClash')
391
+ this.log.debug('checkAliasNameClash')
371
392
  const refs = []
372
- for (const pkg of this.bajo.app.pluginPkgs) {
373
- const plugin = this.bajo.app[camelCase(pkg)]
393
+ for (const pkg of this.app.pluginPkgs) {
394
+ const plugin = this.app[camelCase(pkg)]
374
395
  const { ns, alias } = plugin
375
396
  let item = find(refs, { ns })
376
- if (item) throw this.bajo.error('pluginNameClash%s%s%s%s', ns, pkg, item.ns, item.pkg, { code: 'BAJO_NAME_CLASH' })
397
+ if (item) throw this.error('pluginNameClash%s%s%s%s', ns, pkg, item.ns, item.pkg, { code: 'BAJO_NAME_CLASH' })
377
398
  item = find(refs, { alias })
378
- if (item) throw this.bajo.error('pluginNameClash%s%s%s%s', alias, pkg, item.alias, item.pkg, { code: 'BAJO_ALIAS_CLASH' })
399
+ if (item) throw this.error('pluginNameClash%s%s%s%s', alias, pkg, item.alias, item.pkg, { code: 'BAJO_ALIAS_CLASH' })
379
400
  refs.push({ ns, alias, pkg })
380
401
  }
381
402
  }
382
403
 
383
404
  /**
384
- * Ensure dependencies are met
405
+ * Ensure all plugins' dependencies are met. If there is a missing dependency, an error will be thrown.
385
406
  *
407
+ * Semver is also supported, so if a plugin requires a specific version of another plugin, it will be checked as well.
386
408
  * @async
387
409
  */
388
410
  export async function checkDependencies () {
389
- const { join } = this.bajo
390
- this.bajo.log.debug('checkDeps')
391
- for (const pkg of this.bajo.app.pluginPkgs) {
392
- const plugin = this.bajo.app[camelCase(pkg)]
411
+ const { join } = this
412
+ this.log.debug('checkDeps')
413
+ for (const pkg of this.app.pluginPkgs) {
414
+ const plugin = this.app[camelCase(pkg)]
393
415
  const { ns, dependencies } = plugin
394
- this.bajo.log.trace('- %s', ns)
416
+ this.log.trace('- %s', ns)
395
417
  const odep = reduce(dependencies, (o, k) => {
396
418
  const item = map(k.split('@'), m => trim(m))
397
419
  if (k[0] === '@') o['@' + item[1]] = item[2]
@@ -400,15 +422,15 @@ export async function checkDependencies () {
400
422
  }, {})
401
423
  const deps = keys(odep)
402
424
  if (deps.length > 0) {
403
- if (intersection(this.bajo.app.pluginPkgs, deps).length !== deps.length) {
404
- throw this.bajo.error('dependencyUnfulfilled%s%s', pkg, join(deps), { code: 'BAJO_DEPENDENCY' })
425
+ if (intersection(this.app.pluginPkgs, deps).length !== deps.length) {
426
+ throw this.error('dependencyUnfulfilled%s%s', pkg, join(deps), { code: 'BAJO_DEPENDENCY' })
405
427
  }
406
428
  each(deps, d => {
407
429
  if (!odep[d]) return
408
- const ver = get(this.bajo.app[camelCase(d)], 'pkg.version')
430
+ const ver = get(this.app[camelCase(d)], 'pkg.version')
409
431
  if (!ver) return
410
432
  if (!semver.satisfies(ver, odep[d])) {
411
- throw this.bajo.error('semverCheckFailed%s%s', pkg, `${d}@${odep[d]}`, { code: 'BAJO_DEPENDENCY_SEMVER' })
433
+ throw this.error('semverCheckFailed%s%s', pkg, `${d}@${odep[d]}`, { code: 'BAJO_DEPENDENCY_SEMVER' })
412
434
  }
413
435
  })
414
436
  }
@@ -416,15 +438,14 @@ export async function checkDependencies () {
416
438
  }
417
439
 
418
440
  /**
419
- * Collect and build hooks and push them to the bajo's hook system
420
- *
441
+ * Collect all plugins' hooks and push them to the app's {@link App#hooks|hooks} array.
421
442
  * @async
422
443
  */
423
444
  export async function collectHooks () {
424
- const { eachPlugins, runHook, isLogInRange, importModule } = this.bajo
425
- const { isArray, isPlainObject } = this.lib._
426
- const me = this // "this" is "app"
427
- me.bajo.log.trace('collecting%s', this.t('hooks'))
445
+ const { eachPlugins, runHook, isLogInRange, importModule } = this
446
+ const { isArray, isPlainObject } = this.app.lib._
447
+ const me = this // "this" is "bajo"
448
+ this.log.trace('collecting%s', this.t('hooks'))
428
449
  await eachPlugins(async function ({ dir, file }) {
429
450
  let mod = await importModule(file, { asHandler: true })
430
451
  if (!mod) return undefined
@@ -435,11 +456,11 @@ export async function collectHooks () {
435
456
  if (!m.name) throw me.bajo.error('missing%s%s', 'name', file)
436
457
  if (isArray(m.name)) {
437
458
  for (const name of m.name) {
438
- me.bajo.hooks.push(merge({}, m, { name, src: this.ns }))
459
+ me.hooks.push(merge({}, m, { name, src: this.ns }))
439
460
  }
440
461
  } else {
441
462
  m.src = this.ns
442
- me.bajo.hooks.push(m)
463
+ me.hooks.push(m)
443
464
  }
444
465
  }
445
466
  } else {
@@ -449,81 +470,55 @@ export async function collectHooks () {
449
470
  for (let name of names) {
450
471
  name = name.split('.').map(n => camelCase(n)).join('.')
451
472
  const m = merge({}, mod, { name: `${name}:${camelCase(path)}`, src: this.ns })
452
- me.bajo.hooks.push(m)
473
+ me.hooks.push(m)
453
474
  }
454
475
  }
455
- }, { glob: ['hook/*.js', 'hook.js'], prefix: me.bajo.ns })
476
+ }, { glob: ['hook/*.js', 'hook.js'], prefix: this.ns })
456
477
  // for log trace purpose only
457
478
  if (isLogInRange('trace')) {
458
- const items = groupBy(me.bajo.hooks, item => item.name)
479
+ const items = groupBy(this.hooks, item => item.name)
459
480
  forOwn(items, (v, k) => {
460
481
  const [name, path] = k.split(':')
461
- me.bajo.log.trace('- %s:%s (%d)', name, path, v.length)
482
+ this.log.trace('- %s:%s (%s)', name, path, v.length)
462
483
  })
463
484
  }
464
485
 
465
- await runHook('bajo:afterCollectHooks', this.bajo.hooks)
466
- me.bajo.log.debug('collected%s%d', this.t('hooks'), me.bajo.hooks.length)
486
+ await runHook('bajo:afterCollectHooks', this.hooks)
487
+ this.log.debug('collected%s%d', this.t('hooks'), this.hooks.length)
467
488
  }
468
489
 
469
490
  /**
470
- * Finally, run all plugins
491
+ * Run all loaded plugins' `init()` and `start()` methods in the order determined previously during boot process.
471
492
  *
472
493
  * @async
473
- * @see module:Hook~bajo:beforeAllInit
474
- * @see module:Hook~{ns}:beforeInit
475
- * @see module:Hook~{ns}:afterInit
476
- * @see module:Hook~bajo:afterAllInit
477
- * @see module:Hook~bajo:beforeAllStart
478
- * @see module:Hook~{ns}:beforeStart
479
- * @see module:Hook~{ns}:afterStart
480
- * @see module:Hook~bajo:afterAllStart
494
+ * @see module:Hook
481
495
  */
482
- export async function run () {
496
+ export async function runPlugins () {
483
497
  const me = this
484
- const { runHook, eachPlugins, join } = me.bajo
485
- const { freeze } = me.lib
498
+ const { runHook, eachPlugins, join } = this
499
+ const { freeze } = this.app.lib
486
500
  const methods = ['init']
487
- if (!me.applet) methods.push('start')
501
+ if (!this.app.applet) methods.push('start')
488
502
  for (const method of methods) {
489
503
  await runHook(`bajo:${camelCase(`before all ${method}`)}`)
490
504
  await eachPlugins(async function () {
491
505
  const { ns } = this
492
506
  await runHook(`${ns}:${camelCase(`before ${method}`)}`)
493
- await me[ns][method]()
507
+ await me.app[ns][method]()
494
508
  await runHook(`${ns}:${camelCase(`after ${method}`)}`)
495
- if (method === 'start') freeze(me[ns].config)
509
+ if (method === 'start') freeze(me.app[ns].config)
496
510
  })
497
511
  await runHook(`bajo:${camelCase(`after all ${method}`)}`)
498
512
  }
499
- if (me.bajo.config.log.level === 'trace') {
500
- let text = join(map(me.bajo.app.pluginPkgs, b => camelCase(b)))
501
- text += ` (${me.bajo.app.pluginPkgs.length})`
502
- me.bajo.log.trace('loadedPlugins%s', text)
503
- } else me.bajo.log.debug('loadedPlugins%s', me.bajo.app.pluginPkgs.length)
504
- }
505
-
506
- /**
507
- * Iterate through all plugins loaded and do:
508
- *
509
- * 1. {@link module:Helper/Base.buildConfigs|build configs}
510
- * 2. {@link module:Helper/Base.checkNameAliases|ensure names & aliases uniqueness}
511
- * 3. {@link module:Helper/Base.checkDependencies|ensure dependencies are met}
512
- * 4. {@link module:Helper/Base.collectHooks|collect hooks}
513
- * 5. {@link module:Helper/Base.run|run plugins}
514
- *
515
- * @async
516
- */
517
- export async function bootPlugins () {
518
- await buildConfigs.call(this.app)
519
- await checkNameAliases.call(this.app)
520
- await checkDependencies.call(this.app)
521
- await collectHooks.call(this.app)
522
- await run.call(this.app)
513
+ if (this.config.log.level === 'trace') {
514
+ let text = join(map(this.app.pluginPkgs, b => camelCase(b)))
515
+ text += ` (${this.app.pluginPkgs.length})`
516
+ this.log.trace('loadedPlugins%s', text)
517
+ } else this.log.debug('loadedPlugins%s', this.app.pluginPkgs.length)
523
518
  }
524
519
 
525
520
  /**
526
- * Attach plugins exit handlers and make sure the app shutdowns gracefully
521
+ * Attach plugins exit handlers to make sure when the app shuts down, all plugins shut down gracefully first
527
522
  *
528
523
  * @async
529
524
  */
@@ -531,15 +526,14 @@ export async function exitHandler () {
531
526
  if (!this.config.exitHandler) return
532
527
 
533
528
  async function exit (signal) {
534
- const { eachPlugins } = this
535
529
  if (signal) this.log.warn('signalReceived%s', signal)
536
- const me = this
537
- await eachPlugins(async function ({ ns }) {
530
+ const allNs = this.app.getAllNs().reverse()
531
+ for (const ns of allNs) {
538
532
  try {
539
- await this.exit()
533
+ await this.app[ns].exit()
540
534
  } catch (err) {}
541
- me.log.trace('exited%s', this.ns)
542
- })
535
+ this.log.trace('exited%s', ns)
536
+ }
543
537
  this.log.debug('appShutdown')
544
538
  process.exit(0)
545
539
  }
@@ -585,11 +579,10 @@ export async function exitHandler () {
585
579
  }
586
580
 
587
581
  /**
588
- * If app is in ```applet``` mode, this little helper should take care plugin's applet boot process
582
+ * If app is in `applet` mode, this little helper should take care plugin's applet boot process
589
583
  *
590
584
  * @async
591
- * @fires {ns}:beforeAppletRun
592
- * @fires {ns}:afterAppletRun
585
+ * @see module:Applet
593
586
  */
594
587
  export async function runAsApplet () {
595
588
  const { isString, map, find } = this.app.lib._
@@ -622,27 +615,27 @@ export async function runAsApplet () {
622
615
  * Import file/module from any loaded plugins.
623
616
  *
624
617
  * E.g. your plugin structure looks like this:
625
- * ```
618
+ * `
626
619
  * |- src
627
620
  * | |- lib
628
621
  * | | |- my-module.js
629
622
  * |- index.js
630
623
  * |- package.json
631
- * ```
624
+ * `
632
625
  *
633
- * And this is how to import ```my-module.js```:
634
- * ```javascript
626
+ * And this is how to import `my-module.js`:
627
+ * `javascript
635
628
  * const { importModule } = this.app.bajo
636
629
  * const myModule = await importModule('myPlugin:/src/lib/my-module.js')
637
- * ```
630
+ * `
638
631
  *
639
632
  * @method
640
633
  * @async
641
634
  * @param {TNsPathPairs} file File to import.
642
635
  * @param {Object} [options={}] Options.
643
- * @param {boolean} [options.asDefaultImport=true] If ```true``` (default), return default imported module.
644
- * @param {boolean} [options.asHandler] If ```true```, return as a {@link HandlerType|handler}.
645
- * @param {boolean} [options.noCache] If ```true```, always import as a fresh copy.
636
+ * @param {boolean} [options.asDefaultImport=true] If `true` (default), return default imported module.
637
+ * @param {boolean} [options.asHandler] If `true`, return as a {@link HandlerType|handler}.
638
+ * @param {boolean} [options.noCache] If `true`, always import as a fresh copy.
646
639
  * @returns {any}
647
640
  * @see Bajo#importModule
648
641
  */
@@ -658,6 +651,7 @@ export async function importModule (file, { asDefaultImport = true, asHandler, n
658
651
  if (this) file = this.app.getPluginFile(file)
659
652
  if (!fs.existsSync(file)) return
660
653
  let mod = await load(file, asDefaultImport, noCache)
654
+ if (!mod) return
661
655
  if (!asHandler) return mod
662
656
  if (isFunction(mod)) mod = { level: 999, handler: mod }
663
657
  if (!isPlainObject(mod)) {
@@ -672,7 +666,7 @@ export async function importModule (file, { asDefaultImport = true, asHandler, n
672
666
  *
673
667
  * @method
674
668
  * @param {Object} obj Object to freeze.
675
- * @param {boolean} [shallow=false] If ```false``` (default), deep freeze object.
669
+ * @param {boolean} [shallow=false] If `false` (default), deep freeze object.
676
670
  */
677
671
  export function freeze (obj, options = {}) {
678
672
  const { shallow = false, clone = false } = options
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "bajo",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "The ultimate framework for whipping up massive apps in no time",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "build-doc": "jsdoc -c .jsdoc.conf.json",
8
- "test": "mocha --recursive"
8
+ "test": "mocha --recursive",
9
+ "coverage": "c8 --all --include=class/**/*.js --include=lib/**/*.js --include=index.js --reporter=text --reporter=lcov mocha --recursive",
10
+ "coverage:html": "c8 report --reporter=html",
11
+ "coverage:check": "c8 --check-coverage --lines 90 --functions 80 --branches 55 --statements 90 mocha --recursive"
9
12
  },
10
13
  "type": "module",
11
14
  "repository": {
@@ -38,6 +41,7 @@
38
41
  "semver": "^7.8.5"
39
42
  },
40
43
  "devDependencies": {
44
+ "c8": "^12.0.0",
41
45
  "chai": "^6.2.1",
42
46
  "clean-jsdoc-theme": "^4.3.0",
43
47
  "docdash": "^2.0.2",