bajo 2.23.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.
- package/.jsdoc.conf.json +2 -1
- package/class/app.js +220 -107
- package/class/bajo.js +246 -174
- 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 +6 -5
- 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 +8 -7
- package/docs/data/search.json +1 -1
- package/docs/extend_bajo_hook.js.html +289 -0
- 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-Main.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 +184 -190
- 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 +4 -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/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
|
@@ -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
|
+
})
|