bajo 2.18.0 → 2.20.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 (74) hide show
  1. package/.github/workflows/test.yml +37 -0
  2. package/class/_helper.js +23 -7
  3. package/class/app.js +64 -47
  4. package/class/bajo.js +182 -138
  5. package/class/base.js +3 -3
  6. package/class/cache.js +60 -0
  7. package/class/err.js +14 -11
  8. package/class/log.js +41 -40
  9. package/class/plugin.js +35 -36
  10. package/class/print.js +54 -51
  11. package/class/tools.js +3 -4
  12. package/docs/App.html +7 -7
  13. package/docs/Bajo.html +2 -2
  14. package/docs/Base.html +1 -1
  15. package/docs/Cache.html +3 -0
  16. package/docs/Err.html +2 -2
  17. package/docs/Log.html +2 -2
  18. package/docs/Plugin.html +1 -1
  19. package/docs/Print.html +1 -1
  20. package/docs/Tools.html +3 -0
  21. package/docs/class__helper.js.html +694 -0
  22. package/docs/class_app.js.html +307 -149
  23. package/docs/class_bajo.js.html +316 -464
  24. package/docs/class_base.js.html +35 -32
  25. package/docs/class_cache.js.html +150 -0
  26. package/docs/class_err.js.html +144 -0
  27. package/docs/class_log.js.html +270 -0
  28. package/docs/class_plugin.js.html +98 -71
  29. package/docs/class_print.js.html +261 -0
  30. package/docs/class_tools.js.html +44 -0
  31. package/docs/data/search.json +1 -1
  32. package/docs/global.html +1 -4
  33. package/docs/index.html +1 -1
  34. package/docs/index.js.html +21 -14
  35. package/docs/lib_find-deep.js.html +27 -0
  36. package/docs/lib_formats.js.html +19 -19
  37. package/docs/lib_freeze.js.html +19 -0
  38. package/docs/lib_import-module.js.html +16 -14
  39. package/docs/lib_index.js.html +9 -0
  40. package/docs/lib_log-levels.js.html +2 -2
  41. package/docs/module-Helper.html +3 -0
  42. package/docs/module-Lib.html +3 -8
  43. package/docs/scripts/core.js +477 -476
  44. package/docs/scripts/resize.js +36 -36
  45. package/docs/scripts/search.js +105 -105
  46. package/docs/scripts/third-party/fuse.js +1 -1
  47. package/docs/scripts/third-party/hljs-line-num-original.js +285 -282
  48. package/docs/scripts/third-party/hljs-line-num.js +1 -1
  49. package/docs/scripts/third-party/hljs-original.js +1202 -1195
  50. package/docs/scripts/third-party/hljs.js +1 -1
  51. package/docs/scripts/third-party/popper.js +1 -1
  52. package/docs/scripts/third-party/tippy.js +1 -1
  53. package/docs/scripts/third-party/tocbot.js +509 -508
  54. package/index.js +8 -11
  55. package/lib/find-deep.js +3 -3
  56. package/lib/formats.js +17 -17
  57. package/lib/freeze.js +3 -3
  58. package/lib/import-module.js +8 -8
  59. package/package.json +3 -2
  60. package/test/app.test.js +183 -0
  61. package/test/bajo.test.js +125 -0
  62. package/test/base.test.js +74 -107
  63. package/test/cache.test.js +94 -0
  64. package/test/e2e.test.js +137 -0
  65. package/test/err.test.js +73 -0
  66. package/test/helper.test.js +39 -0
  67. package/test/import-module.test.js +138 -0
  68. package/test/integration.test.js +218 -0
  69. package/test/log.test.js +119 -0
  70. package/test/plugin.test.js +116 -0
  71. package/test/print.test.js +100 -0
  72. package/test/tools.test.js +38 -0
  73. package/wiki/CHANGES.md +12 -0
  74. package/.mocharc.json +0 -4
