bajo 2.17.0 → 2.19.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/class/_helper.js +22 -7
- package/class/app.js +148 -35
- package/class/bajo.js +156 -206
- package/class/base.js +3 -3
- package/class/cache.js +61 -2
- package/class/err.js +14 -11
- package/class/log.js +41 -40
- package/class/plugin.js +35 -36
- package/class/print.js +54 -51
- package/class/tools.js +3 -4
- package/docs/App.html +7 -7
- package/docs/Bajo.html +2 -2
- package/docs/Base.html +1 -1
- package/docs/Cache.html +3 -0
- package/docs/Err.html +2 -2
- package/docs/Log.html +2 -2
- package/docs/Plugin.html +1 -1
- package/docs/Print.html +1 -1
- package/docs/Tools.html +3 -0
- package/docs/class__helper.js.html +694 -0
- package/docs/class_app.js.html +307 -149
- package/docs/class_bajo.js.html +316 -464
- package/docs/class_base.js.html +35 -32
- package/docs/class_cache.js.html +150 -0
- package/docs/class_err.js.html +144 -0
- package/docs/class_log.js.html +270 -0
- package/docs/class_plugin.js.html +98 -71
- package/docs/class_print.js.html +261 -0
- package/docs/class_tools.js.html +44 -0
- package/docs/data/search.json +1 -1
- package/docs/global.html +1 -4
- package/docs/index.html +1 -1
- package/docs/index.js.html +21 -14
- package/docs/lib_find-deep.js.html +27 -0
- package/docs/lib_formats.js.html +19 -19
- package/docs/lib_freeze.js.html +19 -0
- package/docs/lib_import-module.js.html +16 -14
- package/docs/lib_index.js.html +9 -0
- package/docs/lib_log-levels.js.html +2 -2
- package/docs/module-Helper.html +3 -0
- package/docs/module-Lib.html +3 -8
- package/docs/scripts/core.js +477 -476
- package/docs/scripts/resize.js +36 -36
- package/docs/scripts/search.js +105 -105
- package/docs/scripts/third-party/fuse.js +1 -1
- package/docs/scripts/third-party/hljs-line-num-original.js +285 -282
- package/docs/scripts/third-party/hljs-line-num.js +1 -1
- package/docs/scripts/third-party/hljs-original.js +1202 -1195
- package/docs/scripts/third-party/hljs.js +1 -1
- package/docs/scripts/third-party/popper.js +1 -1
- package/docs/scripts/third-party/tippy.js +1 -1
- package/docs/scripts/third-party/tocbot.js +509 -508
- package/index.js +8 -11
- package/lib/find-deep.js +3 -3
- package/lib/formats.js +17 -17
- package/lib/freeze.js +3 -3
- package/lib/import-module.js +9 -9
- package/package.json +1 -1
- package/test/app.test.js +183 -0
- package/test/bajo.test.js +125 -0
- package/test/base.test.js +74 -107
- package/test/cache.test.js +94 -0
- package/test/e2e.test.js +137 -0
- package/test/err.test.js +73 -0
- package/test/helper.test.js +39 -0
- package/test/import-module.test.js +138 -0
- package/test/integration.test.js +218 -0
- package/test/log.test.js +119 -0
- package/test/plugin.test.js +116 -0
- package/test/print.test.js +100 -0
- package/test/tools.test.js +38 -0
- package/wiki/CHANGES.md +10 -0
- package/.mocharc.json +0 -4
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
import fastGlob from 'fast-glob'
|
|
8
|
+
import lodash from 'lodash'
|
|
9
|
+
|
|
10
|
+
import Cache from '../class/cache.js'
|
|
11
|
+
|
|
12
|
+
const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'bajo-cache-test-'))
|
|
13
|
+
|
|
14
|
+
describe('Cache', () => {
|
|
15
|
+
let root
|
|
16
|
+
let app
|
|
17
|
+
let cache
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
root = createTempRoot()
|
|
21
|
+
app = {
|
|
22
|
+
getPluginDataDir: () => path.join(root, 'bajo'),
|
|
23
|
+
bajo: {
|
|
24
|
+
breakNsPath: (name) => {
|
|
25
|
+
const [nsPart, p] = name.split(':')
|
|
26
|
+
const [ns, subNs] = nsPart.split('.')
|
|
27
|
+
return { ns, subNs, path: p }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
lib: {
|
|
31
|
+
aneka: {
|
|
32
|
+
parseDuration: (v) => typeof v === 'number' ? v : 0
|
|
33
|
+
},
|
|
34
|
+
fs,
|
|
35
|
+
fastGlob,
|
|
36
|
+
_: lodash
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
cache = new Cache(app)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (root) fs.rmSync(root, { recursive: true, force: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('prepares cache location only for namespaced key and non-zero ttl', () => {
|
|
47
|
+
expect(cache.prep('main:path', 0)).to.equal(undefined)
|
|
48
|
+
expect(cache.prep('main:path', 1000)).to.equal(undefined)
|
|
49
|
+
|
|
50
|
+
const prep = cache.prep('main.api:path/to/item', 1000)
|
|
51
|
+
expect(prep.file).to.include('/main/api/1000/path/to/item')
|
|
52
|
+
expect(fs.existsSync(prep.dir)).to.equal(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('saves and loads json/object content', async () => {
|
|
56
|
+
await cache.save('main.api:item', { a: 1 }, 1000)
|
|
57
|
+
const item = await cache.load('main.api:item', 1000)
|
|
58
|
+
|
|
59
|
+
expect(item).to.deep.equal({ a: 1 })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns undefined for expired cache and removes ttl directory', async () => {
|
|
63
|
+
await cache.save('main.api:item', 'ok', 1)
|
|
64
|
+
const { dir } = cache.prep('main.api:item', 1)
|
|
65
|
+
const old = Date.now() - 5000
|
|
66
|
+
fs.utimesSync(dir, old / 1000, old / 1000)
|
|
67
|
+
|
|
68
|
+
const result = await cache.load('main.api:item', 1)
|
|
69
|
+
|
|
70
|
+
expect(result).to.equal(undefined)
|
|
71
|
+
expect(fs.existsSync(dir)).to.equal(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('purges by name and wildcard', () => {
|
|
75
|
+
const a = cache.prep('main.api:a', 1000)
|
|
76
|
+
const b = cache.prep('main.web:b', 1000)
|
|
77
|
+
fs.writeFileSync(a.file, 'a', 'utf8')
|
|
78
|
+
fs.writeFileSync(b.file, 'b', 'utf8')
|
|
79
|
+
|
|
80
|
+
cache.purge('main')
|
|
81
|
+
expect(fs.existsSync(cache.getRootDir() + '/main')).to.equal(false)
|
|
82
|
+
|
|
83
|
+
const c = cache.prep('other.api:c', 1000)
|
|
84
|
+
fs.writeFileSync(c.file, 'c', 'utf8')
|
|
85
|
+
cache._purgeItem('*')
|
|
86
|
+
expect(fs.existsSync(cache.getRootDir() + '/other')).to.equal(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('disposes app reference', async () => {
|
|
90
|
+
await cache.dispose()
|
|
91
|
+
|
|
92
|
+
expect(cache.app).to.equal(null)
|
|
93
|
+
})
|
|
94
|
+
})
|
package/test/e2e.test.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* global describe, it, beforeEach, afterEach */
|
|
2
|
+
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
import { expect } from 'chai'
|
|
8
|
+
import fs from 'fs-extra'
|
|
9
|
+
|
|
10
|
+
const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'bajo-e2e-test-'))
|
|
11
|
+
|
|
12
|
+
const runNode = (cwd, file, timeoutMs = 8000) => {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const child = spawn(process.execPath, [file], {
|
|
15
|
+
cwd,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let stdout = ''
|
|
20
|
+
let stderr = ''
|
|
21
|
+
|
|
22
|
+
child.stdout.on('data', chunk => {
|
|
23
|
+
stdout += chunk.toString()
|
|
24
|
+
})
|
|
25
|
+
child.stderr.on('data', chunk => {
|
|
26
|
+
stderr += chunk.toString()
|
|
27
|
+
})
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
child.kill('SIGTERM')
|
|
30
|
+
resolve({ code: null, stdout, stderr, timedOut: true })
|
|
31
|
+
}, timeoutMs)
|
|
32
|
+
child.on('close', code => {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
resolve({ code, stdout, stderr, timedOut: false })
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('E2E', () => {
|
|
40
|
+
let rootDir
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
rootDir = createTempRoot()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
if (rootDir) fs.rmSync(rootDir, { recursive: true, force: true })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('boots a real app in a separate process and runs a real plugin end to end', async function () {
|
|
51
|
+
this.timeout(12000)
|
|
52
|
+
const packageEntry = pathToFileURL(path.join('/mnt/d/Projects/Bajo/bajo', 'index.js')).href
|
|
53
|
+
const pluginName = 'demo-e2e-plugin'
|
|
54
|
+
const pluginDir = path.join(rootDir, 'node_modules', pluginName)
|
|
55
|
+
const appMainDir = path.join(rootDir, 'main')
|
|
56
|
+
const outputFile = path.join(rootDir, 'e2e-output.txt')
|
|
57
|
+
|
|
58
|
+
await fs.ensureDir(pluginDir)
|
|
59
|
+
await fs.ensureDir(appMainDir)
|
|
60
|
+
|
|
61
|
+
await fs.writeJson(path.join(rootDir, 'package.json'), {
|
|
62
|
+
name: 'bajo-e2e-app',
|
|
63
|
+
type: 'module',
|
|
64
|
+
bajo: {
|
|
65
|
+
plugins: [pluginName]
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await fs.writeJson(path.join(pluginDir, 'package.json'), {
|
|
70
|
+
name: pluginName,
|
|
71
|
+
version: '1.0.0',
|
|
72
|
+
type: 'module',
|
|
73
|
+
main: 'index.js'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
await fs.writeFile(path.join(pluginDir, 'index.js'), `
|
|
77
|
+
async function factory (pkgName) {
|
|
78
|
+
const me = this
|
|
79
|
+
|
|
80
|
+
return class E2ePlugin extends this.app.baseClass.Base {
|
|
81
|
+
constructor () {
|
|
82
|
+
super(pkgName, me.app)
|
|
83
|
+
this.config = {}
|
|
84
|
+
this.start = async () => {
|
|
85
|
+
await this.app.lib.fs.writeFile(this.app.dir + '/e2e-output.txt', 'started:' + this.ns, 'utf8')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default factory
|
|
92
|
+
`, 'utf8')
|
|
93
|
+
|
|
94
|
+
await fs.writeFile(path.join(appMainDir, 'index.js'), `
|
|
95
|
+
async function factory (pkgName) {
|
|
96
|
+
const me = this
|
|
97
|
+
|
|
98
|
+
return class Main extends this.app.baseClass.Base {
|
|
99
|
+
constructor () {
|
|
100
|
+
super(pkgName, me.app)
|
|
101
|
+
this.config = {}
|
|
102
|
+
this.start = async () => {
|
|
103
|
+
console.log('MAIN_STARTED')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default factory
|
|
110
|
+
`, 'utf8')
|
|
111
|
+
|
|
112
|
+
await fs.ensureDir(path.join(rootDir, 'data', 'config'))
|
|
113
|
+
await fs.writeJson(path.join(rootDir, 'data', 'config', 'bajo.json'), {
|
|
114
|
+
log: { level: 'silent', save: false },
|
|
115
|
+
exitHandler: false,
|
|
116
|
+
cache: { purgeIntvDur: '1h' }
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
await fs.writeFile(path.join(rootDir, 'run-app.js'), `
|
|
120
|
+
import boot from ${JSON.stringify(packageEntry)}
|
|
121
|
+
|
|
122
|
+
const app = await boot({ cwd: process.cwd() })
|
|
123
|
+
console.log('BOOT_OK:' + !!app.getPlugin(${JSON.stringify(pluginName)}, true))
|
|
124
|
+
process.exit(0)
|
|
125
|
+
`, 'utf8')
|
|
126
|
+
|
|
127
|
+
const result = await runNode(rootDir, 'run-app.js')
|
|
128
|
+
|
|
129
|
+
expect(result.timedOut).to.equal(false)
|
|
130
|
+
expect(result.code).to.equal(0)
|
|
131
|
+
expect(result.stderr).to.equal('')
|
|
132
|
+
expect(result.stdout).to.include('MAIN_STARTED')
|
|
133
|
+
expect(result.stdout).to.include('BOOT_OK:true')
|
|
134
|
+
expect(fs.existsSync(outputFile)).to.equal(true)
|
|
135
|
+
expect(fs.readFileSync(outputFile, 'utf8')).to.match(/^started:/)
|
|
136
|
+
})
|
|
137
|
+
})
|
package/test/err.test.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* global describe, it */
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai'
|
|
4
|
+
import lodash from 'lodash'
|
|
5
|
+
|
|
6
|
+
import Err from '../class/err.js'
|
|
7
|
+
|
|
8
|
+
describe('Err', () => {
|
|
9
|
+
const makePlugin = () => ({
|
|
10
|
+
ns: 'myPlugin',
|
|
11
|
+
app: {
|
|
12
|
+
lib: {
|
|
13
|
+
_: lodash,
|
|
14
|
+
aneka: { without: (arr) => arr.filter(Boolean) }
|
|
15
|
+
},
|
|
16
|
+
exit: () => {}
|
|
17
|
+
},
|
|
18
|
+
t: (msg, ctx) => {
|
|
19
|
+
if (msg === 'error') return 'Error'
|
|
20
|
+
if (msg === 'fieldError%s%s') return `${ctx}`
|
|
21
|
+
return msg
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('writes error object with payload and metadata', () => {
|
|
26
|
+
const plugin = makePlugin()
|
|
27
|
+
const err = new Err(plugin, 'hello %s', 'x', { code: 'E1', foo: 'bar' }).write()
|
|
28
|
+
|
|
29
|
+
expect(err).to.be.instanceOf(Error)
|
|
30
|
+
expect(err.message).to.equal('hello %s')
|
|
31
|
+
expect(err.code).to.equal('E1')
|
|
32
|
+
expect(err.foo).to.equal('bar')
|
|
33
|
+
expect(err.ns).to.equal('myPlugin')
|
|
34
|
+
expect(err.orgMessage).to.equal('hello %s')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('formats details payload into detailsMessage', () => {
|
|
38
|
+
const plugin = makePlugin()
|
|
39
|
+
plugin.t = (msg) => {
|
|
40
|
+
if (msg === 'error') return 'Error'
|
|
41
|
+
if (msg === 'fieldError%s%s') return 'field-error'
|
|
42
|
+
if (msg.startsWith('validation.')) return 'invalid value'
|
|
43
|
+
return msg
|
|
44
|
+
}
|
|
45
|
+
const item = {
|
|
46
|
+
type: 'string.base',
|
|
47
|
+
message: '~invalid value',
|
|
48
|
+
context: { key: 'name', value: 1 }
|
|
49
|
+
}
|
|
50
|
+
const err = new Err(plugin, 'bad', { details: [item] }).write()
|
|
51
|
+
|
|
52
|
+
expect(err).to.have.property('detailsMessage').that.includes('Error:')
|
|
53
|
+
expect(err.details[0]).to.include({ field: 'name', error: 'invalid value', value: 1 })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('prints and exits on fatal', () => {
|
|
57
|
+
const plugin = makePlugin()
|
|
58
|
+
let exitArg
|
|
59
|
+
plugin.app.exit = (arg) => { exitArg = arg }
|
|
60
|
+
const originalError = console.error
|
|
61
|
+
let called = false
|
|
62
|
+
console.error = () => { called = true }
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
new Err(plugin, 'boom').fatal()
|
|
66
|
+
} finally {
|
|
67
|
+
console.error = originalError
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
expect(called).to.equal(true)
|
|
71
|
+
expect(exitArg).to.equal(true)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* global describe, it */
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai'
|
|
4
|
+
|
|
5
|
+
import { outmatchNs, parseObject, lib } from '../class/_helper.js'
|
|
6
|
+
|
|
7
|
+
describe('_helper', () => {
|
|
8
|
+
it('matches namespace/path patterns', () => {
|
|
9
|
+
const ctx = {
|
|
10
|
+
bajo: {
|
|
11
|
+
breakNsPath: (pattern) => {
|
|
12
|
+
const [fullNs, p] = pattern.split(':')
|
|
13
|
+
return { fullNs, path: p }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
expect(outmatchNs.call(ctx, 'demo.api:users/12', 'demo.api:users/*')).to.equal(true)
|
|
19
|
+
expect(outmatchNs.call(ctx, 'demo.api', 'demo.api')).to.equal(true)
|
|
20
|
+
expect(outmatchNs.call(ctx, 'demo.api:users/12', 'x.api:users/*')).to.equal(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('parses object and applies translator from namespace', () => {
|
|
24
|
+
const ctx = {
|
|
25
|
+
bajo: {
|
|
26
|
+
t: (text, arg) => `T:${text}:${arg}`
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const parsed = parseObject.call(ctx, { msg: 't:hello|world' }, { ns: 'bajo', lang: 'en-US', parseValue: true })
|
|
30
|
+
|
|
31
|
+
expect(parsed).to.deep.equal({ msg: 'T:hello:world' })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('exports library helpers', () => {
|
|
35
|
+
expect(lib).to.have.property('_')
|
|
36
|
+
expect(lib).to.have.property('fs')
|
|
37
|
+
expect(lib).to.have.property('dayjs')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
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 importModule from '../lib/import-module.js'
|
|
9
|
+
|
|
10
|
+
const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'bajo-import-module-test-'))
|
|
11
|
+
|
|
12
|
+
const writeModule = async (filePath, content) => {
|
|
13
|
+
await fs.ensureDir(path.dirname(filePath))
|
|
14
|
+
await fs.writeFile(filePath, content, 'utf8')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('importModule', () => {
|
|
18
|
+
let rootDir
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
rootDir = createTempRoot()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
delete globalThis.__importModuleCounter
|
|
26
|
+
if (rootDir) fs.rmSync(rootDir, { recursive: true, force: true })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('imports default export by default', async () => {
|
|
30
|
+
const file = path.join(rootDir, 'default.js')
|
|
31
|
+
await writeModule(file, 'export default "hello"')
|
|
32
|
+
|
|
33
|
+
const mod = await importModule(file)
|
|
34
|
+
|
|
35
|
+
expect(mod).to.equal('hello')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns module namespace when asDefaultImport is false', async () => {
|
|
39
|
+
const file = path.join(rootDir, 'named.js')
|
|
40
|
+
await writeModule(file, 'export const answer = 42; export default "x"')
|
|
41
|
+
|
|
42
|
+
const mod = await importModule(file, { asDefaultImport: false })
|
|
43
|
+
|
|
44
|
+
expect(mod).to.have.property('default', 'x')
|
|
45
|
+
expect(mod).to.have.property('answer', 42)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns undefined when target file does not exist', async () => {
|
|
49
|
+
const mod = await importModule(path.join(rootDir, 'missing.js'))
|
|
50
|
+
|
|
51
|
+
expect(mod).to.equal(undefined)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('imports a fresh copy when noCache is true', async () => {
|
|
55
|
+
const file = path.join(rootDir, 'counter.js')
|
|
56
|
+
await writeModule(file, 'globalThis.__importModuleCounter = (globalThis.__importModuleCounter ?? 0) + 1; export default globalThis.__importModuleCounter')
|
|
57
|
+
|
|
58
|
+
const first = await importModule(file, { noCache: true })
|
|
59
|
+
const second = await importModule(file, { noCache: true })
|
|
60
|
+
|
|
61
|
+
expect(first).to.equal(1)
|
|
62
|
+
expect(second).to.equal(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('resolves plugin file through context when called with this', async () => {
|
|
66
|
+
const file = path.join(rootDir, 'ctx.js')
|
|
67
|
+
await writeModule(file, 'export default "from-context"')
|
|
68
|
+
const ctx = {
|
|
69
|
+
app: {
|
|
70
|
+
getPluginFile: (input) => {
|
|
71
|
+
expect(input).to.equal('plugin:file')
|
|
72
|
+
return file
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mod = await importModule.call(ctx, 'plugin:file')
|
|
78
|
+
|
|
79
|
+
expect(mod).to.equal('from-context')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('wraps function modules into handler object when asHandler is true', async () => {
|
|
83
|
+
const file = path.join(rootDir, 'handler-fn.js')
|
|
84
|
+
await writeModule(file, 'export default function sampleHandler () { return "ok" }')
|
|
85
|
+
|
|
86
|
+
const mod = await importModule(file, { asHandler: true })
|
|
87
|
+
|
|
88
|
+
expect(mod).to.have.property('level', 999)
|
|
89
|
+
expect(mod).to.have.property('handler').that.is.a('function')
|
|
90
|
+
expect(mod.handler()).to.equal('ok')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns plain object modules as-is in handler mode', async () => {
|
|
94
|
+
const file = path.join(rootDir, 'handler-obj.js')
|
|
95
|
+
await writeModule(file, 'export default { level: 100, handler: () => "ok" }')
|
|
96
|
+
|
|
97
|
+
const mod = await importModule(file, { asHandler: true })
|
|
98
|
+
|
|
99
|
+
expect(mod).to.have.property('level', 100)
|
|
100
|
+
expect(mod).to.have.property('handler').that.is.a('function')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('throws a generic error for non-handler modules without context', async () => {
|
|
104
|
+
const file = path.join(rootDir, 'bad-handler.js')
|
|
105
|
+
await writeModule(file, 'export default 123')
|
|
106
|
+
|
|
107
|
+
let err
|
|
108
|
+
try {
|
|
109
|
+
await importModule(file, { asHandler: true })
|
|
110
|
+
} catch (error) {
|
|
111
|
+
err = error
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(err).to.be.instanceOf(Error)
|
|
115
|
+
expect(err.message).to.equal(`File '${file}' is NOT a handler module`)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('throws contextual error for non-handler modules with context', async () => {
|
|
119
|
+
const file = path.join(rootDir, 'bad-handler-ctx.js')
|
|
120
|
+
await writeModule(file, 'export default 123')
|
|
121
|
+
const ctx = {
|
|
122
|
+
app: {
|
|
123
|
+
getPluginFile: () => file
|
|
124
|
+
},
|
|
125
|
+
error: (code, target) => new Error(`${code}:${target}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let err
|
|
129
|
+
try {
|
|
130
|
+
await importModule.call(ctx, 'any:file', { asHandler: true })
|
|
131
|
+
} catch (error) {
|
|
132
|
+
err = error
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(err).to.be.instanceOf(Error)
|
|
136
|
+
expect(err.message).to.equal(`fileNotModuleHandler%s:${file}`)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
import Base from '../class/base.js'
|
|
11
|
+
import Plugin from '../class/plugin.js'
|
|
12
|
+
import Log from '../class/log.js'
|
|
13
|
+
import boot from '../index.js'
|
|
14
|
+
|
|
15
|
+
const createTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'bajo-integration-test-'))
|
|
16
|
+
|
|
17
|
+
const cleanupNewProcessListeners = (beforeCounts) => {
|
|
18
|
+
const events = Object.keys(beforeCounts)
|
|
19
|
+
for (const event of events) {
|
|
20
|
+
const listeners = process.listeners(event)
|
|
21
|
+
const keep = beforeCounts[event]
|
|
22
|
+
if (listeners.length <= keep) continue
|
|
23
|
+
for (const listener of listeners.slice(keep)) {
|
|
24
|
+
process.removeListener(event, listener)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Integration', () => {
|
|
30
|
+
let rootDir
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
rootDir = createTempRoot()
|
|
34
|
+
await fs.writeJson(path.join(rootDir, 'package.json'), {
|
|
35
|
+
name: 'bajo-integration-app',
|
|
36
|
+
type: 'module'
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
if (rootDir) fs.rmSync(rootDir, { recursive: true, force: true })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('integrates App + Bajo + Plugin with real package info and ns-path file resolution', async () => {
|
|
45
|
+
const app = new App({ cwd: rootDir })
|
|
46
|
+
const bajo = new Bajo(app)
|
|
47
|
+
app.bajo = bajo
|
|
48
|
+
bajo.dir = {
|
|
49
|
+
base: rootDir,
|
|
50
|
+
data: path.join(rootDir, 'data')
|
|
51
|
+
}
|
|
52
|
+
await fs.ensureDir(bajo.dir.data)
|
|
53
|
+
|
|
54
|
+
const pluginDir = path.join(rootDir, 'plugins', 'demo-plugin')
|
|
55
|
+
await fs.ensureDir(pluginDir)
|
|
56
|
+
await fs.writeJson(path.join(pluginDir, 'package.json'), {
|
|
57
|
+
name: 'demo-plugin',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
description: 'integration plugin'
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const plugin = new Plugin('demo-plugin', app)
|
|
63
|
+
plugin.dir = { pkg: pluginDir }
|
|
64
|
+
app.addPlugin(plugin)
|
|
65
|
+
app.pluginPkgs = ['demo-plugin']
|
|
66
|
+
|
|
67
|
+
expect(plugin.getPkgInfo()).to.deep.equal({
|
|
68
|
+
name: 'demo-plugin',
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
description: 'integration plugin'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const dataDir = app.getPluginDataDir('demoPlugin')
|
|
74
|
+
expect(fs.existsSync(dataDir)).to.equal(true)
|
|
75
|
+
|
|
76
|
+
const resolved = app.getPluginFile('demoPlugin:/src/index.js')
|
|
77
|
+
expect(resolved).to.equal(path.join(pluginDir, 'src/index.js'))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('integrates Base.loadConfig with real config files', async () => {
|
|
81
|
+
const app = new App({ cwd: rootDir })
|
|
82
|
+
const bajo = new Bajo(app)
|
|
83
|
+
app.bajo = bajo
|
|
84
|
+
app.pluginPkgs = ['main']
|
|
85
|
+
|
|
86
|
+
bajo.dir = {
|
|
87
|
+
base: rootDir,
|
|
88
|
+
data: path.join(rootDir, 'data')
|
|
89
|
+
}
|
|
90
|
+
bajo.config = { env: 'dev' }
|
|
91
|
+
await fs.ensureDir(path.join(bajo.dir.data, 'config'))
|
|
92
|
+
bajo.config.log = {
|
|
93
|
+
level: 'silent',
|
|
94
|
+
save: false,
|
|
95
|
+
pretty: false,
|
|
96
|
+
useUtc: false,
|
|
97
|
+
timeTaken: false,
|
|
98
|
+
dateFormat: 'YYYY-MM-DD',
|
|
99
|
+
rotation: { cycle: 'none', byPlugin: false }
|
|
100
|
+
}
|
|
101
|
+
app.log = new Log(app)
|
|
102
|
+
await fs.writeJson(path.join(bajo.dir.data, 'config', 'main.json'), {
|
|
103
|
+
title: 'Integration Main',
|
|
104
|
+
main: {
|
|
105
|
+
feature: true,
|
|
106
|
+
port: 7000
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const main = new Base('main', app)
|
|
111
|
+
main.config = {
|
|
112
|
+
main: {
|
|
113
|
+
feature: false,
|
|
114
|
+
port: 3000
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
app.addPlugin(main)
|
|
118
|
+
|
|
119
|
+
await main.loadConfig()
|
|
120
|
+
|
|
121
|
+
expect(main.getConfig('main.feature')).to.equal(true)
|
|
122
|
+
expect(main.getConfig('main.port')).to.equal(7000)
|
|
123
|
+
expect(main.getConfig('title')).to.equal('Integration Main')
|
|
124
|
+
expect(main.dir.pkg).to.equal(path.join(rootDir, 'main'))
|
|
125
|
+
expect(main.dir.data).to.equal(path.join(bajo.dir.data, 'plugins', 'main'))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('integrates App cache save/load using Bajo ns parser and filesystem cache', async () => {
|
|
129
|
+
const app = new App({ cwd: rootDir })
|
|
130
|
+
const bajo = new Bajo(app)
|
|
131
|
+
app.bajo = bajo
|
|
132
|
+
bajo.dir = {
|
|
133
|
+
base: rootDir,
|
|
134
|
+
data: path.join(rootDir, 'data')
|
|
135
|
+
}
|
|
136
|
+
await fs.ensureDir(bajo.dir.data)
|
|
137
|
+
|
|
138
|
+
const content = { ok: true, items: [1, 2, 3] }
|
|
139
|
+
await app.cache.save('bajo.api:items/list', content, 60000)
|
|
140
|
+
const loaded = await app.cache.load('bajo.api:items/list', 60000)
|
|
141
|
+
|
|
142
|
+
expect(loaded).to.deep.equal(content)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('integrates real full boot flow through index entry with disk plugin', async () => {
|
|
146
|
+
const pluginName = 'integration-plugin'
|
|
147
|
+
const pluginDir = path.join(rootDir, 'node_modules', pluginName)
|
|
148
|
+
await fs.ensureDir(pluginDir)
|
|
149
|
+
await fs.writeJson(path.join(rootDir, 'package.json'), {
|
|
150
|
+
name: 'bajo-integration-app',
|
|
151
|
+
type: 'module',
|
|
152
|
+
bajo: {
|
|
153
|
+
plugins: [pluginName]
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
await fs.writeJson(path.join(pluginDir, 'package.json'), {
|
|
157
|
+
name: pluginName,
|
|
158
|
+
version: '1.0.0',
|
|
159
|
+
type: 'module',
|
|
160
|
+
main: 'index.js'
|
|
161
|
+
})
|
|
162
|
+
await fs.writeFile(path.join(pluginDir, 'index.js'), `
|
|
163
|
+
async function factory (pkgName) {
|
|
164
|
+
const me = this
|
|
165
|
+
|
|
166
|
+
return class IntegrationPlugin extends this.app.baseClass.Base {
|
|
167
|
+
constructor () {
|
|
168
|
+
super(pkgName, me.app)
|
|
169
|
+
this.config = { started: false }
|
|
170
|
+
this.start = async () => {
|
|
171
|
+
this.state.started = true
|
|
172
|
+
const dir = this.app.getPluginDataDir(this.ns)
|
|
173
|
+
this.app.lib.fs.writeFileSync(dir + '/started.txt', 'ok', 'utf8')
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default factory
|
|
180
|
+
`, 'utf8')
|
|
181
|
+
|
|
182
|
+
const dataDir = path.join(rootDir, 'data', 'config')
|
|
183
|
+
await fs.ensureDir(dataDir)
|
|
184
|
+
await fs.writeJson(path.join(dataDir, 'bajo.json'), {
|
|
185
|
+
log: { level: 'silent', save: false },
|
|
186
|
+
exitHandler: false,
|
|
187
|
+
cache: { purgeIntvDur: '1h' }
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const events = ['SIGINT', 'SIGTERM', 'beforeExit', 'uncaughtException', 'unhandledRejection', 'warning']
|
|
191
|
+
const beforeCounts = events.reduce((acc, event) => {
|
|
192
|
+
acc[event] = process.listeners(event).length
|
|
193
|
+
return acc
|
|
194
|
+
}, {})
|
|
195
|
+
|
|
196
|
+
const originalSetInterval = global.setInterval
|
|
197
|
+
const createdIntervals = []
|
|
198
|
+
global.setInterval = (fn, delay, ...args) => {
|
|
199
|
+
const id = originalSetInterval(fn, delay, ...args)
|
|
200
|
+
createdIntervals.push(id)
|
|
201
|
+
return id
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const app = await boot({ cwd: rootDir })
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
expect(app).to.be.an('object')
|
|
208
|
+
expect(app.integrationPlugin).to.be.an('object')
|
|
209
|
+
const startedFile = path.join(rootDir, 'data', 'plugins', 'integrationPlugin', 'started.txt')
|
|
210
|
+
expect(fs.existsSync(startedFile)).to.equal(true)
|
|
211
|
+
expect(fs.readFileSync(startedFile, 'utf8')).to.equal('ok')
|
|
212
|
+
} finally {
|
|
213
|
+
global.setInterval = originalSetInterval
|
|
214
|
+
for (const id of createdIntervals) clearInterval(id)
|
|
215
|
+
cleanupNewProcessListeners(beforeCounts)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
})
|