waibu 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.github/FUNDING.yml +13 -0
  2. package/.github/workflows/repo-lockdown.yml +24 -0
  3. package/.jsdoc.conf.json +45 -0
  4. package/LICENSE +1 -1
  5. package/README.md +40 -7
  6. package/config-prod.json +1 -1
  7. package/docs/Waibu.html +3 -0
  8. package/docs/data/search.json +1 -0
  9. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  10. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  11. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  12. package/docs/global.html +3 -0
  13. package/docs/index.html +3 -0
  14. package/docs/index.js.html +568 -0
  15. package/docs/scripts/core.js +726 -0
  16. package/docs/scripts/core.min.js +23 -0
  17. package/docs/scripts/resize.js +90 -0
  18. package/docs/scripts/search.js +265 -0
  19. package/docs/scripts/search.min.js +6 -0
  20. package/docs/scripts/third-party/Apache-License-2.0.txt +202 -0
  21. package/docs/scripts/third-party/fuse.js +9 -0
  22. package/docs/scripts/third-party/hljs-line-num-original.js +369 -0
  23. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  24. package/docs/scripts/third-party/hljs-original.js +5171 -0
  25. package/docs/scripts/third-party/hljs.js +1 -0
  26. package/docs/scripts/third-party/popper.js +5 -0
  27. package/docs/scripts/third-party/tippy.js +1 -0
  28. package/docs/scripts/third-party/tocbot.js +672 -0
  29. package/docs/scripts/third-party/tocbot.min.js +1 -0
  30. package/docs/static/bitcoin.jpeg +0 -0
  31. package/docs/static/home.md +31 -0
  32. package/docs/static/logo-ecosystem.png +0 -0
  33. package/docs/static/logo.png +0 -0
  34. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  35. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  36. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  37. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  38. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  39. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  40. package/extend/bajo/hook/waibu@on-close.js +5 -0
  41. package/extend/bajo/hook/waibu@on-ready.js +5 -0
  42. package/extend/bajo/hook/{on-request.js → waibu@on-request.js} +6 -6
  43. package/extend/bajo/hook/{on-response.js → waibu@on-response.js} +2 -2
  44. package/extend/bajo/hook/{on-route.js → waibu@on-route.js} +0 -0
  45. package/extend/bajo/intl/en-US.json +0 -0
  46. package/extend/bajo/intl/id.json +0 -0
  47. package/extend/bajoTemplate/layout/email.html +0 -0
  48. package/extend/bajoTemplate/layout/email.id.html +0 -0
  49. package/index.js +259 -55
  50. package/lib/app-hook.js +2 -2
  51. package/lib/app.js +8 -8
  52. package/lib/build-locals.js +22 -10
  53. package/lib/collect-route-path-handlers.js +2 -2
  54. package/lib/handle-download.js +2 -2
  55. package/lib/handle-forward.js +1 -1
  56. package/lib/handle-redirect.js +0 -0
  57. package/lib/handle-xml-body.js +0 -0
  58. package/lib/home.js +2 -2
  59. package/lib/{log-routes.js → print-routes.js} +3 -3
  60. package/lib/webapp-scope/attach-intl.js +4 -4
  61. package/lib/webapp-scope/error-handler.js +0 -0
  62. package/lib/webapp-scope/handle-compress.js +1 -1
  63. package/lib/webapp-scope/handle-cors.js +1 -1
  64. package/lib/webapp-scope/handle-helmet.js +1 -1
  65. package/lib/webapp-scope/handle-multipart-body.js +3 -3
  66. package/lib/webapp-scope/handle-rate-limit.js +2 -2
  67. package/lib/webapp-scope/is-route-disabled.js +2 -2
  68. package/lib/webapp-scope/merge-route-hooks.js +2 -2
  69. package/lib/webapp-scope/rerouted-path.js +0 -0
  70. package/lib/webapp-scope/route-hook.js +1 -1
  71. package/logo.png +0 -0
  72. package/package.json +24 -15
  73. package/wiki/CHANGES.md +16 -0
  74. package/wiki/CONFIG.md +45 -0
  75. package/wiki/CONTRIBUTING.md +5 -0
  76. package/wiki/DEV-GUIDE.md +1 -0
  77. package/wiki/ECOSYSTEM.md +19 -0
  78. package/wiki/GETTING-STARTED.md +76 -0
  79. package/wiki/USER-GUIDE.md +1 -0
  80. package/extend/bajo/hook/on-close.js +0 -5
  81. package/extend/bajo/hook/on-ready.js +0 -5