package/index.js CHANGED
@@ -12,20 +12,17 @@ shim()
12
12
  *
13
13
  * I recommend the second method for its portability.
14
14
  *
15
- * Example:
16
- * ```javascript
17
- * // index.js file. Your main package entry point
18
- * import bajo from 'bajo'
19
- * await bajo()
20
- * ```
21
- *
22
15
  * @global
23
16
  * @async
24
- * @param {Object} [options] - App options
25
- * @param {string} [options.cwd] - Set current working directory. Defaults to the script directory
26
- * @param {string[]} [options.plugins] - Array of plugins to load. If provided, it override the list in ```package.json``` and ```.plugins``` file
27
- * @param {Object} [options.config] - Plugin's config object. If provided, plugin configs will no longer be read from its config files
17
+ * @param {Object} [options] App options.
18
+ * @param {string} [options.cwd] Set current working directory. Defaults to the script directory.
19
+ * @param {string[]} [options.plugins] Array of plugins to load. If provided, it override the list in ```package.json``` and ```.plugins``` file.
20
+ * @param {Object} [options.config] Plugin's config object. If provided, plugin configs will no longer be read from its config files.
28
21
  * @returns {App}
22
+ * @example
23
+ * // index.js file. Your main package entry point
24
+ * import bajo from 'bajo'
25
+ * await bajo()
29
26
  */
