bajo 1.0.11 → 1.1.1
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/bajo/intl/en-US.json +5 -2
- package/bajo/intl/id.json +5 -2
- package/boot/class/app.js +11 -13
- package/boot/class/bajo-core/{helper/boot-order.js → boot-order.js} +3 -4
- package/boot/class/bajo-core/boot-plugins.js +19 -0
- package/boot/class/bajo-core/{helper/build-config.js → build-config.js} +16 -18
- package/boot/class/bajo-core/{helper/build-plugins.js → build-plugins.js} +13 -5
- package/boot/class/bajo-core/{helper/collect-config-handlers.js → collect-config-handlers.js} +2 -4
- package/boot/class/bajo-core.js +738 -11
- package/boot/class/bajo-plugin/{helper/attach-method.js → attach-method.js} +2 -2
- package/boot/class/bajo-plugin/{helper/collect-exit-handlers.js → collect-exit-handlers.js} +1 -1
- package/boot/class/bajo-plugin.js +13 -14
- package/boot/class/error.js +10 -2
- package/boot/class/log.js +9 -10
- package/boot/class/plugin.js +8 -12
- package/boot/class/print.js +16 -17
- package/boot/lib/create-method.js +3 -5
- package/boot/lib/current-loc.js +1 -1
- package/boot/lib/import-module.js +27 -0
- package/boot/lib/log-levels.js +1 -0
- package/boot/lib/parse-args-argv.js +2 -3
- package/boot/lib/read-all-configs.js +3 -6
- package/boot/{class/bajo-core/method → lib}/resolve-path.js +1 -1
- package/package.json +1 -1
- package/boot/class/bajo-core/helper/attach-method.js +0 -31
- package/boot/class/bajo-core/helper/boot-plugins.js +0 -19
- package/boot/class/bajo-core/method/arrange-array.js +0 -19
- package/boot/class/bajo-core/method/break-ns-path-from-file.js +0 -25
- package/boot/class/bajo-core/method/break-ns-path.js +0 -31
- package/boot/class/bajo-core/method/build-collections.js +0 -48
- package/boot/class/bajo-core/method/call-handler.js +0 -31
- package/boot/class/bajo-core/method/defaults-deep.js +0 -17
- package/boot/class/bajo-core/method/each-plugins.js +0 -57
- package/boot/class/bajo-core/method/envs.js +0 -7
- package/boot/class/bajo-core/method/extract-text.js +0 -15
- package/boot/class/bajo-core/method/format.js +0 -36
- package/boot/class/bajo-core/method/generate-id.js +0 -23
- package/boot/class/bajo-core/method/get-global-module-dir.js +0 -28
- package/boot/class/bajo-core/method/get-key-by-value.js +0 -5
- package/boot/class/bajo-core/method/get-method.js +0 -12
- package/boot/class/bajo-core/method/get-module-dir.js +0 -35
- package/boot/class/bajo-core/method/get-plugin-data-dir.js +0 -11
- package/boot/class/bajo-core/method/get-plugin-file.js +0 -21
- package/boot/class/bajo-core/method/get-plugin.js +0 -23
- package/boot/class/bajo-core/method/import-module.js +0 -27
- package/boot/class/bajo-core/method/import-pkg.js +0 -48
- package/boot/class/bajo-core/method/includes.js +0 -11
- package/boot/class/bajo-core/method/is-class.js +0 -7
- package/boot/class/bajo-core/method/is-empty-dir.js +0 -9
- package/boot/class/bajo-core/method/is-log-in-range.js +0 -12
- package/boot/class/bajo-core/method/is-set.js +0 -5
- package/boot/class/bajo-core/method/is-valid-app.js +0 -12
- package/boot/class/bajo-core/method/is-valid-plugin.js +0 -12
- package/boot/class/bajo-core/method/join.js +0 -20
- package/boot/class/bajo-core/method/log-levels.js +0 -9
- package/boot/class/bajo-core/method/num-unit.js +0 -11
- package/boot/class/bajo-core/method/paginate.js +0 -28
- package/boot/class/bajo-core/method/parse-object.js +0 -62
- package/boot/class/bajo-core/method/pascal-case.js +0 -9
- package/boot/class/bajo-core/method/pick.js +0 -13
- package/boot/class/bajo-core/method/read-config.js +0 -50
- package/boot/class/bajo-core/method/read-json.js +0 -14
- package/boot/class/bajo-core/method/round.js +0 -6
- package/boot/class/bajo-core/method/run-hook.js +0 -28
- package/boot/class/bajo-core/method/save-as-download.js +0 -19
- package/boot/class/bajo-core/method/sec-to-hms.js +0 -25
- package/boot/class/bajo-core/method/slice-string.js +0 -13
- package/boot/class/bajo-core/method/titleize.js +0 -24
- package/boot/class/bajo-core/method/white-space.js +0 -3
- package/boot/lib/translate.js +0 -19
- /package/boot/class/bajo-core/{helper/exit-handler.js → exit-handler.js} +0 -0
- /package/boot/class/bajo-core/{helper/run-as-applet.js → run-as-applet.js} +0 -0
- /package/boot/class/bajo-plugin/{helper/build-config.js → build-config.js} +0 -0
- /package/boot/class/bajo-plugin/{helper/check-clash.js → check-clash.js} +0 -0
- /package/boot/class/bajo-plugin/{helper/check-dependency.js → check-dependency.js} +0 -0
- /package/boot/class/bajo-plugin/{helper/collect-hooks.js → collect-hooks.js} +0 -0
- /package/boot/class/bajo-plugin/{helper/run.js → run.js} +0 -0
package/boot/class/bajo-core.js
CHANGED
|
@@ -1,30 +1,757 @@
|
|
|
1
1
|
import Plugin from './plugin.js'
|
|
2
|
+
import BajoPlugin from './bajo-plugin.js'
|
|
2
3
|
import dayjs from '../lib/dayjs.js'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import increment from 'add-filename-increment'
|
|
5
|
+
import fs from 'fs-extra'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import os from 'os'
|
|
8
|
+
import ms from 'ms'
|
|
9
|
+
import dotenvParseVariables from 'dotenv-parse-variables'
|
|
10
|
+
import emptyDir from 'empty-dir'
|
|
6
11
|
import lodash from 'lodash'
|
|
7
|
-
|
|
12
|
+
import currentLoc from '../lib/current-loc.js'
|
|
13
|
+
import { createRequire } from 'module'
|
|
14
|
+
import getGlobalPath from 'get-global-path'
|
|
15
|
+
import { customAlphabet } from 'nanoid'
|
|
16
|
+
import fastGlob from 'fast-glob'
|
|
17
|
+
import querystring from 'querystring'
|
|
18
|
+
import deepFreeze from 'deep-freeze-strict'
|
|
19
|
+
import { sprintf } from 'sprintf-js'
|
|
20
|
+
import outmatch from 'outmatch'
|
|
21
|
+
import resolvePath from '../lib/resolve-path.js'
|
|
22
|
+
import importModule from '../lib/import-module.js'
|
|
23
|
+
import logLevels from '../lib/log-levels.js'
|
|
8
24
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
const require = createRequire(import.meta.url)
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
isFunction, words, upperFirst, map, concat, uniq, forOwn, padStart,
|
|
29
|
+
trim, filter, isEmpty, orderBy, pullAt, find, camelCase, isNumber,
|
|
30
|
+
cloneDeep, isPlainObject, isArray, isString, set, omit, keys, indexOf,
|
|
31
|
+
last, get, has, values, dropRight, mergeWith
|
|
32
|
+
} = lodash
|
|
14
33
|
|
|
15
34
|
class BajoCore extends Plugin {
|
|
16
35
|
constructor (app) {
|
|
17
36
|
super('bajo', app)
|
|
18
37
|
this.runAt = new Date()
|
|
19
38
|
this.mainNs = 'main'
|
|
39
|
+
this.lib._ = lodash
|
|
40
|
+
this.lib.fs = fs
|
|
41
|
+
this.lib.fastGlob = fastGlob
|
|
42
|
+
this.lib.sprintf = sprintf
|
|
43
|
+
this.lib.outmatch = outmatch
|
|
20
44
|
this.lib.dayjs = dayjs
|
|
45
|
+
this.lib.BajoPlugin = BajoPlugin
|
|
21
46
|
this.applets = []
|
|
22
47
|
this.pluginPkgs = []
|
|
23
48
|
this.pluginNames = []
|
|
24
49
|
this.configHandlers = [
|
|
25
|
-
{ ext: '.js', readHandler:
|
|
26
|
-
{ ext: '.json', readHandler: readJson }
|
|
50
|
+
{ ext: '.js', readHandler: this._defConfigHandler },
|
|
51
|
+
{ ext: '.json', readHandler: this.readJson }
|
|
27
52
|
]
|
|
53
|
+
this.whiteSpace = [' ', '\t', '\n', '\r']
|
|
54
|
+
this.logLevels = logLevels
|
|
55
|
+
this.envs = { dev: 'development', staging: 'staging', prod: 'production' }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async _defConfigHandler (file, opts = {}) {
|
|
59
|
+
let mod = await importModule(file)
|
|
60
|
+
if (isFunction(mod)) mod = await mod.call(this, opts)
|
|
61
|
+
return mod
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
resolvePath = (item, asFileUrl) => {
|
|
65
|
+
return resolvePath(item, asFileUrl)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
freeze = (o, shallow) => {
|
|
69
|
+
if (shallow) Object.freeze(o)
|
|
70
|
+
else deepFreeze(o)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setImmediate = async () => {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
setImmediate(() => resolve())
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
arrangeArray = (inputs, trimItem = true) => {
|
|
80
|
+
const first = []
|
|
81
|
+
const last = []
|
|
82
|
+
|
|
83
|
+
const items = filter(inputs, item => {
|
|
84
|
+
if (trimItem) item = trim(item)
|
|
85
|
+
if (item[0] === '^') first.push(item.slice(1))
|
|
86
|
+
else if (item[0] === '$') last.push(item.slice(1))
|
|
87
|
+
return !['^', '$'].includes(item[0])
|
|
88
|
+
})
|
|
89
|
+
items.unshift(...first)
|
|
90
|
+
items.push(...last)
|
|
91
|
+
return items
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
breakNsPathFromFile = ({ file, dir, baseNs, suffix = '', getType } = {}) => {
|
|
95
|
+
let item = file.replace(dir + suffix, '')
|
|
96
|
+
let type
|
|
97
|
+
if (getType) {
|
|
98
|
+
const items = item.split('/')
|
|
99
|
+
type = items.shift()
|
|
100
|
+
item = items.join('/')
|
|
101
|
+
}
|
|
102
|
+
item = item.slice(0, item.length - path.extname(item).length)
|
|
103
|
+
let [name, _path] = item.split('@')
|
|
104
|
+
if (!_path) {
|
|
105
|
+
_path = name
|
|
106
|
+
name = baseNs
|
|
107
|
+
}
|
|
108
|
+
_path = camelCase(_path)
|
|
109
|
+
const names = map(name.split('.'), n => camelCase(n))
|
|
110
|
+
const [ns, subNs] = names
|
|
111
|
+
return { ns, subNs, path: _path, fullNs: names.join('.'), type }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
breakNsPath = (item = '', defaultNs = 'bajo', checkNs = true) => {
|
|
115
|
+
let [ns, ...path] = item.split(':')
|
|
116
|
+
let subNs
|
|
117
|
+
let subSubNs
|
|
118
|
+
path = path.join(':')
|
|
119
|
+
if (path.startsWith('//')) return { ns: undefined, path: item } // for: http:// etc
|
|
120
|
+
if (isEmpty(path)) {
|
|
121
|
+
path = ns
|
|
122
|
+
ns = defaultNs
|
|
123
|
+
}
|
|
124
|
+
[ns, subNs, subSubNs] = ns.split('.')
|
|
125
|
+
if (checkNs) {
|
|
126
|
+
if (!this.app[ns]) {
|
|
127
|
+
const plugin = this.getPlugin(ns)
|
|
128
|
+
if (plugin) ns = plugin.name
|
|
129
|
+
}
|
|
130
|
+
if (!this.app[ns]) throw this.error('unknownPluginOrNotLoaded%s')
|
|
131
|
+
}
|
|
132
|
+
const fullPath = path
|
|
133
|
+
let qs
|
|
134
|
+
[path, qs] = path.split('?')
|
|
135
|
+
qs = querystring.parse(qs) ?? {}
|
|
136
|
+
return { ns, path, subNs, subSubNs, qs, fullPath }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
buildCollections = async (options = {}) => {
|
|
140
|
+
let { ns, handler, dupChecks = [], container, useDefaultName } = options
|
|
141
|
+
useDefaultName = useDefaultName ?? true
|
|
142
|
+
if (!ns) ns = this.name
|
|
143
|
+
const cfg = this.app[ns].getConfig()
|
|
144
|
+
let items = get(cfg, container, [])
|
|
145
|
+
if (!isArray(items)) items = [items]
|
|
146
|
+
this.app[ns].log.trace('collecting%s', this.app[ns].print.write(container))
|
|
147
|
+
await this.runHook(`${ns}:${camelCase('beforeBuildCollection')}`, container)
|
|
148
|
+
const deleted = []
|
|
149
|
+
for (const index in items) {
|
|
150
|
+
const item = items[index]
|
|
151
|
+
if (useDefaultName) {
|
|
152
|
+
if (!has(item, 'name')) {
|
|
153
|
+
if (find(items, { name: 'default' })) throw this.app[ns].error('collExists%s', 'default')
|
|
154
|
+
else item.name = 'default'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.app[ns].log.trace('- %s', item.name)
|
|
158
|
+
const result = await handler.call(this.app[ns], { item, index, cfg })
|
|
159
|
+
if (result) items[index] = result
|
|
160
|
+
else if (result === false) deleted.push(index)
|
|
161
|
+
if (this.app.bajo.applet && item.skipOnTool && !deleted.includes(index)) deleted.push(index)
|
|
162
|
+
}
|
|
163
|
+
if (deleted.length > 0) pullAt(items, deleted)
|
|
164
|
+
|
|
165
|
+
// check for duplicity
|
|
166
|
+
for (const c of items) {
|
|
167
|
+
for (const d of dupChecks) {
|
|
168
|
+
if (isFunction(d)) await d.call(this.app[ns], c, items)
|
|
169
|
+
else {
|
|
170
|
+
const checker = set({}, d, c[d])
|
|
171
|
+
const match = filter(items, checker)
|
|
172
|
+
if (match.length > 1) this.app[ns].fatal('oneOrMoreSharedTheSame%s%s', container, this.join(dupChecks.filter(i => !isFunction(i))))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
await this.runHook(`${ns}:${camelCase('afterBuildCollection')}`, container)
|
|
177
|
+
this.app[ns].log.debug('collected%s%d', this.app[ns].print.write(container), items.length)
|
|
178
|
+
return items
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
callHandler = async (item, ...args) => {
|
|
182
|
+
let result
|
|
183
|
+
let scope = this
|
|
184
|
+
if (item instanceof BajoPlugin) {
|
|
185
|
+
scope = item
|
|
186
|
+
item = args.shift()
|
|
187
|
+
}
|
|
188
|
+
const bajo = scope.app.bajo
|
|
189
|
+
if (isString(item)) {
|
|
190
|
+
if (item.startsWith('applet:') && bajo.applets.length > 0) {
|
|
191
|
+
const [, ns, path] = item.split(':')
|
|
192
|
+
const applet = find(bajo.applets, a => (a.ns === ns || a.alias === ns))
|
|
193
|
+
if (applet) result = await bajo.runApplet(applet, path, ...args)
|
|
194
|
+
} else {
|
|
195
|
+
const method = bajo.getMethod(item)
|
|
196
|
+
if (method) result = await method(...args)
|
|
197
|
+
}
|
|
198
|
+
} else if (isFunction(item)) {
|
|
199
|
+
result = await item.call(scope, ...args)
|
|
200
|
+
} else if (isPlainObject(item) && item.handler) {
|
|
201
|
+
result = await item.handler.call(scope, ...args)
|
|
202
|
+
}
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
defaultsDeep = (...args) => {
|
|
207
|
+
const output = {}
|
|
208
|
+
args.reverse().forEach(function (item) {
|
|
209
|
+
mergeWith(output, item, function (objectValue, sourceValue) {
|
|
210
|
+
return isArray(sourceValue) ? sourceValue : undefined
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
return output
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
eachPlugins = async (handler, options = {}) => {
|
|
217
|
+
if (typeof options === 'string') options = { glob: options }
|
|
218
|
+
const result = {}
|
|
219
|
+
const pluginPkgs = cloneDeep(this.app.bajo.pluginPkgs) ?? []
|
|
220
|
+
const { glob, useBajo, prefix = '', noUnderscore = true, returnItems } = options
|
|
221
|
+
if (useBajo) pluginPkgs.unshift('bajo')
|
|
222
|
+
for (const pkgName of pluginPkgs) {
|
|
223
|
+
const ns = camelCase(pkgName)
|
|
224
|
+
const config = this.app[ns].config
|
|
225
|
+
const alias = this.app[ns].alias
|
|
226
|
+
let r
|
|
227
|
+
if (glob) {
|
|
228
|
+
const base = prefix === '' ? this.app[ns].dir.pkg : `${this.app[ns].dir.pkg}/${prefix}`
|
|
229
|
+
let opts = isString(glob) ? { pattern: [glob] } : glob
|
|
230
|
+
let pattern = opts.pattern ?? []
|
|
231
|
+
if (isString(pattern)) pattern = [pattern]
|
|
232
|
+
opts = omit(opts, ['pattern'])
|
|
233
|
+
for (const i in pattern) {
|
|
234
|
+
if (!path.isAbsolute(pattern[i])) pattern[i] = `${base}/${pattern[i]}`
|
|
235
|
+
}
|
|
236
|
+
const files = await fastGlob(pattern, opts)
|
|
237
|
+
for (const f of files) {
|
|
238
|
+
if (path.basename(f)[0] === '_' && noUnderscore) continue
|
|
239
|
+
const resp = await handler.call(this.app[ns], { ns, pkgName, config, alias, file: f, dir: base })
|
|
240
|
+
if (resp === false) break
|
|
241
|
+
else if (resp === undefined) continue
|
|
242
|
+
else {
|
|
243
|
+
result[ns] = result[ns] ?? {}
|
|
244
|
+
result[ns][f] = resp
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
r = await handler.call(this.app[ns], { ns, pkgName, config, dir: this.app[ns].dir.pkg, alias })
|
|
249
|
+
if (r === false) break
|
|
250
|
+
else if (r === undefined) continue
|
|
251
|
+
else result[ns] = r
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (returnItems) {
|
|
255
|
+
const data = []
|
|
256
|
+
for (const r in result) {
|
|
257
|
+
for (const f in result[r]) {
|
|
258
|
+
data.push(result[r][f])
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return data
|
|
262
|
+
}
|
|
263
|
+
return result
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
extractText = (text, patternStart, patternEnd) => {
|
|
267
|
+
let result = ''
|
|
268
|
+
const open = text.indexOf(patternStart)
|
|
269
|
+
if (open > -1) {
|
|
270
|
+
text = text.slice(open + patternStart.length)
|
|
271
|
+
const close = text.indexOf(patternEnd)
|
|
272
|
+
if (close > -1) {
|
|
273
|
+
result = text.slice(0, close)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const pattern = `${patternStart}${result}${patternEnd}`
|
|
277
|
+
return { result, pattern }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
format = (value, type, options = {}) => {
|
|
281
|
+
const { format } = this.config.intl
|
|
282
|
+
const { emptyValue = format.emptyValue } = options
|
|
283
|
+
const lang = options.lang ?? this.config.lang
|
|
284
|
+
if ([undefined, null, ''].includes(value)) return emptyValue
|
|
285
|
+
if (type === 'auto') {
|
|
286
|
+
if (value instanceof Date) type = 'datetime'
|
|
287
|
+
}
|
|
288
|
+
if (['integer', 'smallint'].includes(type)) {
|
|
289
|
+
value = parseInt(value)
|
|
290
|
+
if (isNaN(value)) return emptyValue
|
|
291
|
+
const setting = this.defaultsDeep(options.integer, format.integer)
|
|
292
|
+
return new Intl.NumberFormat(lang, setting).format(value)
|
|
293
|
+
}
|
|
294
|
+
if (['float', 'double'].includes(type)) {
|
|
295
|
+
value = parseFloat(value)
|
|
296
|
+
if (isNaN(value)) return emptyValue
|
|
297
|
+
if (this.app.bajoSpatial && options.latitude) return this.app.bajoSpatial.latToDms(value)
|
|
298
|
+
if (this.app.bajoSpatial && options.longitude) return this.app.bajoSpatial.lngToDms(value)
|
|
299
|
+
const setting = this.defaultsDeep(options.float, format.float)
|
|
300
|
+
return new Intl.NumberFormat(lang, setting).format(value)
|
|
301
|
+
}
|
|
302
|
+
if (['datetime', 'date'].includes(type)) {
|
|
303
|
+
const setting = this.defaultsDeep(options[type], format[type])
|
|
304
|
+
return new Intl.DateTimeFormat(lang, setting).format(new Date(value))
|
|
305
|
+
}
|
|
306
|
+
if (['time'].includes(type)) {
|
|
307
|
+
const setting = this.defaultsDeep(options.time, format.time)
|
|
308
|
+
return new Intl.DateTimeFormat(lang, setting).format(new Date(`1970-01-01T${value}Z`))
|
|
309
|
+
}
|
|
310
|
+
if (['array'].includes(type)) return value.join(', ')
|
|
311
|
+
if (['object'].includes(type)) return JSON.stringify(value)
|
|
312
|
+
return value
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
generateId = (options = {}) => {
|
|
316
|
+
let type
|
|
317
|
+
if (options === true) options = 'alpha'
|
|
318
|
+
if (options === 'int') {
|
|
319
|
+
type = options
|
|
320
|
+
options = { pattern: '0123456789', length: 15 }
|
|
321
|
+
} else if (options === 'alpha') {
|
|
322
|
+
type = options
|
|
323
|
+
options = { pattern: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', length: 15 }
|
|
324
|
+
}
|
|
325
|
+
let { pattern, length = 13, returnInstance } = options
|
|
326
|
+
pattern = pattern ?? 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
327
|
+
if (options.case === 'lower') pattern = pattern.toLowerCase()
|
|
328
|
+
else if (options.case === 'upper') pattern = pattern.toUpperCase()
|
|
329
|
+
const nid = customAlphabet(pattern, length)
|
|
330
|
+
if (returnInstance) return nid
|
|
331
|
+
const value = nid()
|
|
332
|
+
return type === 'int' ? parseInt(value) : value
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
getGlobalModuleDir = (pkgName, silent = true) => {
|
|
336
|
+
let nodeModulesDir = process.env.BAJO_GLOBAL_MODULE_DIR
|
|
337
|
+
if (!nodeModulesDir) {
|
|
338
|
+
const npmPath = getGlobalPath('npm')
|
|
339
|
+
if (!npmPath) {
|
|
340
|
+
if (silent) return
|
|
341
|
+
throw this.error('cantLocateNpmGlobalDir', { code: 'BAJO_CANT_LOCATE_NPM_GLOBAL_DIR' })
|
|
342
|
+
}
|
|
343
|
+
nodeModulesDir = dropRight(resolvePath(npmPath).split('/'), 1).join('/')
|
|
344
|
+
process.env.BAJO_GLOBAL_MODULE_DIR = nodeModulesDir
|
|
345
|
+
}
|
|
346
|
+
if (!pkgName) return nodeModulesDir
|
|
347
|
+
const dir = `${nodeModulesDir}/${pkgName}`
|
|
348
|
+
if (!fs.existsSync(dir)) {
|
|
349
|
+
if (silent) return
|
|
350
|
+
throw this.error('cantLocateGlobalDir%s', pkgName, { code: 'BAJO_CANT_LOCATE_MODULE_GLOBAL_DIR' })
|
|
351
|
+
}
|
|
352
|
+
return dir
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
getKeyByValue = (object, value) => {
|
|
356
|
+
return Object.keys(object).find(key => object[key] === value)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getMethod = (name = '', thrown = true) => {
|
|
360
|
+
const { ns, path } = this.breakNsPath(name)
|
|
361
|
+
const method = get(this.app, `${ns}.${path}`)
|
|
362
|
+
if (method && isFunction(method)) return method
|
|
363
|
+
if (thrown) throw this.error('cantFindMethod%s', name)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
findDeep = (item, paths) => {
|
|
367
|
+
let dir
|
|
368
|
+
for (const p of paths) {
|
|
369
|
+
const d = `${p}/${item}`
|
|
370
|
+
if (fs.existsSync(d)) {
|
|
371
|
+
dir = d
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return dir
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getModuleDir = (pkgName, base) => {
|
|
379
|
+
if (pkgName === 'main') return resolvePath(process.env.BAJOCWD)
|
|
380
|
+
if (base === 'main') base = process.env.BAJOCWD
|
|
381
|
+
else if (this && this.app && this.app[base]) base = this.app[base].pkgName
|
|
382
|
+
const pkgPath = pkgName + '/package.json'
|
|
383
|
+
const paths = require.resolve.paths(pkgPath)
|
|
384
|
+
const gdir = this.getGlobalModuleDir()
|
|
385
|
+
paths.unshift(gdir)
|
|
386
|
+
paths.unshift(resolvePath(path.join(process.env.BAJOCWD, 'node_modules')))
|
|
387
|
+
let dir = this.findDeep(pkgPath, paths)
|
|
388
|
+
if (base && !dir) dir = this.findDeep(`${base}/node_modules/${pkgPath}`, paths)
|
|
389
|
+
if (!dir) return null
|
|
390
|
+
return resolvePath(path.dirname(dir))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
getPluginDataDir = (name, ensureDir = true) => {
|
|
394
|
+
const { getPlugin } = this.app.bajo
|
|
395
|
+
const plugin = getPlugin(name)
|
|
396
|
+
const dir = `${this.app.bajo.dir.data}/plugins/${plugin.name}`
|
|
397
|
+
if (ensureDir) fs.ensureDirSync(dir)
|
|
398
|
+
return dir
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
getPluginFile = (file) => {
|
|
402
|
+
if (!this) return file
|
|
403
|
+
if (file[0] === '.') file = `${currentLoc(import.meta).dir}/${trim(file.slice(1), '/')}`
|
|
404
|
+
if (file.includes(':')) {
|
|
405
|
+
if (file.slice(1, 2) === ':') return file // windows fs
|
|
406
|
+
const { ns, path } = this.breakNsPath(file)
|
|
407
|
+
if (ns !== 'file' && this && this.app && this.app[ns] && ns.length > 1) {
|
|
408
|
+
file = `${this.app[ns].dir.pkg}${path}`
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return file
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getPlugin = (name, silent) => {
|
|
415
|
+
if (!this.app[name]) {
|
|
416
|
+
// alias?
|
|
417
|
+
let plugin
|
|
418
|
+
for (const key in this.app) {
|
|
419
|
+
const item = this.app[key]
|
|
420
|
+
if (item instanceof BajoPlugin && (item.alias === name || item.pkgName === name)) {
|
|
421
|
+
plugin = item
|
|
422
|
+
break
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!plugin) {
|
|
426
|
+
if (silent) return false
|
|
427
|
+
throw this.error('pluginWithNameAliasNotLoaded%s', name)
|
|
428
|
+
}
|
|
429
|
+
name = plugin.name
|
|
430
|
+
}
|
|
431
|
+
return this.app[name]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
importModule = async (file, { asDefaultImport, asHandler, noCache } = {}) => {
|
|
435
|
+
return await importModule.call(this, file, { asDefaultImport, asHandler, noCache })
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
importPkg = async (...pkgs) => {
|
|
439
|
+
const result = {}
|
|
440
|
+
const notFound = []
|
|
441
|
+
let opts = { returnDefault: true, thrownNotFound: false }
|
|
442
|
+
if (isPlainObject(last(pkgs))) {
|
|
443
|
+
opts = this.defaultsDeep(pkgs.pop(), opts)
|
|
444
|
+
}
|
|
445
|
+
for (const pkg of pkgs) {
|
|
446
|
+
const { ns, path: name } = this.breakNsPath(pkg)
|
|
447
|
+
const dir = this.getModuleDir(name, ns)
|
|
448
|
+
if (!dir) {
|
|
449
|
+
notFound.push(pkg)
|
|
450
|
+
continue
|
|
451
|
+
}
|
|
452
|
+
const p = this.readJson(`${dir}/package.json`, opts.thrownNotFound)
|
|
453
|
+
const mainFileOrg = dir + '/' + (p.main ?? get(p, 'exports.default', 'index.js'))
|
|
454
|
+
let mainFile = resolvePath(mainFileOrg, os.platform() === 'win32')
|
|
455
|
+
if (isEmpty(path.extname(mainFile))) {
|
|
456
|
+
if (fs.existsSync(`${mainFileOrg}/index.js`)) mainFile += '/index.js'
|
|
457
|
+
else mainFile += '.js'
|
|
458
|
+
}
|
|
459
|
+
if (opts.noCache) mainFile += `?_=${Date.now()}`
|
|
460
|
+
let mod = await import(mainFile)
|
|
461
|
+
if (opts.returnDefault && has(mod, 'default')) {
|
|
462
|
+
mod = mod.default
|
|
463
|
+
if (opts.returnDefault && has(mod, 'default')) mod = mod.default
|
|
464
|
+
}
|
|
465
|
+
result[name] = mod
|
|
466
|
+
}
|
|
467
|
+
if (notFound.length > 0) throw this.error('cantFind%s', this.join(notFound))
|
|
468
|
+
if (pkgs.length === 1) return result[keys(result)[0]]
|
|
469
|
+
if (opts.asObject) return result
|
|
470
|
+
return values(result)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
includes = (matcher = [], array = []) => {
|
|
474
|
+
if (typeof matcher === 'string') matcher = [matcher]
|
|
475
|
+
let found = false
|
|
476
|
+
for (const m of matcher) {
|
|
477
|
+
found = array.includes(m)
|
|
478
|
+
if (found) break
|
|
479
|
+
}
|
|
480
|
+
return found
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
isClass = (item) => {
|
|
484
|
+
return typeof item === 'function' &&
|
|
485
|
+
Object.prototype.hasOwnProperty.call(item, 'prototype') &&
|
|
486
|
+
!Object.prototype.hasOwnProperty.call(item, 'arguments')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
isEmptyDir = async (dir) => {
|
|
490
|
+
await fs.exists(dir)
|
|
491
|
+
return await emptyDir(dir)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
isLogInRange = (level) => {
|
|
495
|
+
const levels = keys(this.logLevels)
|
|
496
|
+
const logLevel = indexOf(levels, this.app.bajo.config.log.level)
|
|
497
|
+
return indexOf(levels, level) >= logLevel
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
isSet = (input) => {
|
|
501
|
+
return ![null, undefined].includes(input)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
isValidApp = (dir) => {
|
|
505
|
+
if (!dir) dir = process.env.BAJOCWD
|
|
506
|
+
dir = resolvePath(dir)
|
|
507
|
+
const hasMainDir = fs.existsSync(`${dir}/main/plugin`)
|
|
508
|
+
const hasPackageJson = fs.existsSync(`${dir}/package.json`)
|
|
509
|
+
return hasMainDir && hasPackageJson
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
isValidPlugin = (dir) => {
|
|
513
|
+
if (!dir) dir = process.env.BAJOCWD
|
|
514
|
+
dir = resolvePath(dir)
|
|
515
|
+
const hasPluginDir = fs.existsSync(`${dir}/plugin`)
|
|
516
|
+
const hasPackageJson = fs.existsSync(`${dir}/package.json`)
|
|
517
|
+
return hasPluginDir && hasPackageJson
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
join = (array, sep) => {
|
|
521
|
+
const translate = val => {
|
|
522
|
+
if (this && this.print) return this.print.write(val).toLowerCase()
|
|
523
|
+
return val
|
|
524
|
+
}
|
|
525
|
+
if (array.length === 0) return translate('none')
|
|
526
|
+
if (array.length === 1) return array[0]
|
|
527
|
+
if (this.isSet(sep) && !isPlainObject(sep)) return array.join(sep)
|
|
528
|
+
let { separator = ', ', joiner = 'and' } = sep ?? {}
|
|
529
|
+
joiner = translate(joiner)
|
|
530
|
+
const last = (array.pop() ?? '').trim()
|
|
531
|
+
return array.map(a => (a + '').trim()).join(separator) + ` ${joiner} ${last}`
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
numUnit = (value = '', defUnit = '') => {
|
|
535
|
+
const num = value.match(/\d+/g)
|
|
536
|
+
const unit = value.match(/[a-zA-Z]+/g)
|
|
537
|
+
return `${num[0]}${isEmpty(unit) ? defUnit : unit[0]}`
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
paginate = (collection, { page = 1, limit = 25, sort } = {}) => {
|
|
541
|
+
const count = collection.length
|
|
542
|
+
const offset = (page - 1) * limit
|
|
543
|
+
const fields = []
|
|
544
|
+
const dirs = []
|
|
545
|
+
if (isPlainObject(sort)) {
|
|
546
|
+
forOwn(sort, (v, k) => {
|
|
547
|
+
fields.push(k)
|
|
548
|
+
dirs.push(v < 0 ? 'desc' : 'asc')
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
if (!isEmpty(fields)) collection = orderBy(collection, fields, dirs)
|
|
552
|
+
const data = collection.slice(offset, offset + limit)
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
data,
|
|
556
|
+
page,
|
|
557
|
+
limit,
|
|
558
|
+
count,
|
|
559
|
+
pages: Math.ceil(collection.length / limit)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
parseDur = (val) => {
|
|
564
|
+
return isNumber(val) ? val : ms(val)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
parseDt = (val) => {
|
|
568
|
+
const dt = this.lib.dayjs(val)
|
|
569
|
+
if (!dt.isValid()) throw this.error('dtUnparsable%s', val)
|
|
570
|
+
return dt.toDate()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
parseObject = (input, { silent = true, parseValue = false, lang, ns } = {}) => {
|
|
574
|
+
const statics = ['*']
|
|
575
|
+
let obj = cloneDeep(input)
|
|
576
|
+
const keys = Object.keys(obj)
|
|
577
|
+
const me = this
|
|
578
|
+
const mutated = []
|
|
579
|
+
keys.forEach(k => {
|
|
580
|
+
const v = obj[k]
|
|
581
|
+
if (isPlainObject(v)) obj[k] = this.parseObject(v)
|
|
582
|
+
else if (isArray(v)) {
|
|
583
|
+
v.forEach((i, idx) => {
|
|
584
|
+
if (isPlainObject(i)) obj[k][idx] = this.parseObject(i)
|
|
585
|
+
else if (statics.includes(i)) obj[k][idx] = i
|
|
586
|
+
else if (parseValue) obj[k][idx] = dotenvParseVariables(set({}, 'item', obj[k][idx]), { assignToProcessEnv: false }).item
|
|
587
|
+
if (isArray(obj[k][idx])) obj[k][idx] = obj[k][idx].map(item => typeof item === 'string' ? item.trim() : item)
|
|
588
|
+
})
|
|
589
|
+
} else if (this.isSet(v)) {
|
|
590
|
+
try {
|
|
591
|
+
if (statics.includes(v)) obj[k] = v
|
|
592
|
+
else if (k.startsWith('t:') && isString(v)) {
|
|
593
|
+
const newK = k.slice(2)
|
|
594
|
+
if (lang) {
|
|
595
|
+
const scope = ns ? me.app[ns] : me
|
|
596
|
+
const [text, ...args] = v.split('|')
|
|
597
|
+
obj[newK] = scope.print.write(text, ...args, { lang })
|
|
598
|
+
} else obj[newK] = v
|
|
599
|
+
mutated.push(k)
|
|
600
|
+
} else if (parseValue) {
|
|
601
|
+
obj[k] = dotenvParseVariables(set({}, 'item', v), { assignToProcessEnv: false }).item
|
|
602
|
+
if (isArray(obj[k])) obj[k] = obj[k].map(item => typeof item === 'string' ? item.trim() : item)
|
|
603
|
+
}
|
|
604
|
+
if (k.slice(-3) === 'Dur') obj[k] = this.parseDur(v)
|
|
605
|
+
if (k.slice(-2) === 'Dt') obj[k] = this.parseDt(v)
|
|
606
|
+
} catch (err) {
|
|
607
|
+
obj[k] = undefined
|
|
608
|
+
if (!silent) throw err
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
})
|
|
612
|
+
if (mutated.length > 0) obj = omit(obj, mutated)
|
|
613
|
+
return obj
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
pascalCase = (text) => {
|
|
617
|
+
return upperFirst(camelCase(text))
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
pick = (obj, items, excludeUnset) => {
|
|
621
|
+
const result = {}
|
|
622
|
+
for (const item of items) {
|
|
623
|
+
const [k, nk] = item.split(':')
|
|
624
|
+
if (excludeUnset && !this.isSet(obj[k])) continue
|
|
625
|
+
result[nk ?? k] = obj[k]
|
|
626
|
+
}
|
|
627
|
+
return result
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
readConfig = async (file, { ns, pattern, globOptions = {}, ignoreError, defValue = {}, opts = {} } = {}) => {
|
|
631
|
+
if (!ns) ns = this.name
|
|
632
|
+
file = resolvePath(this.getPluginFile(file))
|
|
633
|
+
let ext = path.extname(file)
|
|
634
|
+
const fname = path.dirname(file) + '/' + path.basename(file, ext)
|
|
635
|
+
ext = ext.toLowerCase()
|
|
636
|
+
if (['.mjs', '.js'].includes(ext)) {
|
|
637
|
+
const { readHandler } = find(this.app.bajo.configHandlers, { ext })
|
|
638
|
+
return this.parseObject(await readHandler.call(this.app[ns], file, opts))
|
|
639
|
+
}
|
|
640
|
+
if (ext === '.json') return await this.readJson(file)
|
|
641
|
+
if (!['', '.*'].includes(ext)) {
|
|
642
|
+
const item = find(this.app.bajo.configHandlers, { ext })
|
|
643
|
+
if (!item) {
|
|
644
|
+
if (!ignoreError) throw this.error('cantParse%s', file, { code: 'BAJO_CONFIG_NO_PARSER' })
|
|
645
|
+
return this.parseObject(defValue)
|
|
646
|
+
}
|
|
647
|
+
return this.parseObject(await item.readHandler.call(this.app[ns], file, opts))
|
|
648
|
+
}
|
|
649
|
+
const item = pattern ?? `${fname}.{${map(map(this.app.bajo.configHandlers, 'ext'), k => k.slice(1)).join(',')}}`
|
|
650
|
+
const files = await fastGlob(item, globOptions)
|
|
651
|
+
if (files.length === 0) {
|
|
652
|
+
if (!ignoreError) throw this.error('noConfigFileFound', { code: 'BAJO_CONFIG_FILE_NOT_FOUND' })
|
|
653
|
+
return this.parseObject(defValue)
|
|
654
|
+
}
|
|
655
|
+
let config = defValue
|
|
656
|
+
for (const f of files) {
|
|
657
|
+
const ext = path.extname(f).toLowerCase()
|
|
658
|
+
const item = find(this.app.bajo.configHandlers, { ext })
|
|
659
|
+
if (!item) {
|
|
660
|
+
if (!ignoreError) throw this.error('cantParse%s', f, { code: 'BAJO_CONFIG_NO_PARSER' })
|
|
661
|
+
continue
|
|
662
|
+
}
|
|
663
|
+
config = await item.readHandler.call(this.app[ns], f, opts)
|
|
664
|
+
if (!isEmpty(config)) break
|
|
665
|
+
}
|
|
666
|
+
return this.parseObject(config)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
readJson = (file, thrownNotFound = false) => {
|
|
670
|
+
if (isPlainObject(thrownNotFound)) thrownNotFound = false
|
|
671
|
+
if (!fs.existsSync(file) && thrownNotFound) throw this.error('notFound%s%s', this.print.write('file'), file)
|
|
672
|
+
let resp
|
|
673
|
+
try {
|
|
674
|
+
resp = fs.readFileSync(file, 'utf8')
|
|
675
|
+
} catch (err) {}
|
|
676
|
+
if (isEmpty(resp)) return resp
|
|
677
|
+
return this.parseObject(JSON.parse(resp))
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
round = (val, scale = 0) => {
|
|
681
|
+
scale = scale <= 0 ? 1 : 10 ** scale
|
|
682
|
+
return Math.round(val * scale) / scale
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
runHook = async (hookName, ...args) => {
|
|
686
|
+
const [ns, path] = (hookName ?? '').split(':')
|
|
687
|
+
let fns = filter(this.app.bajo.hooks, { ns, path })
|
|
688
|
+
if (isEmpty(fns)) return
|
|
689
|
+
fns = orderBy(fns, ['level'])
|
|
690
|
+
const results = []
|
|
691
|
+
const removed = []
|
|
692
|
+
for (const i in fns) {
|
|
693
|
+
const fn = fns[i]
|
|
694
|
+
const scope = this.app[fn.src]
|
|
695
|
+
const res = await fn.handler.call(scope, ...args)
|
|
696
|
+
results.push({
|
|
697
|
+
hook: hookName,
|
|
698
|
+
resp: res
|
|
699
|
+
})
|
|
700
|
+
if (path.startsWith('once')) removed.push(i)
|
|
701
|
+
if (this.config.log.traceHook) scope.log.trace('hookExecuted%s', hookName)
|
|
702
|
+
}
|
|
703
|
+
if (removed.length > 0) pullAt(this.app.bajo.hooks, removed)
|
|
704
|
+
|
|
705
|
+
return results
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
saveAsDownload = async (file, obj, printSaved = true) => {
|
|
709
|
+
const { print, getPluginDataDir } = this.app.bajo
|
|
710
|
+
const plugin = this.name
|
|
711
|
+
const fname = increment(`${getPluginDataDir(plugin)}/${trim(file, '/')}`, { fs: true })
|
|
712
|
+
const dir = path.dirname(fname)
|
|
713
|
+
if (!fs.existsSync(dir)) fs.ensureDirSync(dir)
|
|
714
|
+
await fs.writeFile(fname, obj, 'utf8')
|
|
715
|
+
if (printSaved) print.succeed('savedAs%s', path.resolve(fname), { skipSilence: true })
|
|
716
|
+
return fname
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// based on: https://stackoverflow.com/questions/1322732/convert-seconds-to-hh-mm-ss-with-javascript
|
|
720
|
+
secToHms = (secs, ms) => {
|
|
721
|
+
let remain
|
|
722
|
+
if (ms) {
|
|
723
|
+
remain = secs % 1000
|
|
724
|
+
secs = Math.floor(secs / 1000)
|
|
725
|
+
}
|
|
726
|
+
const secNum = parseInt(secs, 10)
|
|
727
|
+
const hours = Math.floor(secNum / 3600)
|
|
728
|
+
const minutes = Math.floor(secNum / 60) % 60
|
|
729
|
+
const seconds = secNum % 60
|
|
730
|
+
|
|
731
|
+
let hms = [hours, minutes, seconds]
|
|
732
|
+
.map(v => v < 10 ? '0' + v : v)
|
|
733
|
+
.filter((v, i) => v !== '00' || i > 0)
|
|
734
|
+
.join(':')
|
|
735
|
+
if (ms) hms += '+' + padStart(remain, 3, '0')
|
|
736
|
+
return hms
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
titleize = (text, { ignores = [], replacement = {} } = {}) => {
|
|
740
|
+
const defIgnores = ['or', 'and', 'of', 'with']
|
|
741
|
+
const replacer = {}
|
|
742
|
+
forOwn(replacement, (v, k) => {
|
|
743
|
+
const id = this.generateId('int')
|
|
744
|
+
replacer[id] = k
|
|
745
|
+
text = text.replace(k, ` ${id} `)
|
|
746
|
+
})
|
|
747
|
+
return map(words(text), t => {
|
|
748
|
+
forOwn(replacer, (v, k) => {
|
|
749
|
+
if (k === t) t = replacement[replacer[k]]
|
|
750
|
+
})
|
|
751
|
+
ignores = uniq(concat(ignores, defIgnores))
|
|
752
|
+
if (ignores.includes(t)) return t
|
|
753
|
+
return upperFirst(t)
|
|
754
|
+
}).join(' ')
|
|
28
755
|
}
|
|
29
756
|
}
|
|
30
757
|
|