bajo 2.23.0 → 2.25.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.
- package/.jsdoc.conf.json +2 -1
- package/class/app.js +220 -107
- package/class/bajo.js +257 -177
- package/class/base.js +85 -24
- package/class/cache.js +8 -16
- package/class/err.js +31 -10
- package/class/log.js +46 -38
- package/class/plugin.js +53 -30
- package/class/print.js +18 -15
- package/class/tools.js +17 -7
- package/docs/App.html +51 -1
- package/docs/Bajo.html +1 -1
- package/docs/Base.html +34 -1
- package/docs/Cache.html +1 -1
- package/docs/Err.html +2 -2
- package/docs/Log.html +2 -2
- package/docs/Main.html +3 -0
- package/docs/Plugin.html +1 -1
- package/docs/Print.html +1 -1
- package/docs/Tools.html +1 -1
- package/docs/class_app.js.html +222 -109
- package/docs/class_bajo.js.html +260 -179
- package/docs/class_base.js.html +87 -26
- package/docs/class_cache.js.html +10 -18
- package/docs/class_err.js.html +33 -12
- package/docs/class_log.js.html +48 -40
- package/docs/class_plugin.js.html +55 -32
- package/docs/class_print.js.html +20 -17
- package/docs/class_tools.js.html +19 -9
- package/docs/data/search.json +1 -1
- package/docs/global.html +22 -1
- package/docs/index.html +1 -1
- package/docs/index.js.html +53 -16
- package/docs/lib_applet-hook.js.html +328 -0
- package/docs/lib_config.js.html +167 -0
- package/docs/lib_helper.js.html +186 -192
- package/docs/module-Applet.html +3 -0
- package/docs/module-Helper.html +1 -8
- package/docs/module-Hook.html +1 -1
- package/docs/tutorial-01_WELCOME.html +3 -0
- package/docs/tutorial-02_GETTING_STARTED.html +125 -0
- package/docs/tutorial-03_USER_GUIDE.html +54 -0
- package/docs/tutorial-04_DEV_GUIDE.html +178 -0
- package/docs/tutorial-05_ECOSYSTEM.html +3 -0
- package/extend/bajo/intl/en-US.json +2 -1
- package/extend/bajo/intl/id.json +2 -1
- package/index.js +51 -14
- package/lib/applet-hook.js +325 -0
- package/lib/config.js +164 -0
- package/lib/helper.js +185 -191
- package/package.json +6 -2
- package/test/00-global-hooks.test.js +44 -0
- package/test/e2e/_run.js +19 -0
- package/test/e2e/e2e-applet-process.test.js +66 -0
- package/test/e2e/e2e-boot-process.test.js +62 -0
- package/test/e2e/e2e-hooks-process.test.js +68 -0
- package/test/integration/_setup.js +55 -0
- package/test/integration/aspect-01-boot-instance.test.js +24 -0
- package/test/integration/aspect-02-plugin-loading.test.js +24 -0
- package/test/integration/aspect-03-config-merge.test.js +27 -0
- package/test/integration/aspect-04-intl-loading.test.js +29 -0
- package/test/integration/aspect-05-cache-roundtrip.test.js +26 -0
- package/test/integration/aspect-06-log-file-save.test.js +28 -0
- package/test/integration/aspect-07-plugin-data-dir.test.js +25 -0
- package/test/integration/aspect-08-plugin-lookup.test.js +25 -0
- package/test/integration/aspect-09-config-formats.test.js +26 -0
- package/test/integration/aspect-10-hooks.test.js +38 -0
- package/test/unit/app.test.js +115 -0
- package/test/unit/applet-hook.test.js +10 -0
- package/test/unit/bajo.test.js +151 -0
- package/test/unit/base.test.js +90 -0
- package/test/unit/cache.test.js +89 -0
- package/test/unit/config.test.js +14 -0
- package/test/unit/err.test.js +57 -0
- package/test/unit/helper.test.js +116 -0
- package/test/unit/index.test.js +40 -0
- package/test/unit/log.test.js +84 -0
- package/test/unit/plugin.test.js +95 -0
- package/test/unit/print.test.js +72 -0
- package/test/unit/tools.test.js +29 -0
- package/wiki/CHANGES.md +10 -0
- package/wiki/tutorials/01_WELCOME.md +18 -0
- package/wiki/tutorials/02_GETTING_STARTED.md +366 -0
- package/wiki/tutorials/03_USER_GUIDE.md +262 -0
- package/wiki/tutorials/04_DEV_GUIDE.md +386 -0
- package/wiki/tutorials/05_ECOSYSTEM.md +95 -0
- package/wiki/tutorials/tutorials.json +17 -0
- package/docs/lib_hook.js.html +0 -231
- package/docs/module-Lib.html +0 -3
- package/lib/hook.js +0 -228
- package/test/app.test.js +0 -183
- package/test/bajo.test.js +0 -124
- package/test/base.test.js +0 -75
- package/test/cache.test.js +0 -94
- package/test/e2e.test.js +0 -137
- package/test/err.test.js +0 -73
- package/test/helper.test.js +0 -39
- package/test/import-module.test.js +0 -138
- package/test/integration.test.js +0 -218
- package/test/log.test.js +0 -119
- package/test/plugin.test.js +0 -116
- package/test/print.test.js +0 -100
- package/test/tools.test.js +0 -38
- package/wiki/CONFIG.md +0 -36
- package/wiki/DEV_GUIDE.md +0 -3
- package/wiki/ECOSYSTEM.md +0 -93
- package/wiki/GETTING-STARTED.md +0 -356
- 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
169
|
-
* -
|
|
170
|
-
* -
|
|
171
|
-
* -
|
|
172
|
-
* -
|
|
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
|
-
|
|
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
|
-
*
|
|
212
|
-
*
|
|
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
|
|
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
|
|
271
|
-
*
|
|
272
|
-
* -
|
|
273
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
391
|
+
this.log.debug('checkAliasNameClash')
|
|
371
392
|
const refs = []
|
|
372
|
-
for (const pkg of this.
|
|
373
|
-
const plugin = this.
|
|
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.
|
|
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.
|
|
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
|
|
390
|
-
this.
|
|
391
|
-
for (const pkg of this.
|
|
392
|
-
const plugin = this.
|
|
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.
|
|
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.
|
|
404
|
-
throw this.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
425
|
-
const { isArray, isPlainObject } = this.lib._
|
|
426
|
-
const me = this // "this" is "
|
|
427
|
-
|
|
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.
|
|
459
|
+
me.hooks.push(merge({}, m, { name, src: this.ns }))
|
|
439
460
|
}
|
|
440
461
|
} else {
|
|
441
462
|
m.src = this.ns
|
|
442
|
-
me.
|
|
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.
|
|
473
|
+
me.hooks.push(m)
|
|
453
474
|
}
|
|
454
475
|
}
|
|
455
|
-
}, { glob: ['hook/*.js', 'hook.js'], prefix:
|
|
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(
|
|
479
|
+
const items = groupBy(this.hooks, item => item.name)
|
|
459
480
|
forOwn(items, (v, k) => {
|
|
460
481
|
const [name, path] = k.split(':')
|
|
461
|
-
|
|
482
|
+
this.log.trace('- %s:%s (%s)', name, path, v.length)
|
|
462
483
|
})
|
|
463
484
|
}
|
|
464
485
|
|
|
465
|
-
await runHook('bajo:afterCollectHooks', this.
|
|
466
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
496
|
+
export async function runPlugins () {
|
|
483
497
|
const me = this
|
|
484
|
-
const { runHook, eachPlugins, join } =
|
|
485
|
-
const { freeze } =
|
|
498
|
+
const { runHook, eachPlugins, join } = this
|
|
499
|
+
const { freeze } = this.app.lib
|
|
486
500
|
const methods = ['init']
|
|
487
|
-
if (!
|
|
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 (
|
|
500
|
-
let text = join(map(
|
|
501
|
-
text += ` (${
|
|
502
|
-
|
|
503
|
-
} else
|
|
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
|
|
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
|
|
537
|
-
|
|
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
|
-
|
|
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
|
|
582
|
+
* If app is in `applet` mode, this little helper should take care plugin's applet boot process
|
|
589
583
|
*
|
|
590
584
|
* @async
|
|
591
|
-
* @
|
|
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
|
|
634
|
-
*
|
|
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
|
|
644
|
-
* @param {boolean} [options.asHandler] If
|
|
645
|
-
* @param {boolean} [options.noCache] If
|
|
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
|
|
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
|
|
@@ -680,7 +674,7 @@ export function freeze (obj, options = {}) {
|
|
|
680
674
|
else deepFreeze(obj, clone)
|
|
681
675
|
}
|
|
682
676
|
|
|
683
|
-
//
|
|
677
|
+
// taken from https://github.com/3imed-jaberi/deepfreeze/blob/master/index.js
|
|
684
678
|
export function deepFreeze (object, clone = false) {
|
|
685
679
|
function _deepFreeze (_object) {
|
|
686
680
|
if (Object.isFrozen(_object)) return _object
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bajo",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.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",
|