30
27
  async function boot (options = {}) {
31
28
  if (!options.cwd) options.cwd = process.cwd()
package/lib/find-deep.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import fs from 'fs'
2
2
 
3
3
  /**
4
- * Find item deep in paths
4
+ * Find item deep in paths.
5
5
  *
6
6
  * @method
7
7
  * @memberof module:Lib
8
- * @param {string} item - Item to find
9
- * @param {Array} paths - Array of path to look for
8
+ * @param {string} item Item to find.
9
+ * @param {Array} paths Array of path to look for.
10
10
  * @returns {string}
11
11
  */
12
12
  function findDeep (item, paths) {
package/lib/formats.js CHANGED
@@ -1,30 +1,30 @@
1
1
  /**
2
- * Supported data types
2
+ * Supported data types.
3
3
  *
4
4
  * @typedef {Object} TBajoDataType
5
5
  * @type {Array}
6
- * @property {string} 0 - string
7
- * @property {string} 1 - float
8
- * @property {string} 2 - double
9
- * @property {string} 3 - integer
10
- * @property {string} 4 - smallint
11
- * @property {string} 5 - date
12
- * @property {string} 6 - time
13
- * @property {string} 7 - datetime
14
- * @property {string} 8 - array
15
- * @property {string} 9 - object
16
- * @property {string} 10 - auto
6
+ * @property {string} 0 string
7
+ * @property {string} 1 float
8
+ * @property {string} 2 double
9
+ * @property {string} 3 integer
10
+ * @property {string} 4 smallint
11
+ * @property {string} 5 date
12
+ * @property {string} 6 time
13
+ * @property {string} 7 datetime
14
+ * @property {string} 8 array
15
+ * @property {string} 9 object
16
+ * @property {string} 10 auto
17
17
  */
18
18
 
19
19
  /**
20
- * General format types
20
+ * General format types,
21
21
  *
22
22
  * @typedef {Object} TBajoFormatType
23
23
  * @type {Array}
24
- * @property {string} 0 - speed
25
- * @property {string} 1 - distance
26
- * @property {string} 3 - area
27
- * @property {string} 4 - degree
24
+ * @property {string} 0 speed
25
+ * @property {string} 1 distance
26
+ * @property {string} 2 area
27
+ * @property {string} 3 degree
28
28
  */
29
29
  export const types = ['speed', 'distance', 'area', 'degree']
30
30
 
package/lib/freeze.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import deepFreezeStrict from 'deep-freeze-strict'
2
2
 
3
3
  /**
4
- * Freeze object
4
+ * Freeze object.
5
5
  *
6
6
  * @method
7
7
  * @memberof module:Lib
8
- * @param {Object} obj - Object to freeze
9
- * @param {boolean} [shallow=false] - If ```false``` (default), deep freeze object
8
+ * @param {Object} obj Object to freeze.
9
+ * @param {boolean} [shallow=false] If ```false``` (default), deep freeze object.
10
10
  */
11
11
  function freeze (obj, shallow = false) {
12
12
  if (shallow) Object.freeze(obj)
@@ -6,9 +6,9 @@ const { resolvePath } = aneka
6
6
  const { isFunction, isPlainObject } = lodash
7
7
 
8
8
  /**
9
- * Import file/module from any loaded plugins
9
+ * Import file/module from any loaded plugins.
10
10
  *
11
- * Example: your plugin structure looks like this
11
+ * E.g. your plugin structure looks like this:
12
12
  * ```
13
13
  * |- src
14
14
  * | |- lib
@@ -17,7 +17,7 @@ const { isFunction, isPlainObject } = lodash
17
17
  * |- package.json
18
18
  * ```
19
19
  *
20
- * And now this is how to import ```my-module.js```:
20
+ * And this is how to import ```my-module.js```:
21
21
  * ```javascript
22
22
  * const { importModule } = this.app.bajo
23
23
  * const myModule = await importModule('myPlugin:/src/lib/my-module.js')
@@ -26,11 +26,11 @@ const { isFunction, isPlainObject } = lodash
26
26
  * @method
27
27
  * @async
28
28
  * @memberof module:Lib
29
- * @param {TNsPathPairs} file - File to import
30
- * @param {Object} [options={}] - Options
31
- * @param {boolean} [options.asDefaultImport=true] - If ```true``` (default), return default imported module
32
- * @param {boolean} [options.asHandler] - If ```true```, return as a {@link HandlerType|handler}
33
- * @param {boolean} [options.noCache] - If ```true```, always import as a fresh copy
29
+ * @param {TNsPathPairs} file File to import.
30
+ * @param {Object} [options={}] Options.
31
+ * @param {boolean} [options.asDefaultImport=true] If ```true``` (default), return default imported module.
32
+ * @param {boolean} [options.asHandler] If ```true```, return as a {@link HandlerType|handler}.
33
+ * @param {boolean} [options.noCache] If ```true```, always import as a fresh copy.
34
34
  * @returns {any}
35
35
  * @see Bajo#importModule
36
36
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bajo",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "description": "The ultimate framework for whipping up massive apps in no time",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -44,6 +44,7 @@
44
44
  "devDependencies": {
45
45
  "chai": "^6.2.1",
46
46
  "clean-jsdoc-theme": "^4.3.0",
47
- "docdash": "^2.0.2"
47
+ "docdash": "^2.0.2",
48
+ "mocha": "^11.7.6"
48
49
  }
49
50
  }
@@ -0,0 +1,183 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { expect } from 'chai'
6
+ import fs from 'fs-extra'
7
+
8
+ import App from '../class/app.js'
9
+ import Plugin from '../class/plugin.js'
10
+
11
+ const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'bajo-app-test-'))
12
+
13
+ describe('App', () => {
14
+ let rootDir
15
+ let app
16
+
17
+ beforeEach(() => {
18
+ rootDir = createTempRoot()
19
+ app = new App({ cwd: rootDir })
20
+ })
21
+
22
+ afterEach(() => {
23
+ if (rootDir) fs.rmSync(rootDir, { recursive: true, force: true })
24
+ })
25
+
26
+ it('adds plugins and base class mappings', () => {
27
+ const plugin = new Plugin('my-plugin', app)
28
+ const MyBase = class {}
29
+
30
+ app.addPlugin(plugin, MyBase)
31
+
32
+ expect(app.myPlugin).to.equal(plugin)
33
+ expect(app.baseClass.MyPlugin).to.equal(MyBase)
34
+ })
35
+
36
+ it('throws when adding the same plugin namespace twice', () => {
37
+ const plugin = new Plugin('dup-plugin', app)
38
+ app.addPlugin(plugin)
39
+
40
+ expect(() => app.addPlugin(plugin)).to.throw("Plugin 'dupPlugin' added already")
41
+ })
42
+
43
+ it('lists namespaces and loaded plugins', () => {
44
+ app.pluginPkgs = ['alpha-plugin', 'beta-plugin']
45
+ app.alphaPlugin = { ns: 'alphaPlugin' }
46
+ app.betaPlugin = { ns: 'betaPlugin' }
47
+
48
+ expect(app.getAllNs()).to.deep.equal(['alphaPlugin', 'betaPlugin'])
49
+ expect(app.getPlugins()).to.deep.equal([app.alphaPlugin, app.betaPlugin])
50
+ expect(app.getPlugins(['betaPlugin'])).to.deep.equal([app.betaPlugin])
51
+ expect(app.getAllPlugins()).to.deep.equal([app.alphaPlugin, app.betaPlugin])
52
+ })
53
+
54
+ it('gets plugin by namespace, alias, and package name', () => {
55
+ const plugin = new Plugin('my-plugin', app)
56
+ plugin.alias = 'mine'
57
+ app.addPlugin(plugin)
58
+ app.bajo = { error: (code, value) => new Error(`${code}:${value}`) }
59
+
60
+ expect(app.getPlugin('myPlugin')).to.equal(plugin)
61
+ expect(app.getPlugin('mine')).to.equal(plugin)
62
+ expect(app.getPlugin('my-plugin')).to.equal(plugin)
63
+ })
64
+
65
+ it('supports silent and throwing behavior for missing plugin lookup', () => {
66
+ app.bajo = { error: (code, value) => new Error(`${code}:${value}`) }
67
+
68
+ expect(app.getPlugin('missing', true)).to.equal(false)
69
+ expect(() => app.getPlugin('missing')).to.throw('pluginWithNameAliasNotLoaded%s:missing')
70
+ })
71
+
72
+ it('creates and returns plugin data directory', () => {
73
+ const plugin = new Plugin('my-plugin', app)
74
+ app.addPlugin(plugin)
75
+ app.bajo = { dir: { data: path.join(rootDir, 'data') } }
76
+
77
+ const dir = app.getPluginDataDir('myPlugin')
78
+
79
+ expect(dir).to.equal(path.join(rootDir, 'data', 'plugins', 'myPlugin'))
80
+ expect(fs.existsSync(dir)).to.equal(true)
81
+ })
82
+
83
+ it('resolves plugin-scoped files and node_modules fallback paths', async () => {
84
+ const plugin = new Plugin('my-plugin', app)
85
+ plugin.dir = { pkg: path.join(rootDir, 'plugins', 'my-plugin') }
86
+ await fs.ensureDir(plugin.dir.pkg)
87
+ app.addPlugin(plugin)
88
+
89
+ app.bajo = {
90
+ breakNsPath: () => ({ ns: 'myPlugin', path: '/src/feature.js' })
91
+ }
92
+ expect(app.getPluginFile('myPlugin:/src/feature.js')).to.equal(path.join(plugin.dir.pkg, 'src/feature.js'))
93
+
94
+ const fallbackFile = path.join(rootDir, 'plugins', 'dep', 'index.js')
95
+ await fs.ensureDir(path.dirname(fallbackFile))
96
+ await fs.writeFile(fallbackFile, 'export default 1', 'utf8')
97
+ app.bajo.breakNsPath = () => ({ ns: 'myPlugin', path: 'node_modules/dep/index.js' })
98
+
99
+ expect(app.getPluginFile('myPlugin:node_modules/dep/index.js')).to.equal(`${plugin.dir.pkg}/../dep/index.js`)
100
+ })
101
+
102
+ it('translates and checks translation existence', () => {
103
+ app.pluginPkgs = ['my-plugin']
104
+ app.myPlugin = {
105
+ intl: {
106
+ id: {
107
+ greet: 'Halo %s',
108
+ list: 'Daftar: %s'
109
+ },
110
+ en: {
111
+ greet: 'Hello %s'
112
+ }
113
+ }
114
+ }
115
+ app.bajo = {
116
+ config: {
117
+ lang: 'id',
118
+ intl: {
119
+ fallback: 'en',
120
+ supported: ['id', 'en']
121
+ }
122
+ },
123
+ join: (items) => items.join(', '),
124
+ log: { warn: () => {} }
125
+ }
126
+
127
+ expect(app.t('myPlugin', 'greet', 'Ardhi')).to.equal('Halo Ardhi')
128
+ expect(app.t('myPlugin', 'list', ['A', 'B'])).to.equal('Daftar: A, B')
129
+ expect(app.t('myPlugin', 'unknown %s', 'X')).to.equal('unknown X')
130
+ expect(app.te('myPlugin', 'greet')).to.equal(true)
131
+ expect(app.te('myPlugin', 'not.exists')).to.equal(false)
132
+ })
133
+
134
+ it('returns supported config formats', () => {
135
+ app.configHandlers = [{ ext: '.json' }, { ext: '.yaml' }]
136
+
137
+ expect(app.getConfigFormats()).to.deep.equal(['.json', '.yaml'])
138
+ })
139
+
140
+ it('starts plugin by namespace with arguments', () => {
141
+ const calls = []
142
+ app.sample = {
143
+ start: (...args) => {
144
+ calls.push(args)
145
+ }
146
+ }
147
+
148
+ app.startPlugin('sample', 1, 'x')
149
+
150
+ expect(calls).to.deep.equal([[1, 'x']])
151
+ })
152
+
153
+ it('kills process with signal and exits abruptly when true is passed', () => {
154
+ const originalKill = process.kill
155
+ const originalExit = process.exit
156
+ const killCalls = []
157
+ let exitArg
158
+
159
+ process.kill = (...args) => {
160
+ killCalls.push(args)
161
+ return true
162
+ }
163
+ process.exit = (arg) => {
164
+ exitArg = arg
165
+ throw new Error('__EXIT__')
166
+ }
167
+
168
+ try {
169
+ app.exit('SIGTERM')
170
+ try {
171
+ app.exit(true)
172
+ } catch (err) {
173
+ if (err.message !== '__EXIT__') throw err
174
+ }
175
+ } finally {
176
+ process.kill = originalKill
177
+ process.exit = originalExit
178
+ }
179
+
180
+ expect(killCalls).to.deep.equal([[process.pid, 'SIGTERM']])
181
+ expect(exitArg).to.equal('1')
182
+ })
183
+ })
@@ -0,0 +1,125 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { expect } from 'chai'
6
+ import fs from 'fs-extra'
7
+
8
+ import App from '../class/app.js'
9
+ import Bajo from '../class/bajo.js'
10
+
11
+ const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'bajo-core-test-'))
12
+
13
+ describe('Bajo', () => {
14
+ let root
15
+ let app
16
+ let bajo
17
+
18
+ beforeEach(() => {
19
+ root = createTempRoot()
20
+ app = new App({ cwd: root })
21
+ bajo = new Bajo(app)
22
+ app.bajo = bajo
23
+ app.main = { dir: { pkg: root } }
24
+ app.mainNs = 'main'
25
+ app.main = { pkgName: 'main', dir: { pkg: root } }
26
+ app.demo = {
27
+ dir: { pkg: path.join(root, 'demo') },
28
+ greet: (name) => `hi ${name}`
29
+ }
30
+ bajo.config = {
31
+ env: 'dev',
32
+ lang: 'en-US',
33
+ intl: {
34
+ unitSys: { 'en-US': 'metric' },
35
+ format: {
36
+ emptyValue: '-',
37
+ datetime: { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC' },
38
+ date: { dateStyle: 'medium', timeZone: 'UTC' },
39
+ time: { timeStyle: 'short', timeZone: 'UTC' },
40
+ float: { maximumFractionDigits: 2 },
41
+ double: { maximumFractionDigits: 2 },
42
+ integer: {},
43
+ smallint: {}
44
+ }
45
+ },
46
+ log: {
47
+ level: 'trace'
48
+ }
49
+ }
50
+ bajo.t = (text) => text
51
+ })
52
+
53
+ afterEach(() => {
54
+ if (root) fs.rmSync(root, { recursive: true, force: true })
55
+ })
56
+
57
+ it('builds and breaks namespace paths', () => {
58
+ const built = bajo.buildNsPath({ ns: 'demo', subNs: 'api', subSubNs: 'v1', path: '/users' })
59
+ const broken = bajo.breakNsPath('demo.api:/users/:id|42?q=x')
60
+
61
+ expect(built).to.equal('demo.api.v1:/users')
62
+ expect(broken).to.include({ ns: 'demo', subNs: 'api', path: '/users/:id', realPath: '/users/42' })
63
+ expect(broken.qs).to.deep.equal({ q: 'x' })
64
+ })
65
+
66
+ it('breaks ns path details from filename', () => {
67
+ const info = bajo.breakNsPathFromFile({
68
+ file: 'x/extend/route/demo.api@user-list.js',
69
+ dir: 'x/extend/',
70
+ suffix: '',
71
+ ns: 'demo',
72
+ getType: true
73
+ })
74
+
75
+ expect(info).to.include({ ns: 'demo', subNs: 'api', path: 'userList', type: 'route' })
76
+ })
77
+
78
+ it('returns unit format and formats values', () => {
79
+ const unit = bajo.getUnitFormat({ lang: 'en-US' })
80
+ const joined = bajo.format(['a', 'b'], 'array')
81
+ const integer = bajo.format(12345, 'integer')
82
+
83
+ expect(unit.unitSys).to.equal('metric')
84
+ expect(unit.format).to.be.an('object')
85
+ expect(joined).to.equal('a, b')
86
+ expect(integer).to.be.a('string')
87
+ })
88
+
89
+ it('gets method by ns path and supports non-throw mode', () => {
90
+ const fn = bajo.getMethod('demo:greet')
91
+ const miss = bajo.getMethod('demo:notFound', false)
92
+
93
+ expect(fn('a')).to.equal('hi a')
94
+ expect(miss).to.equal(undefined)
95
+ })
96
+
97
+ it('validates app/plugin package and utility helpers', async () => {
98
+ const appPkg = path.join(root, 'appdir')
99
+ const pluginPkg = path.join(root, 'plugindir')
100
+ await fs.ensureDir(appPkg)
101
+ await fs.ensureDir(pluginPkg)
102
+ await fs.ensureDir(path.join(root, 'empty'))
103
+ await fs.writeJson(path.join(appPkg, 'package.json'), { bajo: { type: 'app' } })
104
+ await fs.writeJson(path.join(pluginPkg, 'package.json'), { bajo: { type: 'plugin' } })
105
+
106
+ expect(bajo.isValidApp(appPkg)).to.equal(true)
107
+ expect(bajo.isValidPlugin(pluginPkg)).to.equal(true)
108
+ expect(bajo.join(['a', 'b', 'c'])).to.equal('a, b and c')
109
+ expect(bajo.numUnit('10mb', 'kb')).to.equal('10mb')
110
+ expect(await bajo.isEmptyDir(path.join(root, 'empty'))).to.equal(true)
111
+ })
112
+
113
+ it('reads/writes json helpers', async () => {
114
+ const file = path.join(root, 'x.json')
115
+ const content = bajo.toJson({ a: 1 })
116
+ await fs.writeFile(file, content, 'utf8')
117
+
118
+ expect(bajo.fromJson(content)).to.deep.equal({ a: 1 })
119
+ expect(bajo.readJson(file)).to.deep.equal({ a: 1 })
120
+
121
+ const out = path.join(root, 'out.json')
122
+ bajo.toJson({ b: 2 }, { writeToFile: true, saveAsFile: out })
123
+ expect(fs.existsSync(out)).to.equal(true)
124
+ })
125
+ })
package/test/base.test.js CHANGED
@@ -1,108 +1,75 @@
1
- import { expect } from 'chai'
2
- import Base from '../class/base.js'
3
-
4
- function makeMockApp ({ mainNs = 'app-main', throwOnDataConfig = false } = {}) {
5
- // small lodash-like helpers used by Base.loadConfig
6
- const _ = {
7
- get: (obj, path, defVal) => {
8
- if (!obj) return defVal
9
- const parts = String(path).split('.')
10
- let cur = obj
11
- for (const p of parts) {
12
- if (cur == null) return defVal
13
- cur = cur[p]
14
- }
15
- return cur === undefined ? defVal : cur
16
- },
17
- kebabCase: s => String(s).replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase(),
18
- keys: obj => obj ? Object.keys(obj) : [],
19
- pick: (obj, keys) => {
20
- const out = {}
21
- if (!obj) return out
22
- for (const k of keys) if (k in obj) out[k] = obj[k]
23
- return out
24
- }
25
- }
26
-
27
- // simple defaultsDeep-ish helper suitable for these tests:
28
- // later objects in args take precedence over earlier ones (shallow)
29
- const defaultsDeep = (...objs) => Object.assign({}, ...objs.filter(Boolean))
30
-
31
- // bajo helpers (file reads etc). readAllConfigs will be called twice by Base.loadConfig:
32
- // - once for module dir config
33
- // - once for data dir config (may throw)
34
- let readAllCalls = 0
35
- const readAllConfigs = async (path) => {
36
- readAllCalls += 1
37
- if (throwOnDataConfig && readAllCalls === 2) throw new Error('readAllConfigs data dir error')
38
- // return a small config object to test merge/path logic
39
- return { alpha: 'file', title: 'FromFile', other: 'ignore-me' }
40
- }
41
-
42
- return {
43
- mainNs,
44
- lib: { aneka: { defaultsDeep }, _ },
45
- bajo: {
46
- dir: { base: '/base', data: '/data' },
47
- readAllConfigs,
48
- readJson: async (p) => ({ name: 'bajo-foo', version: '1.2.3', description: 'pkg' }),
49
- parseObject: (o) => o,
50
- getModuleDir: (pkgName) => `/modules/${pkgName}`,
51
- log: { trace: () => {} }
52
- },
53
- env: {},
54
- argv: {}
55
- }
56
- }
57
-
58
- describe('Base.loadConfig', () => {
59
- it('derives alias from pkgName starting with "bajo-" using kebabCase', async () => {
60
- const app = makeMockApp()
61
- const inst = new Base('bajo-MyPlugin', app)
62
- // ensure namespace used by loadConfig
63
- inst.ns = 'myplugin'
64
- // set an initial config shape so defKeys isn't empty
65
- inst.config = { alpha: 'default', title: 'Default' }
66
-
67
- await inst.loadConfig()
68
-
69
- // alias is stored on the constructor
70
- expect(inst.constructor.alias).to.equal('my-plugin')
71
- // dir pkg should be set according to mainNs mismatch
72
- expect(inst.dir.pkg).to.equal('/modules/bajo-MyPlugin')
73
- // config should keep keys only from defKeys (alpha & title), parseObject ran (no throw)
74
- expect(Object.keys(inst.config)).to.include('alpha')
75
- expect(Object.keys(inst.config)).to.include('title')
76
- })
77
-
78
- it('when ns === app.mainNs uses app.mainNs as alias and sets title to alias', async () => {
79
- const app = makeMockApp({ mainNs: 'mainns' })
80
- const inst = new Base('bajo-mainpkg', app)
81
- inst.ns = 'mainns'
82
- inst.config = { title: undefined } // start with no title
83
-
84
- await inst.loadConfig()
85
-
86
- expect(inst.constructor.alias).to.equal('mainns')
87
- // title should be set to alias when ns is mainNs
88
- expect(inst.title).to.equal(inst.constructor.alias)
89
- })
90
-
91
- it('continues when readAllConfigs for data dir throws (tolerant behavior)', async () => {
92
- const app = makeMockApp({ throwOnDataConfig: true })
93
- const inst = new Base('bajo-throwtest', app)
94
- inst.ns = 'throwtest'
95
- inst.config = { alpha: 'start' }
96
-
97
- // should not throw even if second readAllConfigs throws
98
- let threw = false
99
- try {
100
- await inst.loadConfig()
101
- } catch (err) {
102
- threw = true
103
- }
104
- expect(threw).to.equal(false)
105
- // config should still be an object and title present (or undefined but not crash)
106
- expect(inst.config).to.be.an('object')
107
- })
1
+ /* global describe, it, beforeEach */
2
+
3
+ import { expect } from 'chai'
4
+ import lodash from 'lodash'
5
+
6
+ import Base from '../class/base.js'
7
+
8
+ describe('Base', () => {
9
+ let app
10
+
11
+ beforeEach(() => {
12
+ app = {
13
+ mainNs: 'main',
14
+ getAllNs: () => ['alpha', 'beta'],
15
+ lib: {
16
+ _: lodash,
17
+ aneka: {
18
+ defaultsDeep: (...objs) => lodash.defaultsDeep(...objs)
19
+ },
20
+ parseObject: (obj) => obj
21
+ },
22
+ bajo: {
23
+ dir: { base: '/app', data: '/data' },
24
+ config: { env: 'dev' },
25
+ log: { trace: () => {} },
26
+ getModuleDir: () => '/modules/my-plugin',
27
+ readAllConfigs: async () => ({ fromFile: true })
28
+ },
29
+ env: {
30
+ myPlugin: { x: 3 }
31
+ },
32
+ argv: {
33
+ myPlugin: { y: 2 }
34
+ },
35
+ options: {
36
+ config: {
37
+ myPlugin: { z: 1 }
38
+ }
39
+ }
40
+ }
41
+ })
42
+
43
+ it('initializes defaults', () => {
44
+ const base = new Base('my-plugin', app)
45
+
46
+ expect(base.ns).to.equal('myPlugin')
47
+ expect(base.dependencies).to.deep.equal([])
48
+ expect(base.state).to.deep.equal({})
49
+ expect(base.pkg).to.deep.equal({})
50
+ })
51
+
52
+ it('loads config and sets package/data directories', async () => {
53
+ const base = new Base('my-plugin', app)
54
+ base.config = { title: 'my app', z: 9, x: 0, y: 0 }
55
+
56
+ await base.loadConfig()
57
+
58
+ expect(base.dir).to.deep.equal({
59
+ pkg: '/modules/my-plugin',
60
+ data: '/data/plugins/myPlugin'
61
+ })
62
+ expect(base.getConfig('x')).to.equal(3)
63
+ expect(base.getConfig('y')).to.equal(2)
64
+ expect(base.getConfig('z')).to.equal(1)
65
+ expect(base.getConfig('title')).to.equal('my app')
66
+ })
67
+
68
+ it('runs no-op lifecycle methods and exits by disposing', async () => {
69
+ const base = new Base('my-plugin', app)
70
+
71
+ await base.init()
72
+ await base.start()
73
+ await base.stop()
74
+ })
108
75
  })