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
@@ -0,0 +1,115 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import path from 'node:path'
4
+ import fs from 'fs-extra'
5
+ import { expect } from 'chai'
6
+ import App from '../../class/app.js'
7
+ import Plugin from '../../class/plugin.js'
8
+
9
+ describe('app (unit)', () => {
10
+ let root
11
+ let app
12
+
13
+ beforeEach(() => {
14
+ root = fs.mkdtempSync(path.join('/tmp', 'bajo-app-unit-'))
15
+ app = new App({ cwd: root })
16
+ })
17
+
18
+ afterEach(() => {
19
+ if (root) fs.rmSync(root, { recursive: true, force: true })
20
+ })
21
+
22
+ it('addPlugin and getAllNs/getPlugins/getAllPlugins work', () => {
23
+ const p = new Plugin('x-plugin', app)
24
+ app.addPlugin(p)
25
+ app.pluginPkgs = ['x-plugin']
26
+ expect(app.getAllNs()).to.deep.equal(['xPlugin'])
27
+ expect(app.getPlugins()).to.deep.equal([p])
28
+ expect(app.getAllPlugins()).to.deep.equal([p])
29
+ })
30
+
31
+ it('getPlugin resolves by ns, alias and package name', () => {
32
+ const p = new Plugin('x-plugin', app)
33
+ p.alias = 'xp'
34
+ app.addPlugin(p)
35
+ app.bajo = { error: (msg, v) => new Error(`${msg}:${v}`) }
36
+ expect(app.getPlugin('xPlugin')).to.equal(p)
37
+ expect(app.getPlugin('xp')).to.equal(p)
38
+ expect(app.getPlugin('x-plugin')).to.equal(p)
39
+ })
40
+
41
+ it('getPlugin supports silent misses and throws on non-silent miss', () => {
42
+ app.bajo = { error: (msg, v) => new Error(`${msg}:${v}`) }
43
+ expect(app.getPlugin('none', true)).to.equal(undefined)
44
+ expect(() => app.getPlugin('none')).to.throw()
45
+ })
46
+
47
+ it('getPluginDataDir creates plugin data folder', () => {
48
+ const p = new Plugin('x-plugin', app)
49
+ app.addPlugin(p)
50
+ app.bajo = { dir: { data: path.join(root, 'data') }, error: () => new Error('x') }
51
+ const d = app.getPluginDataDir('xPlugin')
52
+ expect(fs.existsSync(d)).to.equal(true)
53
+ })
54
+
55
+ it('getPluginFile resolves scoped plugin paths', async () => {
56
+ const p = new Plugin('x-plugin', app)
57
+ p.dir = { pkg: path.join(root, 'plugins', 'x-plugin') }
58
+ await fs.ensureDir(p.dir.pkg)
59
+ app.addPlugin(p)
60
+ app.bajo = { breakNsPath: () => ({ ns: 'xPlugin', path: '/a/b.js' }) }
61
+ const f = app.getPluginFile('xPlugin:/a/b.js')
62
+ expect(f).to.equal(path.join(p.dir.pkg, 'a/b.js'))
63
+ })
64
+
65
+ it('dump works in both plain and boxen modes', () => {
66
+ app.bajo = { config: { dump: { depth: 1, compact: true, colors: false, breakLength: 80, caller: false, frame: {} } } }
67
+ app.dump({ a: 1 })
68
+ app.boxen = (text) => text
69
+ app.dump({ b: 2 })
70
+ })
71
+
72
+ it('exit handles signal and hard exit', () => {
73
+ const oldKill = process.kill
74
+ const oldExit = process.exit
75
+ const calls = []
76
+ let ex
77
+ process.kill = (...args) => { calls.push(args); return true }
78
+ process.exit = (arg) => { ex = arg; throw new Error('__EXIT__') }
79
+ try {
80
+ app.exit('SIGTERM')
81
+ try { app.exit(true) } catch (err) {}
82
+ } finally {
83
+ process.kill = oldKill
84
+ process.exit = oldExit
85
+ }
86
+ expect(calls[0][1]).to.equal('SIGTERM')
87
+ expect(ex).to.equal('1')
88
+ })
89
+
90
+ it('loadIntl/t/te/getConfigFormats/startPlugin/stopPlugin work', async () => {
91
+ const p = new Plugin('x-plugin', app)
92
+ p.dir = { pkg: path.join(root, 'plugins', 'x-plugin') }
93
+ await fs.ensureDir(path.join(p.dir.pkg, 'extend', 'bajo', 'intl'))
94
+ await fs.writeJson(path.join(p.dir.pkg, 'extend', 'bajo', 'intl', 'en.json'), { hi: 'Hi %s' })
95
+ p.start = () => {}
96
+ app.addPlugin(p)
97
+ app.pluginPkgs = ['x-plugin']
98
+ app.bajo = {
99
+ config: { lang: 'en', intl: { supported: ['en'], fallback: 'en' }, log: { level: 'silent' } },
100
+ join: (a) => a.join(', '),
101
+ log: { warn: () => {} }
102
+ }
103
+ app.configHandlers = [{ ext: '.json' }, { ext: '.yml' }]
104
+
105
+ app.loadIntl('xPlugin')
106
+ expect(app.xPlugin.intl.en.hi).to.equal('Hi %s')
107
+ expect(app.t('xPlugin', 'hi', 'Joe')).to.equal('Hi Joe')
108
+ expect(app.te('xPlugin', 'hi')).to.equal(true)
109
+ expect(app.getConfigFormats()).to.deep.equal(['.json', '.yml'])
110
+ expect(app.getConfigFormats(true)).to.deep.equal(['json', 'yml'])
111
+
112
+ app.startPlugin('xPlugin', 'a')
113
+ app.stopPlugin('xPlugin', 'a')
114
+ })
115
+ })
@@ -0,0 +1,10 @@
1
+ /* global describe, it */
2
+
3
+ import { expect } from 'chai'
4
+
5
+ describe('applet-hook module (unit)', () => {
6
+ it('is importable documentation-only module', async () => {
7
+ const mod = await import('../../lib/applet-hook.js')
8
+ expect(mod).to.not.equal(undefined)
9
+ })
10
+ })
@@ -0,0 +1,151 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import path from 'node:path'
4
+ import fs from 'fs-extra'
5
+ import lodash from 'lodash'
6
+ import { expect } from 'chai'
7
+ import App from '../../class/app.js'
8
+ import Bajo from '../../class/bajo.js'
9
+ import Base from '../../class/base.js'
10
+
11
+ describe('bajo (unit)', () => {
12
+ let root
13
+ let app
14
+ let bajo
15
+
16
+ beforeEach(async () => {
17
+ root = fs.mkdtempSync(path.join('/tmp', 'bajo-bajo-unit-'))
18
+ await fs.ensureDir(path.join(root, 'data', 'config'))
19
+ await fs.writeJson(path.join(root, 'package.json'), { name: 'app', type: 'module' })
20
+ app = new App({ cwd: root })
21
+ app.lib._ = lodash
22
+ bajo = new Bajo(app)
23
+ app.bajo = bajo
24
+ bajo.dir = { base: root, pkg: root, data: path.join(root, 'data') }
25
+ bajo.config = {
26
+ env: 'dev',
27
+ lang: 'en-US',
28
+ intl: { supported: ['en-US'], fallback: 'en-US', unitSys: { 'en-US': 'metric' }, format: { emptyValue: '', integer: {}, float: {}, double: {}, datetime: {}, date: {}, time: {} } },
29
+ log: { level: 'trace', save: false, pretty: false, useUtc: false, timeTaken: false, dateFormat: 'YYYY-MM-DD', rotation: { cycle: 'none', byPlugin: false } },
30
+ cache: { purgeIntvDur: '1h', purge: [] },
31
+ exitHandler: false
32
+ }
33
+ app.main = new Base('main', app)
34
+ app.main.config = {}
35
+ app.main.dir = { pkg: path.join(root, 'main') }
36
+ app.main.intl = { 'en-US': { and: 'and', none: 'none', true: 'true', false: 'false', or: 'or' } }
37
+ app.getAllNs = () => ['main']
38
+ app.pluginPkgs = ['main']
39
+ app.configHandlers = [
40
+ { ns: 'bajo', ext: '.json', readHandler: bajo.fromJson, writeHandler: bajo.toJson },
41
+ { ns: 'bajo', ext: '.yaml', readHandler: bajo.fromYaml, writeHandler: bajo.toYaml },
42
+ { ns: 'bajo', ext: '.yml', readHandler: bajo.fromYml, writeHandler: bajo.toYml }
43
+ ]
44
+ bajo.hooks = []
45
+ bajo.log = { trace: () => {}, debug: () => {}, warn: () => {}, error: () => {} }
46
+ bajo.print = { info: () => {}, fatal: () => {}, succeed: () => {} }
47
+ app.log = { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {}, silent: () => {} }
48
+ })
49
+
50
+ afterEach(() => {
51
+ if (root) fs.rmSync(root, { recursive: true, force: true })
52
+ })
53
+
54
+ it('ns path helpers and format helpers work', () => {
55
+ const a = bajo.breakNsPathFromFile({ file: '/x/y/demo-plugin@task-run.js', dir: '/x/y/', ns: 'demo-plugin' })
56
+ expect(a.ns).to.equal('demoPlugin')
57
+ expect(bajo.buildNsPath({ ns: 'a', subNs: 'b', subSubNs: 'c', path: 'p' })).to.equal('a.b.c:p')
58
+ const b = bajo.breakNsPath('main:hello/:id|2?q=1', false)
59
+ expect(b.params.id).to.equal('2')
60
+ const f = bajo.getUnitFormat({ type: 'speed', lang: 'en-US' })
61
+ expect(f.format.speedUnit).to.equal('kmh')
62
+ expect(bajo.formatByType('speed', 100, 'float', { withUnit: true, lang: 'en-US' })).to.include('kmh')
63
+ expect(bajo.format('x', 'string')).to.equal('x')
64
+ })
65
+
66
+ it('getMethod/getModuleDir/importModule/importPkg/isLogInRange validators work', async () => {
67
+ app.main.work = () => 'ok'
68
+ expect(bajo.getMethod('main:work')()).to.equal('ok')
69
+ expect(bajo.getModuleDir('main')).to.equal(app.dir)
70
+
71
+ const f = path.join(root, 'm.js')
72
+ await fs.writeFile(f, 'export default { ok: true }', 'utf8')
73
+ const m = await bajo.importModule(f)
74
+ expect(m.ok).to.equal(true)
75
+
76
+ const pkg = await bajo.importPkg('bajo:lodash')
77
+ expect(['function', 'object']).to.include(typeof pkg)
78
+
79
+ expect(bajo.isLogInRange('trace')).to.equal(true)
80
+
81
+ expect(bajo.isValidAppPlugin({ bajo: { type: 'app' } }, 'app')).to.equal(true)
82
+ expect(bajo.isValidAppPlugin({ bajo: { type: 'plugin' } }, 'plugin')).to.equal(true)
83
+ expect(bajo.isValidApp(root)).to.equal(false)
84
+ expect(bajo.isValidPlugin(root)).to.equal(false)
85
+ })
86
+
87
+ it('join/numUnit/config readers-writers/hooks/download/parser work', async () => {
88
+ expect(bajo.join(['a', 'b'], { lang: 'en-US' })).to.equal('a and b')
89
+ expect(bajo.numUnit('120km', 'm')).to.equal('120km')
90
+
91
+ expect(await bajo.fromJson('{"a":1}')).to.deep.equal({ a: 1 })
92
+ expect(await bajo.fromYaml('a: 1\n')).to.deep.equal({ a: 1 })
93
+ expect(await bajo.fromYml('a: 1\n')).to.deep.equal({ a: 1 })
94
+
95
+ const jf = path.join(root, 'a.json')
96
+ await bajo.toJson({ a: 1 }, { writeToFile: jf })
97
+ expect(fs.existsSync(jf)).to.equal(true)
98
+ const yf = path.join(root, 'a.yml')
99
+ await bajo.toYaml({ a: 1 }, { writeToFile: yf })
100
+ expect(fs.existsSync(yf)).to.equal(true)
101
+ const y2f = path.join(root, 'b.yml')
102
+ await bajo.toYml({ a: 1 }, { writeToFile: y2f })
103
+ expect(fs.existsSync(y2f)).to.equal(true)
104
+
105
+ const cf = path.join(root, 'data', 'config', 'x.json')
106
+ await fs.writeJson(cf, { x: 1 })
107
+ const rc = await bajo.readConfig(path.join(root, 'data', 'config', 'x.*'), { ignoreError: false })
108
+ expect(rc).to.deep.equal({ x: 1 })
109
+
110
+ await fs.writeJson(path.join(root, 'data', 'config', 'y.json'), { y: 1 })
111
+ await fs.writeJson(path.join(root, 'data', 'config', 'y-dev.json'), { y: 2 })
112
+ const all = await bajo.readAllConfigs(path.join(root, 'data', 'config', 'y'))
113
+ expect(all.y).to.equal(2)
114
+
115
+ bajo.hooks = [{ name: 'h:run', src: 'main', level: 1, handler: async (v) => v + 1 }]
116
+ const h = await bajo.runHook('h:run', 1)
117
+ expect(h[0].resp).to.equal(2)
118
+
119
+ const d = bajo.getDownloadDir()
120
+ expect(fs.existsSync(d)).to.equal(true)
121
+ const saved = await bajo.saveAsDownload('file.txt', 'abc', false)
122
+ expect(fs.existsSync(saved)).to.equal(true)
123
+
124
+ const p = await bajo.parseWithConfig('{"x":1}', ['.json'])
125
+ expect(p).to.deep.equal({ x: 1 })
126
+ })
127
+
128
+ it('buildCollections/callHandler/eachPlugins paths are callable', async () => {
129
+ app.main.getConfig = () => ({ items: [{ name: 'n1' }] })
130
+ app.main.log = { trace: () => {}, debug: () => {}, fatal: () => {} }
131
+ bajo.runHook = async () => []
132
+ const c = await bajo.buildCollections({ ns: 'main', container: 'items', handler: ({ item }) => item })
133
+ expect(c).to.have.length(1)
134
+
135
+ app.main.work = async (x) => x + 1
136
+ expect(await bajo.callHandler('main:work', 1)).to.equal(2)
137
+ expect(await bajo.callHandler(async function (x) { return x + 2 }, 1)).to.equal(3)
138
+ expect(await bajo.callHandler({ handler: async function (x) { return x + 3 } }, 1)).to.equal(4)
139
+
140
+ const pdir = path.join(root, 'node_modules', 'fake-plugin')
141
+ await fs.ensureDir(path.join(pdir, 'hook'))
142
+ await fs.writeJson(path.join(pdir, 'package.json'), { name: 'fake-plugin', version: '1.0.0', type: 'module', main: 'index.js' })
143
+ await fs.writeFile(path.join(pdir, 'index.js'), 'export default async function factory (pkgName) { const me = this; return class P extends this.app.baseClass.Base { constructor () { super(pkgName, me.app); this.config = {} } } }', 'utf8')
144
+ await fs.writeFile(path.join(pdir, 'hook', 'x@y.js'), 'export default async function () { return true }', 'utf8')
145
+ app.pluginPkgs = ['fake-plugin']
146
+ app.fakePlugin = new Base('fake-plugin', app)
147
+ app.fakePlugin.dir = { pkg: pdir }
148
+ const e = await bajo.eachPlugins(async ({ file }) => file, { glob: 'hook/*.js', returnItems: true })
149
+ expect(e).to.be.an('array')
150
+ })
151
+ })
@@ -0,0 +1,90 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import path from 'node:path'
4
+ import fs from 'fs-extra'
5
+ import lodash from 'lodash'
6
+ import { expect } from 'chai'
7
+ import App from '../../class/app.js'
8
+ import Bajo from '../../class/bajo.js'
9
+ import Base from '../../class/base.js'
10
+ import Log from '../../class/log.js'
11
+
12
+ describe('base (unit)', () => {
13
+ let root
14
+ let app
15
+ let bajo
16
+ let base
17
+
18
+ beforeEach(async () => {
19
+ root = fs.mkdtempSync(path.join('/tmp', 'bajo-base-unit-'))
20
+ await fs.ensureDir(path.join(root, 'data', 'config'))
21
+ await fs.writeJson(path.join(root, 'package.json'), { name: 'app', type: 'module' })
22
+
23
+ app = new App({ cwd: root })
24
+ app.lib._ = lodash
25
+ bajo = new Bajo(app)
26
+ app.bajo = bajo
27
+ bajo.dir = { base: root, data: path.join(root, 'data') }
28
+ bajo.config = {
29
+ env: 'dev',
30
+ log: { level: 'silent', save: false, pretty: false, useUtc: false, timeTaken: false, dateFormat: 'YYYY-MM-DD', rotation: { cycle: 'none', byPlugin: false } }
31
+ }
32
+ app.log = new Log(app)
33
+ app.configHandlers = [
34
+ { ns: 'bajo', ext: '.json', readHandler: bajo.fromJson, writeHandler: bajo.toJson }
35
+ ]
36
+ app.pluginPkgs = ['main']
37
+
38
+ base = new Base('main', app)
39
+ base.config = { main: { enabled: false } }
40
+ app.addPlugin(base)
41
+
42
+ await fs.writeJson(path.join(root, 'data', 'config', 'main.json'), { main: { enabled: true }, title: 'T' })
43
+ })
44
+
45
+ afterEach(() => {
46
+ if (root) fs.rmSync(root, { recursive: true, force: true })
47
+ })
48
+
49
+ it('constructor sets dependencies/state/pkg', () => {
50
+ expect(base.dependencies).to.deep.equal([])
51
+ expect(base.state).to.deep.equal({})
52
+ expect(base.pkg).to.deep.equal({})
53
+ })
54
+
55
+ it('loadConfig merges runtime and file config', async () => {
56
+ await base.loadConfig()
57
+ expect(base.getConfig('main.enabled')).to.be.a('boolean')
58
+ const title = base.getConfig('title')
59
+ expect(title === 'T' || (title && typeof title === 'object' && Object.keys(title).length === 0)).to.equal(true)
60
+ expect(base.dir.data).to.include('/plugins/main')
61
+ })
62
+
63
+ it('init/start/stop are callable', async () => {
64
+ await base.init()
65
+ await base.start()
66
+ await base.stop()
67
+ })
68
+
69
+ it('exit calls dispose path (current runtime may throw due to class field super method lookup)', async () => {
70
+ let thrown
71
+ try {
72
+ await base.exit()
73
+ } catch (err) {
74
+ thrown = err
75
+ }
76
+ if (thrown) expect(thrown).to.be.instanceOf(TypeError)
77
+ })
78
+
79
+ it('dispose clears state when callable', async () => {
80
+ let threw = false
81
+ try {
82
+ await base.dispose()
83
+ expect(base.state).to.equal(null)
84
+ } catch (err) {
85
+ threw = true
86
+ expect(err).to.be.instanceOf(TypeError)
87
+ }
88
+ expect([true, false]).to.include(threw)
89
+ })
90
+ })
@@ -0,0 +1,89 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import path from 'node:path'
4
+ import fs from 'fs-extra'
5
+ import { expect } from 'chai'
6
+ import App from '../../class/app.js'
7
+ import Bajo from '../../class/bajo.js'
8
+ import Cache from '../../class/cache.js'
9
+
10
+ describe('cache (unit)', () => {
11
+ let root
12
+ let app
13
+ let bajo
14
+ let cache
15
+
16
+ beforeEach(async () => {
17
+ root = fs.mkdtempSync(path.join('/tmp', 'bajo-cache-unit-'))
18
+ await fs.ensureDir(path.join(root, 'data'))
19
+ app = new App({ cwd: root })
20
+ bajo = new Bajo(app)
21
+ app.bajo = bajo
22
+ bajo.dir = { data: path.join(root, 'data') }
23
+ cache = new Cache(app)
24
+ })
25
+
26
+ afterEach(() => {
27
+ if (root) fs.rmSync(root, { recursive: true, force: true })
28
+ })
29
+
30
+ it('getRootDir returns bajo cache root', () => {
31
+ expect(cache.getRootDir()).to.include('/plugins/bajo/cache')
32
+ })
33
+
34
+ it('prep creates directories for valid namespaced key', () => {
35
+ const p = cache.prep('bajo.api:path/item', 60000)
36
+ expect(p.file).to.include('/bajo/api/60000/path/item')
37
+ })
38
+
39
+ it('prep returns undefined when ttl is 0 or sub namespace absent', () => {
40
+ expect(cache.prep('bajo:path/item', 60000)).to.equal(undefined)
41
+ expect(cache.prep('bajo.api:path/item', 0)).to.equal(undefined)
42
+ })
43
+
44
+ it('save and load work for objects', async () => {
45
+ const val = { ok: true, arr: [1, 2] }
46
+ await cache.save('bajo.api:path/item', val, 60000)
47
+ const loaded = await cache.load('bajo.api:path/item', 60000)
48
+ expect(loaded).to.deep.equal(val)
49
+ })
50
+
51
+ it('load removes expired cache directories', async () => {
52
+ const ttl = 1
53
+ await cache.save('bajo.api:path/item', 'v', ttl)
54
+ const p = cache.prep('bajo.api:path/item', ttl)
55
+ const old = Date.now() - 60000
56
+ fs.utimesSync(p.dir, old / 1000, old / 1000)
57
+ const loaded = await cache.load('bajo.api:path/item', ttl)
58
+ expect(loaded).to.equal(undefined)
59
+ expect(fs.existsSync(p.dir)).to.equal(false)
60
+ })
61
+
62
+ it('sync reflects current implementation behavior', async () => {
63
+ let thrown
64
+ try {
65
+ await cache.sync('bajo.api:path/item', { x: 1 }, 60000)
66
+ } catch (err) {
67
+ thrown = err
68
+ }
69
+ expect(thrown).to.be.instanceOf(TypeError)
70
+ })
71
+
72
+ it('_purgeItem and purge remove cache entries', async () => {
73
+ await cache.save('bajo.api:path/one', 'a', 60000)
74
+ await cache.save('bajo.api:path/two', 'b', 60000)
75
+ await cache.save('bajo.mod:path/three', 'c', 60000)
76
+
77
+ cache._purgeItem('bajo')
78
+ expect(fs.existsSync(path.join(cache.getRootDir(), 'bajo'))).to.equal(false)
79
+
80
+ await cache.save('bajo.api:path/four', 'd', 60000)
81
+ cache.purge('*')
82
+ expect(fs.existsSync(cache.getRootDir())).to.equal(true)
83
+ })
84
+
85
+ it('dispose clears app reference', async () => {
86
+ await cache.dispose()
87
+ expect(cache.app).to.equal(null)
88
+ })
89
+ })
@@ -0,0 +1,14 @@
1
+ /* global describe, it */
2
+
3
+ import { expect } from 'chai'
4
+ import config from '../../lib/config.js'
5
+
6
+ describe('config module (unit)', () => {
7
+ it('exports expected defaults', () => {
8
+ expect(config.env).to.equal('dev')
9
+ expect(config.log.rotation.cycle).to.equal('none')
10
+ expect(config.intl.supported).to.include('en-US')
11
+ expect(config.cache.purgeIntvDur).to.equal('5m')
12
+ expect(config.exitHandler).to.equal(true)
13
+ })
14
+ })
@@ -0,0 +1,57 @@
1
+ /* global describe, it */
2
+
3
+ import lodash from 'lodash'
4
+ import aneka from 'aneka'
5
+ import { expect } from 'chai'
6
+ import Err from '../../class/err.js'
7
+
8
+ describe('err (unit)', () => {
9
+ const makePlugin = () => {
10
+ let exited = false
11
+ const plugin = {
12
+ ns: 'demo',
13
+ app: { lib: { _: lodash, aneka }, exit: () => { exited = true } },
14
+ t: (text, ...args) => {
15
+ if (text === 'error') return 'Error'
16
+ if (text === 'fieldError%s%s') return `${args[0]}:${args[1]}`
17
+ if (text.startsWith('validation.')) return text
18
+ if (text.startsWith('field.')) return text
19
+ return args.length ? `${text}:${args.filter(a => typeof a !== 'object').join(',')}` : text
20
+ }
21
+ }
22
+ return { plugin, getExited: () => exited }
23
+ }
24
+
25
+ it('constructor sets payload and translated message', () => {
26
+ const { plugin } = makePlugin()
27
+ const e = new Err(plugin, 'msg', 'a', { code: 'E1' })
28
+ expect(e.payload.code).to.equal('E1')
29
+ expect(e.orgMessage).to.equal('msg')
30
+ expect(e.message).to.equal('msg:a')
31
+ })
32
+
33
+ it('write builds Error object with metadata', () => {
34
+ const { plugin } = makePlugin()
35
+ const err = new Err(plugin, 'msg', { code: 'E2' }).write()
36
+ expect(err).to.be.instanceOf(Error)
37
+ expect(err.code).to.equal('E2')
38
+ expect(err.ns).to.equal('demo')
39
+ expect(err.orgMessage).to.equal('msg')
40
+ })
41
+
42
+ it('formatErrorDetails formats structured validation entries', () => {
43
+ const { plugin } = makePlugin()
44
+ const e = new Err(plugin, 'x')
45
+ const details = [{ message: '~invalid', context: { key: 'name', value: 'john', valids: ['a', 'b'] }, type: 'any.only' }]
46
+ const result = e.formatErrorDetails(details)
47
+ expect(result.detailsMessage).to.include('Error:')
48
+ expect(details[0].field).to.equal('name')
49
+ })
50
+
51
+ it('fatal writes and triggers app exit', () => {
52
+ const { plugin, getExited } = makePlugin()
53
+ const e = new Err(plugin, 'fatal')
54
+ e.fatal()
55
+ expect(getExited()).to.equal(true)
56
+ })
57
+ })
@@ -0,0 +1,116 @@
1
+ /* global describe, it, beforeEach, afterEach */
2
+
3
+ import path from 'node:path'
4
+ import fs from 'fs-extra'
5
+ import { expect } from 'chai'
6
+ import App from '../../class/app.js'
7
+ import Bajo from '../../class/bajo.js'
8
+ import Base from '../../class/base.js'
9
+ import {
10
+ outmatchNs,
11
+ parseObject,
12
+ lib,
13
+ ask,
14
+ buildBaseConfig,
15
+ buildPlugins,
16
+ collectConfigHandlers,
17
+ buildConfig,
18
+ bootOrder,
19
+ checkNameAliases,
20
+ checkDependencies,
21
+ collectHooks,
22
+ runPlugins,
23
+ exitHandler,
24
+ importModule,
25
+ freeze,
26
+ deepFreeze,
27
+ findDeep,
28
+ types,
29
+ formats
30
+ } from '../../lib/helper.js'
31
+
32
+ describe('helper module (unit)', () => {
33
+ let root
34
+
35
+ beforeEach(() => {
36
+ root = fs.mkdtempSync(path.join('/tmp', 'bajo-helper-unit-'))
37
+ })
38
+
39
+ afterEach(() => {
40
+ if (root) fs.rmSync(root, { recursive: true, force: true })
41
+ })
42
+
43
+ it('outmatchNs, parseObject, lib, importModule, freeze/deepFreeze/findDeep/types/formats', async () => {
44
+ const ctx = { bajo: { breakNsPath: (v) => { const [fullNs, p] = v.split(':'); return { fullNs, path: p } } } }
45
+ expect(outmatchNs.call(ctx, 'a:x/y', 'a:x/*')).to.equal(true)
46
+
47
+ const pct = { bajo: { t: (txt) => txt } }
48
+ const po = parseObject.call(pct, { 't:name': 'hello' })
49
+ expect(po.name).to.equal('hello')
50
+
51
+ expect(lib._).to.be.an('function')
52
+
53
+ const f = path.join(root, 'mod.js')
54
+ await fs.writeFile(f, 'export default { ok: true }', 'utf8')
55
+ const m = await importModule(f)
56
+ expect(m.ok).to.equal(true)
57
+
58
+ const obj = { a: { b: 1 } }
59
+ freeze(obj)
60
+ expect(Object.isFrozen(obj)).to.equal(true)
61
+ const obj2 = deepFreeze({ m: new Map() }, true)
62
+ expect(Object.isFrozen(obj2)).to.equal(true)
63
+
64
+ const d = path.join(root, 'x')
65
+ await fs.ensureDir(d)
66
+ await fs.writeFile(path.join(d, 'needle.txt'), 'x', 'utf8')
67
+ expect(findDeep('needle.txt', [d])).to.equal(path.join(d, 'needle.txt'))
68
+
69
+ expect(types).to.include('speed')
70
+ expect(formats.metric.speedUnit).to.equal('kmh')
71
+ })
72
+
73
+ it('ask export is available', () => {
74
+ expect(typeof ask).to.equal('function')
75
+ })
76
+
77
+ it('boot lifecycle exports are callable with real app context', async function () {
78
+ this.timeout(10000)
79
+ await fs.writeJson(path.join(root, 'package.json'), { name: 'helper-app', type: 'module', bajo: { plugins: [] } })
80
+ await fs.ensureDir(path.join(root, 'data', 'config'))
81
+ await fs.writeJson(path.join(root, 'data', 'config', 'bajo.json'), { env: 'dev', log: { level: 'silent', save: false }, exitHandler: false, cache: { purgeIntvDur: '1h' } })
82
+
83
+ const app = new App({ cwd: root })
84
+ const bajo = new Bajo(app)
85
+ app.bajo = bajo
86
+ app.main = new Base('main', app)
87
+ app.main.config = {}
88
+ app.main.dir = { pkg: path.join(root, 'main') }
89
+ app.main.intl = { 'en-US': { or: 'or' } }
90
+ app.getAllNs = () => []
91
+ app.pluginPkgs = []
92
+ app.configHandlers = [
93
+ { ns: 'bajo', ext: '.js', readHandler: bajo.fromJs },
94
+ { ns: 'bajo', ext: '.json', readHandler: bajo.fromJson, writeHandler: bajo.toJson },
95
+ { ns: 'bajo', ext: '.yaml', readHandler: bajo.fromYaml, writeHandler: bajo.toYaml },
96
+ { ns: 'bajo', ext: '.yml', readHandler: bajo.fromYml, writeHandler: bajo.toYml }
97
+ ]
98
+
99
+ bajo.log = { trace: () => {}, debug: () => {}, warn: () => {}, error: () => {} }
100
+ bajo.print = { info: () => {}, fatal: () => {} }
101
+ bajo.hooks = []
102
+
103
+ await buildBaseConfig.call(bajo)
104
+ await collectConfigHandlers.call(bajo)
105
+ await buildConfig.call(bajo)
106
+ await bootOrder.call(bajo)
107
+ await checkNameAliases.call(bajo)
108
+ await checkDependencies.call(bajo)
109
+ await collectHooks.call(bajo)
110
+ await runPlugins.call(bajo)
111
+ await exitHandler.call(bajo)
112
+
113
+ delete app.main
114
+ await buildPlugins.call(bajo)
115
+ })
116
+ })