package/index.js CHANGED
@@ -2,7 +2,7 @@ import collectRoutePathHandlers from './lib/collect-route-path-handlers.js'
2
2
  import fastify from 'fastify'
3
3
  import appHook from './lib/app-hook.js'
4
4
  import routeHook from './lib/webapp-scope/route-hook.js'
5
- import logRoutes from './lib/log-routes.js'
5
+ import printRoutes from './lib/print-routes.js'
6
6
  import { boot } from './lib/app.js'
7
7
  import sensible from '@fastify/sensible'
8
8
  import noIcon from 'fastify-no-icon'
@@ -12,18 +12,72 @@ import handleRedirect from './lib/handle-redirect.js'
12
12
  import buildLocals from './lib/build-locals.js'
13
13
  import queryString from 'query-string'
14
14
 
15
+ /**
16
+ * @typedef TEscapeChars
17
+ * @type {Object}
18
+ * @memberof Waibu
19
+ * @property {string} <=<
20
+ * @property {string} >=>
21
+ * @property {string} "="
22
+ * @property {string} '='
23
+ */
24
+
25
+ /**
26
+ * Plugin factory
27
+ *
28
+ * @param {string} pkgName - NPM package name
29
+ * @returns {class}
30
+ */
15
31
  async function factory (pkgName) {
16
32
  const me = this
17
33
 
18
- return class Waibu extends this.lib.Plugin {
34
+ /**
35
+ * Waibu Web Framework plugin for Bajo. This is the main foundation of all web apps attached to
36
+ * the system through a route prefix. Those web apps are then build as childrens with
37
+ * its own fastify's context.
38
+ *
39
+ * There are currently 3 web apps available:
40
+ * - {@link https://github.com/ardhi/waibu-static|waibu-static} for static content delivery
41
+ * - {@link https://github.com/ardhi/waibu-rest-api|waibu-rest-api} for rest api setup
42
+ * - and {@link https://github.com/ardhi/waibu-mpa|waibu-mpa} for normal multi-page application
43
+ *
44
+ * You should write your code as the extension of above web apps. Not to this main app.
45
+ * Unless, of course, if you want to write custom web apps with its own context.
46
+ *
47
+ * @class
48
+ */
49
+ class Waibu extends this.app.baseClass.Base {
50
+ /**
51
+ * @constant {string[]}
52
+ * @default ['onRequest', 'onResponse', 'preParsing', 'preValidation', 'preHandler', 'preSerialization', 'onSend', 'onTimeout', 'onError']
53
+ * @memberof Waibu
54
+ */
55
+ static hookTypes = ['onRequest', 'onResponse', 'preParsing', 'preValidation', 'preHandler',
56
+ 'preSerialization', 'onSend', 'onTimeout', 'onError']
57
+
58
+ /**
59
+ * @constant {TEscapeChars}
60
+ * @memberof Waibu
61
+ */
62
+ static escapeChars = {
63
+ '<': '&lt;',
64
+ '>': '&gt;',
65
+ '"': '&quot;',
66
+ "'": '&apos;'
67
+ }
68
+
19
69
  constructor () {
20
70
  super(pkgName, me.app)
21
- this.alias = 'w'
22
- this.dependencies = ['bajo-extra']
71
+
72
+ /**
73
+ * @see {@tutorial config}
74
+ * @type {Object}
75
+ */
23
76
  this.config = {
77
+ home: undefined,
24
78
  server: {
25
79
  host: '127.0.0.1',
26
- port: 7771
80
+ port: 17845
27
81
  },
28
82
  factory: {
29
83
  trustProxy: true,
@@ -46,7 +100,7 @@ async function factory (pkgName) {
46
100
  lang: 'lang'
47
101
  },
48
102
  paramsCharMap: {},
49
- logRoutes: true,
103
+ printRoutes: true,
50
104
  pageTitleFormat: '%s : %s',
51
105
  siteInfo: {
52
106
  title: 'My Website',
@@ -77,12 +131,6 @@ async function factory (pkgName) {
77
131
  }
78
132
  }
79
133
  }
80
- this.escapeChars = {
81
- '<': '&lt;',
82
- '>': '&gt;',
83
- '"': '&quot;',
84
- "'": '&apos;'
85
- }
86
134
  this.qs = {
87
135
  parse: (item) => {
88
136
  return queryString.parse(item, {
@@ -94,17 +142,28 @@ async function factory (pkgName) {
94
142
  stringify: queryString.stringify,
95
143
  stringifyUrl: queryString.stringifyUrl
96
144
  }
97
- this.hookTypes = ['onRequest', 'onResponse', 'preParsing', 'preValidation', 'preHandler',
98
- 'preSerialization', 'onSend', 'onTimeout', 'onError']
99
145
  }
100
146
 
147
+ /**
148
+ * Initialize plugin
149
+ *
150
+ * @method
151
+ * @async
152
+ */
101
153
  init = async () => {
102
154
  if (this.config.home === '/') this.config.home = false
103
155
  await collectRoutePathHandlers.call(this)
104
156
  }
105
157
 
158
+ /**
159
+ * Start plugin
160
+ *
161
+ * @method
162
+ * @async
163
+ */
106
164
  start = async () => {
107
- const { generateId, runHook } = this.app.bajo
165
+ const { runHook } = this.app.bajo
166
+ const { generateId } = this.app.lib.aneka
108
167
  const cfg = this.getConfig()
109
168
  if (this.app.bajoLogger) {
110
169
  cfg.factory.loggerInstance = this.app.bajoLogger.instance.child(
@@ -132,21 +191,33 @@ async function factory (pkgName) {
132
191
  await handleRedirect.call(this, instance)
133
192
  await handleForward.call(this, instance)
134
193
  await appHook.call(this)
135
- await routeHook.call(this, this.name)
194
+ await routeHook.call(this, this.ns)
136
195
  await boot.call(this)
137
196
  await instance.listen(cfg.server)
138
- if (cfg.logRoutes) logRoutes.call(this)
197
+ if (cfg.printRoutes) printRoutes.call(this)
139
198
  }
140
199
 
200
+ /**
201
+ * Exit handler
202
+ *
203
+ * @method
204
+ * @async
205
+ */
141
206
  exit = async () => {
142
207
  this.instance.close()
143
208
  }
144
209
 
145
- findRoute = (route) => {
146
- const { outmatch } = this.lib
147
- const { find } = this.lib._
210
+ /**
211
+ * Find route by route name
212
+ *
213
+ * @param {string} name - ns based route name
214
+ * @returns {Object} Route object
215
+ */
216
+ findRoute = (name) => {
217
+ const { outmatch } = this.app.lib
218
+ const { find } = this.app.lib._
148
219
  const { breakNsPath } = this.app.bajo
149
- let { ns, subNs = '', path } = breakNsPath(route)
220
+ let { ns, subNs = '', path } = breakNsPath(name)
150
221
  const params = path.split('|')
151
222
  if (params.length > 1) path = params[0]
152
223
  return find(this.routes, r => {
@@ -158,15 +229,37 @@ async function factory (pkgName) {
158
229
  })
159
230
  }
160
231
 
232
+ get escapeChars () {
233
+ return this.constructor.escapeChars
234
+ }
235
+
236
+ /**
237
+ * Escape text
238
+ *
239
+ * @method
240
+ * @param {string} text
241
+ * @returns {string}
242
+ */
161
243
  escape = (text = '') => {
162
244
  if (typeof text !== 'string') return text
163
- const { forOwn } = this.lib._
245
+ const { forOwn } = this.app.lib._
164
246
  forOwn(this.escapeChars, (v, k) => {
165
247
  text = text.replaceAll(k, v)
166
248
  })
167
249
  return text
168
250
  }
169
251
 
252
+ /**
253
+ * Fetch something from url. A wrapper of bajo-extra's fetchUrl which support
254
+ * bajo's ns based url.
255
+ *
256
+ * @method
257
+ * @async
258
+ * @param {string} url - Also support ns based url
259
+ * @param {Object} [opts={}] - node's fetch options
260
+ * @param {Object} [extra={}] - See {@link https://ardhi.github.io/bajo-extra|bajo-extra}
261
+ * @returns {Object}
262
+ */
170
263
  fetch = async (url, opts = {}, extra = {}) => {
171
264
  const { fetch } = this.app.bajoExtra
172
265
  extra.rawResponse = true
@@ -183,22 +276,43 @@ async function factory (pkgName) {
183
276
  return result
184
277
  }
185
278
 
279
+ /**
280
+ * Get visitor IP from fastify's request object
281
+ *
282
+ * @method
283
+ * @param {Object} req - request object
284
+ * @returns {string}
285
+ */
186
286
  getIp = (req) => {
187
- const { isEmpty } = this.lib._
287
+ const { isEmpty } = this.app.lib._
188
288
  let fwd = req.headers['x-forwarded-for'] ?? ''
189
289
  if (!Array.isArray(fwd)) fwd = fwd.split(',').map(ip => ip.trim())
190
290
  return isEmpty(fwd[0]) ? req.ip : fwd[0]
191
291
  }
192
292
 
293
+ /**
294
+ * Get origin of fastify's request object
295
+ *
296
+ * @method
297
+ * @param {Object} req
298
+ * @returns {string}
299
+ */
193
300
  getOrigin = (req) => {
194
- const { isEmpty } = this.lib._
301
+ const { isEmpty } = this.app.lib._
195
302
  let host = req.host
196
303
  if (isEmpty(host) || host === ':authority') host = `${this.config.server.host}:${this.config.server.port}`
197
304
  return `${req.protocol}://${host}`
198
305
  }
199
306
 
307
+ /**
308
+ * Get plugin by prefix
309
+ *
310
+ * @method
311
+ * @param {string} prefix
312
+ * @returns {Object}
313
+ */
200
314
  getPluginByPrefix = (prefix) => {
201
- const { get, find } = this.lib._
315
+ const { get, find } = this.app.lib._
202
316
  const item = find(this.app.waibu.routes, r => {
203
317
  return get(r, 'config.prefix') === prefix
204
318
  })
@@ -206,19 +320,34 @@ async function factory (pkgName) {
206
320
  if (ns) return this.app[ns]
207
321
  }
208
322
 
209
- getPluginPrefix = (base, webApp = 'waibuMpa') => {
210
- const { get, trim } = this.lib._
211
- let prefix = get(this, `app.${base}.config.waibu.prefix`, this.app[base].alias)
212
- if (base === 'main') {
323
+ /**
324
+ * Get plugin's prefix by name
325
+ *
326
+ * @method
327
+ * @param {string} name - Plugin's name
328
+ * @param {string} [webApp=waibuMpa] - Web app to use
329
+ * @returns {string}
330
+ */
331
+ getPluginPrefix = (name, webApp = 'waibuMpa') => {
332
+ const { get, trim } = this.app.lib._
333
+ let prefix = get(this, `app.${name}.config.${webApp}.prefix`, get(this, `app.${name}.config.waibu.prefix`, this.app[name].alias))
334
+ if (name === 'main') {
213
335
  const cfg = this.app[webApp].config
214
336
  if (cfg.mountMainAsRoot) prefix = ''
215
337
  }
216
-
217
338
  return trim(prefix, '/')
218
339
  }
219
340
 
220
- getRoutes = (grouped, lite) => {
221
- const { groupBy, orderBy, mapValues, map, pick } = this.lib._
341
+ /**
342
+ * Get all available routes
343
+ *
344
+ * @method
345
+ * @param {boolean} [grouped=false] - Returns as groups of urls and methods
346
+ * @param {*} [lite=false] - Retuns only urls and methods
347
+ * @returns {Array}
348
+ */
349
+ getRoutes = (grouped = false, lite = false) => {
350
+ const { groupBy, orderBy, mapValues, map, pick } = this.app.lib._
222
351
  const all = this.routes
223
352
  let routes
224
353
  if (grouped) {
@@ -229,18 +358,35 @@ async function factory (pkgName) {
229
358
  return routes
230
359
  }
231
360
 
232
- getUploadedFiles = async (reqId, fileUrl, returnDir) => {
233
- const { getPluginDataDir, resolvePath } = this.app.bajo
234
- const { fastGlob } = this.lib
235
- const dir = `${getPluginDataDir(this.name)}/upload/${reqId}`
361
+ /**
362
+ * Get uploaded files by request ID
363
+ *
364
+ * @method
365
+ * @param {string} reqId - Request ID
366
+ * @param {boolean} [fileUrl=false] - If ```true```, files returned as file url format (```file:///...```)
367
+ * @param {*} returnDir - If ```true```, also return its directory
368
+ * @returns {(Object|Array)} - Returns object if ```returnDir``` is ```true```, array of files otherwise
369
+ */
370
+ getUploadedFiles = async (reqId, fileUrl = false, returnDir = false) => {
371
+ const { getPluginDataDir } = this.app.bajo
372
+ const { resolvePath } = this.app.lib.aneka
373
+ const { fastGlob } = this.app.lib
374
+ const dir = `${getPluginDataDir(this.ns)}/upload/${reqId}`
236
375
  const result = await fastGlob(`${dir}/*`)
237
376
  if (!fileUrl) return returnDir ? { dir, files: result } : result
238
377
  const files = result.map(f => resolvePath(f, true))
239
378
  return returnDir ? { dir, files } : files
240
379
  }
241
380
 
381
+ /**
382
+ * Is namespace's path contains language detector token?
383
+ *
384
+ * @method
385
+ * @param {string} ns - Plugin name
386
+ * @returns {boolean}
387
+ */
242
388
  isIntlPath = (ns) => {
243
- const { get } = this.lib._
389
+ const { get } = this.app.lib._
244
390
  return get(this.app[ns], 'config.intl.detectors', []).includes('path')
245
391
  }
246
392
 
@@ -248,6 +394,13 @@ async function factory (pkgName) {
248
394
  throw this.error('_notFound', { path: name })
249
395
  }
250
396
 
397
+ /**
398
+ * Parse filter found from Fastify's request based on keys set in config object
399
+ *
400
+ * @method
401
+ * @param {Object} req - Request object
402
+ * @returns {Object}
403
+ */
251
404
  parseFilter = (req) => {
252
405
  const result = {}
253
406
  const items = Object.keys(this.config.qsKey)
@@ -257,26 +410,45 @@ async function factory (pkgName) {
257
410
  return result
258
411
  }
259
412
 
260
- routeDir = (ns, base) => {
261
- const { get } = this.lib._
262
- if (!base) base = ns
263
- const cfg = this.app[base].config
264
- const prefix = get(cfg, 'waibu.prefix', this.app[base].alias)
413
+ /**
414
+ * Get route directory by plugin's name
415
+ *
416
+ * @param {*} ns - Namespace
417
+ * @param {*} [baseNs] - Base namespace. If not provided, defaults to scope's ns
418
+ * @returns {string}
419
+ */
420
+ routeDir = (ns, baseNs) => {
421
+ const { get } = this.app.lib._
422
+ if (!baseNs) baseNs = ns
423
+ const cfg = this.app[baseNs].config
424
+ const prefix = get(cfg, 'waibu.prefix', this.app[baseNs].alias)
265
425
  const dir = prefix === '' ? '' : `/${prefix}`
266
- if (!ns) return dir
267
426
  const cfgMpa = get(this, 'app.waibuMpa.config')
268
- if (ns === this.app.bajo.mainNs && cfgMpa.mountMainAsRoot) return ''
269
- if (ns === base) return dir
427
+ if (ns === this.app.mainNs && cfgMpa.mountMainAsRoot) return ''
428
+ if (ns === baseNs) return dir
270
429
  return dir + `/${get(this.app[ns].config, 'waibu.prefix', this.app[ns].alias)}`
271
430
  }
272
431
 
273
- routePath = (name = '', options = {}) => {
432
+ /**
433
+ * Get route path by route's name:
434
+ * - If it is a ```mailto:``` or ```tel:``` url, it returns as is
435
+ * - If it is a ns based name, it will be parsed first
436
+ *
437
+ * @method
438
+ * @param {string} name
439
+ * @param {Object} [options={}] - Options object
440
+ * @param {string} [options.base=waibu] - Base namespace
441
+ * @param {boolean} [options.guessHost] - If true, guest host if host is not set
442
+ * @param {Object} [options.query={}] - Query string's object. If provided, it will be added to returned value
443
+ * @param {Object} [options.params={}] - Parameter object. If provided, it will be merged to returned value
444
+ * @returns {string}
445
+ */
446
+ routePath = (name, options = {}) => {
274
447
  const { getPlugin } = this.app.bajo
275
- const { defaultsDeep } = this.lib.aneka
276
- const { isEmpty, get, trimEnd, trimStart } = this.lib._
448
+ const { defaultsDeep } = this.app.lib.aneka
449
+ const { isEmpty, get, trimEnd, trimStart } = this.app.lib._
277
450
  const { breakNsPath } = this.app.bajo
278
- const { query = {}, base = 'waibu', params = {}, guessHost } = options
279
- if (isEmpty(name)) return name
451
+ const { query = {}, base = this.ns, params = {}, guessHost } = options
280
452
 
281
453
  const plugin = getPlugin(base)
282
454
  const cfg = plugin.config ?? {}
@@ -304,11 +476,23 @@ async function factory (pkgName) {
304
476
  return url
305
477
  }
306
478
 
479
+ /**
480
+ * Method to send mail through Masohi Messaging System. It is a thin wrapper
481
+ * for {@link https://github.com/ardhi/masohi-mail|masohi-mail} send method.
482
+ *
483
+ * If masohi is not loaded, nothing is delivered.
484
+ *
485
+ * @method
486
+ * @async
487
+ * @param {(string|Array)} tpl - Mail's template to use. If a string is given, the same template will be used for html & plaintext versions. Otherwise, the first template will be used for html mail, and the second one is for it's plaintext version
488
+ * @param {Object} [params={}] - {@link https://github.com/ardhi/masohi-mail|masohi-mail}'s params object.
489
+ * @returns
490
+ */
307
491
  sendMail = async (tpl, { to, cc, bcc, from, subject, data = {}, conn, source, options = {} }) => {
308
492
  conn = conn ?? 'masohiMail:default'
309
493
  if (!this.app.masohi || !this.app.masohiMail) return
310
- const { get, isString } = this.lib._
311
- const { generateId } = this.app.bajo
494
+ const { get, isString } = this.app.lib._
495
+ const { generateId } = this.app.lib.aneka
312
496
  const { render } = this.app.bajoTemplate
313
497
  if (isString(tpl)) tpl = [tpl]
314
498
  const locals = await buildLocals.call(this, { tpl, params: data, opts: options })
@@ -319,11 +503,22 @@ async function factory (pkgName) {
319
503
  const message = await render(tpl[0], locals, opts)
320
504
  if (tpl[1]) opts.messageText = await render(tpl[1], locals, opts)
321
505
  const payload = { type: 'object', data: { to, cc, bcc, from, subject, message, options: opts } }
322
- await this.app.masohi.send({ payload, source: source ?? this.name, conn }) // mail sent through worker
506
+ await this.app.masohi.send({ payload, source: source ?? this.ns, conn }) // mail sent through worker
323
507
  }
324
508
 
509
+ /**
510
+ * Recursively unescape block of texts
511
+ *
512
+ * @method
513
+ * @param {string} content - Source content
514
+ * @param {string} start - Block's start
515
+ * @param {string} end - Block's end
516
+ * @param {string} startReplacer - Token to use as block's start replacer
517
+ * @param {string} endReplacer - Token to use as block's end replacer
518
+ * @returns {string}
519
+ */
325
520
  unescapeBlock = (content, start, end, startReplacer, endReplacer) => {
326
- const { extractText } = this.lib.aneka
521
+ const { extractText } = this.app.lib.aneka
327
522
  const { result } = extractText(content, start, end)
328
523
  if (result.length === 0) return content
329
524
  const unescaped = this.unescape(result)
@@ -333,8 +528,15 @@ async function factory (pkgName) {
333
528
  return this.unescapeBlock(block, start, end, startReplacer, endReplacer)
334
529
  }
335
530
 
531
+ /**
532
+ * Unescape text using {@link TEscapeChars} rules
533
+ *
534
+ * @method
535
+ * @param {string} text - Text to unescape
536
+ * @returns {string}
537
+ */
336
538
  unescape = (text) => {
337
- const { forOwn, invert } = this.lib._
539
+ const { forOwn, invert } = this.app.lib._
338
540
  const mapping = invert(this.escapeChars)
339
541
  forOwn(mapping, (v, k) => {
340
542
  text = text.replaceAll(k, v)
@@ -342,6 +544,8 @@ async function factory (pkgName) {
342
544
  return text
343
545
  }
344
546
  }
547
+
548
+ return Waibu
345
549
  }
346
550
 
347
551
  export default factory
package/lib/app-hook.js CHANGED
@@ -5,8 +5,8 @@ async function appHook () {
5
5
  for (const hook of hooks) {
6
6
  me.instance.addHook(hook, async function (...args) {
7
7
  args.push(this)
8
- if (['onClose', 'onReady'].includes(hook)) await runHook(`${me.name}:${hook}`, ...args)
9
- else await runHook(`${me.name}:${hook}`, ...args)
8
+ if (['onClose', 'onReady'].includes(hook)) await runHook(`${me.ns}:${hook}`, ...args)
9
+ else await runHook(`${me.ns}:${hook}`, ...args)
10
10
  })
11
11
  }
12
12
  }
package/lib/app.js CHANGED
@@ -2,12 +2,12 @@ import home from './home.js'
2
2
 
3
3
  export async function collect (glob = 'boot.js', baseNs) {
4
4
  const { eachPlugins, importModule } = this.app.bajo
5
- const { orderBy, get } = this.lib._
6
- if (!baseNs) baseNs = this.name
5
+ const { orderBy, get } = this.app.lib._
6
+ if (!baseNs) baseNs = this.ns
7
7
  const mods = []
8
8
 
9
9
  await eachPlugins(async function ({ file }) {
10
- const { name: ns, alias, config } = this
10
+ const { ns, alias, config } = this
11
11
  const mod = await importModule(file, { asHandler: true })
12
12
  mod.prefix = get(config, 'waibu.prefix', alias)
13
13
  if (get(config, 'intl.detectors', []).includes('path')) mod.prefix = `/:lang${mod.prefix}`
@@ -27,23 +27,23 @@ export async function collect (glob = 'boot.js', baseNs) {
27
27
  export async function boot () {
28
28
  const { runHook } = this.app.bajo
29
29
  const mods = await collect.call(this)
30
- await runHook(`${this.name}:beforeAppBoot`)
30
+ await runHook(`${this.ns}:beforeAppBoot`)
31
31
  for (const m of mods) {
32
32
  const disabled = this.app[m.ns].config.disabled
33
33
  if (Array.isArray(disabled) && disabled.length === 1 && ['*', 'all'].includes(disabled[0])) {
34
34
  this.log.warn('allRoutesConfigDisabled%s', m.ns)
35
35
  continue
36
36
  }
37
- await runHook(`${this.name}.${m.ns}:beforeAppBoot`)
37
+ await runHook(`${this.ns}.${m.ns}:beforeAppBoot`)
38
38
  this.log.debug('bootApp%s', m.ns)
39
39
  await this.instance.register(async (ctx) => {
40
40
  const plugin = this.app[m.ns]
41
41
  plugin.instance = ctx
42
- await runHook(`${plugin.name}:afterCreateContext`, ctx)
42
+ await runHook(`${plugin.ns}:afterCreateContext`, ctx)
43
43
  await m.handler.call(plugin, ctx, m.prefix)
44
44
  }, { prefix: m.prefix })
45
- await runHook(`${this.name}.${m.ns}:afterAppBoot`)
45
+ await runHook(`${this.ns}.${m.ns}:afterAppBoot`)
46
46
  }
47
- await runHook(`${this.name}:afterAppBoot`)
47
+ await runHook(`${this.ns}:afterAppBoot`)
48
48
  await home.call(this)
49
49
  }
@@ -1,8 +1,10 @@
1
+ const errs = {}
2
+
1
3
  async function buildHomesMenu (req) {
2
4
  const { callHandler } = this.app.bajo
3
- const { get, find, orderBy } = this.lib._
5
+ const { get, find, orderBy } = this.app.lib._
4
6
  const routes = []
5
- for (const ns of this.app.bajo.pluginNames) {
7
+ for (const ns of this.app.getAllNs()) {
6
8
  let home = get(this, `app.${ns}.config.waibuMpa.home`)
7
9
  const homeHandler = get(this, `app.${ns}.config.waibuMpa.homeHandler`)
8
10
  if (homeHandler) home = await callHandler(this.app[ns], homeHandler)
@@ -14,15 +16,15 @@ async function buildHomesMenu (req) {
14
16
  }
15
17
 
16
18
  async function buildPagesMenu (req) {
17
- const { find, orderBy, merge, isString } = this.lib._
19
+ const { find, orderBy, merge, isString } = this.app.lib._
18
20
  const { callHandler, runHook } = this.app.bajo
19
21
  const all = [{ icon: 'house', href: '/', level: 1 }]
20
- for (const ns of this.app.bajo.pluginNames) {
22
+ for (const ns of this.app.getAllNs()) {
21
23
  const items = []
22
24
  let pages = this.app[ns].getConfig('waibuMpa.menuHandler', { defValue: [] })
23
25
  if (isString(pages)) pages = await callHandler(this.app[ns], pages, req)
24
26
  if (pages.length === 0) continue
25
- await runHook(`${this.name}.${ns}:afterBuildPagesMenu`, pages, req)
27
+ await runHook(`${this.ns}.${ns}:afterBuildPagesMenu`, pages, req)
26
28
  for (const page of pages) {
27
29
  const existing = find(all, { title: page.title })
28
30
  page.level = page.level ?? 1000
@@ -49,13 +51,15 @@ async function buildPagesMenu (req) {
49
51
  }
50
52
  return orderBy(all, ['level', 'title'])
51
53
  }
54
+
52
55
  async function buildLocals ({ tpl, params = {}, opts = {} } = {}) {
53
56
  const { runHook } = this.app.bajo
54
- const { set, merge, pick, get, isEmpty, find } = this.lib._
57
+ const { set, merge, pick, get, isEmpty, find, pullAt } = this.app.lib._
55
58
  const { req, reply } = opts
56
59
 
57
60
  const appTitle = this.app.waibuMpa ? req.t(this.app.waibuMpa.getAppTitle(req.ns)) : ''
58
61
  params.page = merge(params.page ?? {}, { ns: req.ns, appTitle })
62
+ params.sidebar = params.sidebar ?? []
59
63
 
60
64
  const { site, user, lang, darkMode } = req
61
65
  const theme = pick(find(this.themes, { name: req.theme }) ?? {}, ['name', 'framework'])
@@ -72,6 +76,11 @@ async function buildLocals ({ tpl, params = {}, opts = {} } = {}) {
72
76
  _meta.hostHeader = req.headers.host
73
77
  _meta.statusCode = 200
74
78
  _meta.isAdmin = _meta.user && find(_meta.user.teams, { alias: 'administrator' })
79
+ const pulled = []
80
+ for (const k in errs) {
81
+ if (Date.now() - errs[k] > 5000) pulled.push(k)
82
+ }
83
+ pullAt(errs, pulled)
75
84
  if (params.error) {
76
85
  if (params.error.statusCode) _meta.statusCode = params.error.statusCode
77
86
  _meta.errorMessage = params.error.message
@@ -79,15 +88,18 @@ async function buildLocals ({ tpl, params = {}, opts = {} } = {}) {
79
88
  params.page.ns = params.error.ns
80
89
  params.page.appTitle = this.app.waibuMpa.getAppTitle(params.error.ns)
81
90
  }
82
- this.log.error('error%s', params.error.message)
83
- if (this.app.bajo.config.env === 'dev') console.log(params.error)
91
+ if (!errs[req.id]) {
92
+ this.log.error('error%s', params.error.message)
93
+ if (this.app.bajo.config.env === 'dev') console.log(params.error)
94
+ }
95
+ errs[req.id] = Date.now()
84
96
  }
85
97
  if (reply && req.session && req.flash && !opts.partial) _meta.flash = reply.flash()
86
98
  set(params, 'menu.homes', await buildHomesMenu.call(this, req))
87
99
  set(params, 'menu.pages', await buildPagesMenu.call(this, req))
88
100
  const merged = merge({}, params, { _meta })
89
- await runHook(`${this.name}:afterBuildLocals`, merged, req)
90
- if (!isEmpty(routeOpts.ns)) await runHook(`${this.name}.${routeOpts.ns}:afterBuildLocals`, merged, req)
101
+ await runHook(`${this.ns}:afterBuildLocals`, merged, req)
102
+ if (!isEmpty(routeOpts.ns)) await runHook(`${this.ns}.${routeOpts.ns}:afterBuildLocals`, merged, req)
91
103
  return merged
92
104
  }
93
105
 
@@ -1,11 +1,11 @@
1
1
  async function collectRoutePathHandlers () {
2
2
  const { eachPlugins } = this.app.bajo
3
- const { isEmpty } = this.lib._
3
+ const { isEmpty } = this.app.lib._
4
4
  this.routePathHandlers = this.routePathHandlers ?? {}
5
5
  const me = this
6
6
 
7
7
  await eachPlugins(async function () {
8
- const { name: ns } = this
8
+ const { ns } = this
9
9
  if (isEmpty(this.routePathHandlers) || !this.routePath) return undefined
10
10
  for (const key of this.routePathHandlers) {
11
11
  me.routePathHandlers[key] = { handler: this.routePath, ns }
@@ -2,9 +2,9 @@ import path from 'path'
2
2
 
3
3
  async function handleDownload (input, req, reply) {
4
4
  const { importPkg } = this.app.bajo
5
- const { isFunction } = this.lib._
5
+ const { isFunction } = this.app.lib._
6
6
  const mime = await importPkg('waibu:mime')
7
- const { fs } = this.lib
7
+ const { fs } = this.app.lib
8
8
  const file = isFunction(input) ? (await input.call(this, req)) : input
9
9
  if (!fs.existsSync(file)) throw this.error('_notFound')
10
10
  const mimeType = mime.getType(path.extname(file))
@@ -1,7 +1,7 @@
1
1
  import replyFrom from '@fastify/reply-from'
2
2
 
3
3
  async function handleForward (ctx) {
4
- const { defaultsDeep } = this.lib.aneka
4
+ const { defaultsDeep } = this.app.lib.aneka
5
5
  const me = this
6
6
 
7
7
  function rewriteHeaders (headers, req) {
File without changes
File without changes