waibu-mpa 2.15.2 → 2.17.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.
@@ -0,0 +1,41 @@
1
+ async function checkLang (req, reply) {
2
+ if (!req.session) return
3
+ if (req.langDetector) {
4
+ req.session.lang = req.lang
5
+ return
6
+ }
7
+ if (req.session.lang) req.lang = req.session.lang
8
+ }
9
+
10
+ async function checkDark (req, reply) {
11
+ const { isSet } = this.app.lib.aneka
12
+ const key = this.config.darkMode.qsKey
13
+ const value = this.config.darkMode.set ?? req.query[key]
14
+ if (isSet(value)) {
15
+ req.darkMode = value
16
+ if (req.session) req.session.darkMode = req.darkMode
17
+ return
18
+ }
19
+ if (isSet(req.session.darkMode)) req.darkMode = req.session.darkMode
20
+ }
21
+
22
+ async function hook () {
23
+ return [{
24
+ level: 9,
25
+ name: 'waibuMpa:preParsing',
26
+ handler: async function (req, reply) {
27
+ const { importModule } = this.app.bajo
28
+ const attachIntl = await importModule('waibu:/lib/webapp-scope/attach-intl.js')
29
+ await attachIntl.call(this, this.config.intl.detectors, req, reply)
30
+ await checkLang.call(this, req, reply)
31
+ await checkDark.call(this, req, reply)
32
+ }
33
+ }, {
34
+ name: 'waibuMpa.theme:afterInjectScripts',
35
+ handler: async function ({ items }) {
36
+ // items.push(`${this.ns}.virtual:/json2csv/json2csv.js`)
37
+ }
38
+ }]
39
+ }
40
+
41
+ export default hook
@@ -8,8 +8,6 @@ class Wmpa {
8
8
  this.prefixMain = '<%= prefix.main %>'
9
9
  this.accessTokenUrl = '<%= accessTokenUrl %>'
10
10
  this.renderUrl = '<%= renderUrl %>'
11
- this.theme = '<%= _meta.theme.name %>'
12
- this.iconset = '<%= _meta.iconset.name %>'
13
11
  this.apiPrefix = '<%= api.prefix %>'
14
12
  this.apiExt = '<%= api.ext %>'
15
13
  this.apiHeaderKey = '<%= api.headerKey %>'
@@ -59,6 +57,7 @@ class Wmpa {
59
57
  }
60
58
 
61
59
  init = () => {
60
+ this.reqId = document.querySelector('meta[name="req-id"]').getAttribute('content')
62
61
  window.addEventListener('load', evt => {
63
62
  if (window.hljs) window.hljs.highlightAll()
64
63
  })
@@ -103,7 +102,7 @@ class Wmpa {
103
102
  fetchRender = async (body, qs = {}) => {
104
103
  if (_.isArray(body)) body = body.join('\n')
105
104
  let url = this.renderUrl + '?'
106
- _.forOwn(_.omit(qs, ['theme', 'iconset']), (v, k) => {
105
+ _.forOwn(qs, (v, k) => {
107
106
  url += '&' + k + '=' + v
108
107
  })
109
108
  const opts = {
@@ -111,8 +110,7 @@ class Wmpa {
111
110
  headers: {
112
111
  'Content-Type': 'text/plain',
113
112
  'X-Referer': window.location.href,
114
- 'X-Theme': this.theme,
115
- 'X-Iconset': this.iconset
113
+ 'X-Req-Id': this.reqId
116
114
  },
117
115
  body
118
116
  }
@@ -1,12 +1,24 @@
1
1
  const component = {
2
2
  method: 'POST',
3
+ noCacheReq: true,
3
4
  handler: async function (req, reply) {
5
+ const { merge, get } = this.app.lib._
4
6
  req.referer = req.headers['x-referer']
7
+ const pageId = req.headers['x-req-id']
5
8
  const { ext = '.html' } = req.body
9
+ let params = req.query
6
10
  reply.header('Content-Type', `text/html; charset=${this.config.page.charset}`)
7
11
  reply.header('Content-Language', req.lang)
8
- const opts = { req, reply, partial: true, ext, theme: req.headers['x-theme'], iconset: req.headers['x-iconset'] }
9
- return this.renderString(req.body, req.query, opts)
12
+ if (pageId) {
13
+ try {
14
+ const locals = await this.app.cache.load(`${this.ns}.req:/${pageId}-locals.json`, this.config.reqTtlDur)
15
+ params = merge({}, locals ?? {}, params)
16
+ } catch (err) {}
17
+ }
18
+ const theme = get(req, 'headers.x-theme', get(params, '_meta.theme.name'))
19
+ const iconset = get(req, 'headers.x-iconset', get(params, '_meta.iconset.name'))
20
+ const opts = { pageId, req, reply, partial: true, ext, theme, iconset }
21
+ return this.renderString(req.body, params, opts)
10
22
  }
11
23
  }
12
24
 
@@ -1,6 +1,7 @@
1
1
  const wmpa = {
2
2
  url: '/wmpa.js',
3
3
  method: 'GET',
4
+ noCacheReq: true,
4
5
  handler: async function (req, reply) {
5
6
  const { get, trim, cloneDeep } = this.app.lib._
6
7
  const { getPluginPrefix } = this.app.waibu
package/index.js CHANGED
@@ -43,6 +43,7 @@ async function factory (pkgName) {
43
43
  ttlDur: 0,
44
44
  urls: []
45
45
  },
46
+ refreshDur: '1d',
46
47
  insertWarning: false,
47
48
  usePluginTitle: false,
48
49
  scriptsAtEndOfBody: true
@@ -136,7 +137,8 @@ async function factory (pkgName) {
136
137
  },
137
138
  compress: false,
138
139
  rateLimit: false,
139
- disabled: []
140
+ disabled: [],
141
+ reqTtlDur: '1d'
140
142
  }
141
143
 
142
144
  this.configDev = {
@@ -495,23 +497,17 @@ async function factory (pkgName) {
495
497
  return output
496
498
  }
497
499
 
498
- renderString = async (text, params = {}, opts = {}) => {
499
- const { importModule } = this.app.bajo
500
- const buildLocals = await importModule('waibu:/lib/build-locals.js')
501
- const locals = await buildLocals.call(this, { tpl: null, params, opts })
500
+ renderString = async (text, locals = {}, opts = {}) => {
502
501
  const ve = this.getViewEngine(opts.ext)
503
502
  return await ve.renderString(text, locals, opts)
504
503
  }
505
504
 
506
- render = async (tpl, params = {}, opts = {}) => {
507
- const { importModule } = this.app.bajo
508
- const buildLocals = await importModule('waibu:/lib/build-locals.js')
509
- const locals = await buildLocals.call(this, { tpl, params, opts })
505
+ render = async (tpl, locals = {}, opts = {}) => {
510
506
  const ext = path.extname(tpl)
511
507
  if (['.json', '.js', '.css'].includes(ext)) opts.partial = true
512
508
  opts.ext = ext
513
- const viewEngine = this.getViewEngine(ext)
514
- return await viewEngine.render(tpl, locals, opts)
509
+ const ve = this.getViewEngine(ext)
510
+ return await ve.render(tpl, locals, opts)
515
511
  }
516
512
 
517
513
  stripHtmlTags = (html, options = {}) => {
@@ -11,7 +11,19 @@ async function meta (options) {
11
11
  const { sprintf } = this.app.lib
12
12
  const { $, theme, req, locals } = options ?? {}
13
13
  const { page = {} } = locals
14
- let items = []
14
+ let items = [{
15
+ tag: 'meta',
16
+ name: 'req-id',
17
+ content: req.id
18
+ }]
19
+ const refreshDur = get(req, 'routeOptions.config.refreshDur', get(this, 'config.page.refreshDur', 0))
20
+ if (refreshDur > 0) {
21
+ items.push({
22
+ tag: 'meta',
23
+ httpEquiv: 'Refresh',
24
+ content: Math.floor(refreshDur / 60) + ''
25
+ })
26
+ }
15
27
  // meta
16
28
  for (const attr of keys(omit(locals.page, [...omitted]))) {
17
29
  if (!page[attr]) continue
@@ -38,7 +38,7 @@ async function injectElements (options) {
38
38
  }
39
39
  options.inlineScript = rsc.inlineScript
40
40
  options.inlineCss = rsc.inlineCss
41
- await runHook(`${this.ns}:beforeBuildPageInjectElement`, options)
41
+ await runHook(`${this.ns}.injectElement:beforeBuildPage`, options)
42
42
  await injectMeta.call(this, options)
43
43
  await injectLink.call(this, options)
44
44
  await injectCss.call(this, options)
package/lib/build-page.js CHANGED
@@ -33,7 +33,7 @@ async function buildPage ({ text, locals = {}, opts = {} } = {}) {
33
33
  const ns = req.ns
34
34
  let $ = cheerio.load(text, this.config.cheerio.loadOptions, false)
35
35
  if (partial) $('c\\:page-start, c\\:page-end').remove()
36
- await runHook(`${this.ns}:beforeBuildPage${partial ? 'Partial' : ''}`, { $, theme, iconset, req, reply, locals, ns, text })
36
+ await runHook(`${this.ns}${partial ? '.partial' : ''}:beforeBuildPage`, { $, theme, iconset, req, reply, locals, ns, text })
37
37
  await attrsMutation.call(this, { $, el: $.root(), req })
38
38
  const cmp = await theme.createComponent({ $, iconset, req, reply, locals: cloneDeep(locals), scriptBlock: reqAsset[req.id].scriptBlock, styleBlock: reqAsset[req.id].styleBlock })
39
39
  await replaceTag.call(this, { el: $.root(), cmp, opts })
@@ -1,7 +1,7 @@
1
1
  import path from 'path'
2
2
 
3
3
  export async function build ({ files, pathPrefix, dir, ns, cfg, parent, urlPrefix, subRoute }) {
4
- const { defaultsDeep } = this.app.lib.aneka
4
+ const { defaultsDeep, parseObject } = this.app.lib.aneka
5
5
  const { importModule, readJson } = this.app.bajo
6
6
  const { isFunction, isPlainObject, pick, last, camelCase, omit } = this.app.lib._
7
7
  const { titleize } = this.app.lib.aneka
@@ -20,6 +20,12 @@ export async function build ({ files, pathPrefix, dir, ns, cfg, parent, urlPrefi
20
20
  if (isFunction(mod)) mod = [{ handler: mod }]
21
21
  else if (isPlainObject(mod)) mod = [mod]
22
22
  for (let m of mod) {
23
+ const mhandler = m.handler // parseObject has problem with function
24
+ const murl = m.url
25
+ m = parseObject(omit(m, ['handler', 'url'], { parseValue: true }))
26
+ m.handler = mhandler
27
+ m.url = murl
28
+
23
29
  m.url = m.url ?? url
24
30
  if (isFunction(m.url)) m.url = await m.url.call(this)
25
31
  if (m.redirect) {
@@ -48,6 +54,7 @@ export async function build ({ files, pathPrefix, dir, ns, cfg, parent, urlPrefi
48
54
  m.config.xSite = m.xSite
49
55
  m.config.ns = ns
50
56
  m.config.subNs = ''
57
+ m.config.noCacheReq = m.noCacheReq
51
58
  m.config.title = m.title ?? camelCase(last(m.url.split('/')))
52
59
  m.config.subRoute = subRoute
53
60
  if (m.cache === true) m.cache = omit(me.config.page.cache, ['urls'])
@@ -280,7 +280,7 @@ async function componentFactory () {
280
280
  * @returns {Promise<string>} The built sentence.
281
281
  */
282
282
  buildSentence = async (sentence, params = {}, extra = {}) => {
283
- const { get } = this.app.lib._
283
+ const { get, merge } = this.app.lib._
284
284
  if (Array.isArray(sentence)) sentence = sentence.join(' ')
285
285
  const { minify, renderString } = this.app.waibuMpa
286
286
  if (extra.wrapped) sentence = '<w>' + sentence + '</w>'
@@ -292,7 +292,7 @@ async function componentFactory () {
292
292
  theme: get(this, 'theme.name', 'default'),
293
293
  iconset: get(this, 'iconset.name', 'default')
294
294
  }
295
- let html = await renderString(sentence, params, opts)
295
+ let html = await renderString(sentence, merge({}, this.locals, params), opts)
296
296
  if (extra.wrapped) html = html.slice(3, html.length - 4)
297
297
  if (extra.minify) html = await minify(html)
298
298
  return html
@@ -24,6 +24,7 @@ async function viewEngineFactory () {
24
24
  }
25
25
 
26
26
  renderString = async (content, locals = {}, opts = {}) => {
27
+ locals.tpl = null
27
28
  this._applySetting(locals, opts)
28
29
  return await this.app.bajoTemplate.renderString(content, locals, opts)
29
30
  }
package/lib/decorate.js CHANGED
@@ -19,8 +19,10 @@ async function isCacheable (req, cachedUrls) {
19
19
  }
20
20
 
21
21
  async function decorate () {
22
+ const { importModule } = this.app.bajo
23
+ const buildLocals = await importModule('waibu:/lib/build-locals.js')
22
24
  const { importPkg } = this.app.bajo
23
- const { isString, cloneDeep, isEmpty } = this.app.lib._
25
+ const { isString, cloneDeep, isEmpty, get } = this.app.lib._
24
26
  const { get: getCache, set: setCache } = this.app.bajoCache ?? {}
25
27
  const { outmatch } = this.app.lib
26
28
  const { routePath } = this.app.waibu
@@ -46,6 +48,7 @@ async function decorate () {
46
48
  mimeType += `; charset=${cfg.page.charset}`
47
49
  this.header('Content-Type', mimeType)
48
50
  this.header('Content-Language', this.request.lang)
51
+ this.header('X-Req-Id', this.request.id)
49
52
  opts.req = this.request
50
53
  opts.reply = this
51
54
  for (const item of ['theme', 'iconset']) {
@@ -60,7 +63,11 @@ async function decorate () {
60
63
  return cached
61
64
  }
62
65
  }
63
- const result = await me.render(tpl, params, opts)
66
+ const locals = await buildLocals.call(me, { tpl, params, opts })
67
+ if (!get(this.request, 'routeOptions.config.noCacheReq')) {
68
+ await me.app.cache.save(`${me.ns}.req:/${opts.req.id}-locals.json`, locals, me.config.reqTtlDur)
69
+ }
70
+ const result = await me.render(tpl, locals, opts)
64
71
  if (ttl > 0) await setCache({ key, value: result, ttl })
65
72
  if (this.request.session) {
66
73
  ext = path.extname(this.request.url)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waibu-mpa",
3
- "version": "2.15.2",
3
+ "version": "2.17.0",
4
4
  "description": "MPA support for Waibu Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-28
4
+
5
+ - [2.17.0] Change hooks to be written in one ```hook.js``` file
6
+ - [2.17.0] Change model schemas to be written in one ```model.js``` file
7
+
8
+ ## 2026-05-25
9
+
10
+ - [2.16.0] Add theme & iconset auto detection mechanism
11
+ - [2.16.0] Add page expiration & reload through ```routeOptions.config.refreshDur``` or ```config.page.refreshDur```
12
+
3
13
  ## 2026-05-22
4
14
 
5
15
  - [2.15.2] Bug fix in ```error-handler.js```
@@ -1,5 +0,0 @@
1
- async function waibuMpaThemeAfterInjectScripts ({ items }) {
2
- // items.push(`${this.ns}.virtual:/json2csv/json2csv.js`)
3
- }
4
-
5
- export default waibuMpaThemeAfterInjectScripts
@@ -1,33 +0,0 @@
1
- async function checkLang (req, reply) {
2
- if (!req.session) return
3
- if (req.langDetector) {
4
- req.session.lang = req.lang
5
- return
6
- }
7
- if (req.session.lang) req.lang = req.session.lang
8
- }
9
-
10
- async function checkDark (req, reply) {
11
- const { isSet } = this.app.lib.aneka
12
- const key = this.config.darkMode.qsKey
13
- const value = this.config.darkMode.set ?? req.query[key]
14
- if (isSet(value)) {
15
- req.darkMode = value
16
- if (req.session) req.session.darkMode = req.darkMode
17
- return
18
- }
19
- if (isSet(req.session.darkMode)) req.darkMode = req.session.darkMode
20
- }
21
-
22
- const waibuMpaPreParsing = {
23
- level: 9,
24
- handler: async function (req, reply) {
25
- const { importModule } = this.app.bajo
26
- const attachIntl = await importModule('waibu:/lib/webapp-scope/attach-intl.js')
27
- await attachIntl.call(this, this.config.intl.detectors, req, reply)
28
- await checkLang.call(this, req, reply)
29
- await checkDark.call(this, req, reply)
30
- }
31
- }
32
-
33
- export default waibuMpaPreParsing