waibu-mpa 2.2.0 → 2.3.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.
@@ -6,10 +6,12 @@ import collectIconsets from '../../lib/collect-iconsets.js'
6
6
  import handleSession from '../../lib/session/setup.js'
7
7
  import subApp from '../../lib/sub-app.js'
8
8
  import errorHandler from '../../lib/error-handler.js'
9
+ import notFoundHandler from '../../lib/not-found-handler.js'
9
10
 
10
11
  const boot = {
11
12
  level: 10,
12
13
  errorHandler,
14
+ notFoundHandler,
13
15
  handler: async function (prefix) {
14
16
  const { importPkg, importModule } = this.app.bajo
15
17
  const bodyParser = await importPkg('waibu:@fastify/formbody')
@@ -346,7 +346,7 @@ const mapping = {
346
346
  zoomOut: ''
347
347
  }
348
348
 
349
- function iconset (ctx) {
349
+ function iconset () {
350
350
  return {
351
351
  name: 'default',
352
352
  css: 'waibuMpa.asset:/css/linearicons.css',
package/index.js CHANGED
@@ -40,7 +40,8 @@ async function factory (pkgName) {
40
40
  charset: 'utf-8',
41
41
  cacheMaxAge: 0,
42
42
  insertWarning: false,
43
- usePluginTitle: false
43
+ usePluginTitle: false,
44
+ scriptsAtEndOfBody: true
44
45
  },
45
46
  darkMode: {
46
47
  set: null,
@@ -231,7 +232,7 @@ async function factory (pkgName) {
231
232
 
232
233
  getSessionId = (rawCookie, secure) => {
233
234
  const cookieName = this.config.session.cookieName
234
- return this.ctx.parseCookie(rawCookie)[cookieName]
235
+ return this.webAppCtx.parseCookie(rawCookie)[cookieName]
235
236
  }
236
237
 
237
238
  getViewEngine = (ext) => {
@@ -7,10 +7,10 @@ export function printLink (link) {
7
7
 
8
8
  async function css (options) {
9
9
  const { $ } = options ?? {}
10
- const inline = await collectInline.call(this, 'inlineCss', options)
11
- if (inline.length > 0) $('head').prepend(`<style>\n${inline.join('\n')}\n</style>`)
12
10
  const regular = await collectRegular.call(this, 'css', printLink, options)
13
- if (regular.length > 0) $('head').prepend(regular.join('\n'))
11
+ if (regular.length > 0) $('head').append(regular.join('\n'))
12
+ const inline = await collectInline.call(this, 'inlineCss', options)
13
+ if (inline.length > 0) $('head').append(`<style>\n${inline.join('\n')}\n</style>`)
14
14
  }
15
15
 
16
16
  export default css
@@ -4,13 +4,15 @@ export function printLink (link) {
4
4
  const { routePath } = this.app.waibu
5
5
  const { isString } = this.app.lib._
6
6
  const item = isString(link) ? { href: link, rel: 'stylesheet', type: 'text/css' } : link
7
- return `<link href="${routePath(item.href)}" rel="${item.rel}" type="${item.type}" />`
7
+ if (item.href) item.href = routePath(item.href)
8
+ const attrs = this.stringifyAttribs(item)
9
+ return `<link ${attrs} />`
8
10
  }
9
11
 
10
12
  async function link (options) {
11
13
  const { $ } = options ?? {}
12
14
  const regular = await collectRegular.call(this, 'links', printLink, options)
13
- if (regular.length > 0) $('head').prepend(regular.join('\n'))
15
+ if (regular.length > 0) $('head').append(regular.join('\n'))
14
16
  }
15
17
 
16
18
  export default link
@@ -7,14 +7,13 @@ const names = ['description', 'keywords', 'robots', 'viewport', 'author', 'publi
7
7
 
8
8
  async function meta (options) {
9
9
  const { runHook, importPkg } = this.app.bajo
10
- const { map, uniq, isArray, kebabCase, keys, omit, get, isFunction } = this.app.lib._
10
+ const { map, uniq, isArray, keys, omit, get, isFunction } = this.app.lib._
11
11
  const { sprintf } = this.app.lib
12
12
  const { $, theme, req, locals } = options ?? {}
13
13
  const { page = {} } = locals
14
14
  let items = []
15
15
  // meta
16
- const links = ['preconnect', 'dnsPrefetch']
17
- for (const attr of keys(omit(locals.page, [...links, ...omitted]))) {
16
+ for (const attr of keys(omit(locals.page, [...omitted]))) {
18
17
  if (!page[attr]) continue
19
18
  if (!names.includes(attr)) continue
20
19
  items.push({
@@ -22,28 +21,19 @@ async function meta (options) {
22
21
  content: isArray(page[attr]) ? page[attr].join(', ') : page[attr]
23
22
  })
24
23
  }
25
- // links
26
- for (const attr of links) {
27
- if (!page[attr]) continue
28
- const all = isArray(page[attr]) ? page[attr] : page[attr] = [page[attr]]
29
- for (const a of all) {
30
- const [href, ...params] = a.split('|')
31
- const item = {
32
- tag: 'link',
33
- rel: kebabCase(attr),
34
- href
35
- }
36
- for (const p of params ?? []) {
37
- item[p] = true
38
- }
39
- items.push(item)
40
- }
41
- }
42
24
  if (this.config.theme && this.config.theme.autoInsert.meta) {
43
25
  await runHook(`${this.ns}.${theme.name}:beforeInjectMeta`, { meta: theme.meta, items, req })
44
26
  items.push(...(theme.meta ?? []))
45
27
  await runHook(`${this.ns}.${theme.name}:afterInjectMeta`, { meta: theme.meta, items, req })
46
28
  }
29
+ // title
30
+ const formatter = get(this, 'app.waibuMpa.pageTitleFormat', '%s : %s')
31
+ const title = page.fullTitle ?? page.title
32
+ let pageTitle
33
+ if (isFunction(formatter)) pageTitle = await formatter.call(this, locals)
34
+ else pageTitle = sprintf(formatter, title, this.app.waibuMpa.getAppTitle(req.lang))
35
+ $('head').append(`<title>${pageTitle}</title>`)
36
+ // favicon
47
37
  const favicon = this.app.waibu.config.favicon
48
38
  if (favicon) {
49
39
  const mime = await importPkg('waibu:mime')
@@ -51,20 +41,14 @@ async function meta (options) {
51
41
  const type = mime.getType(ext)
52
42
  items.push({ tag: 'link', href: `/favicon${ext}`, rel: 'icon', type })
53
43
  }
44
+ // meta
54
45
  items = map(items, m => {
55
46
  const tag = m.tag ?? 'meta'
56
47
  delete m.tag
57
48
  const attrs = this.stringifyAttribs(m)
58
49
  return `<${tag} ${attrs} />`
59
50
  })
60
- $('head').prepend(uniq(items).join('\n'))
61
- // title
62
- const formatter = get(this, 'app.waibuMpa.pageTitleFormat', '%s : %s')
63
- const title = page.fullTitle ?? page.title
64
- let pageTitle
65
- if (isFunction(formatter)) pageTitle = await formatter.call(this, locals)
66
- else pageTitle = sprintf(formatter, title, this.app.waibuMpa.getAppTitle(req.lang))
67
- $('head').append(`<title>${pageTitle}</title>`)
51
+ $('head').append(uniq(items).join('\n'))
68
52
  }
69
53
 
70
54
  export default meta
@@ -64,12 +64,13 @@ export async function collectInline (type, options = {}) {
64
64
  async function script (options = {}) {
65
65
  const { $, cmp, locals } = options
66
66
  const { render } = this.app.bajoTemplate
67
+ const parent = this.config.page.scriptsAtEndOfBody ? 'body' : 'head'
67
68
  const regular = await collectRegular.call(this, 'scripts', printScript, options)
68
- if (regular.length > 0) $('body').append(regular.join('\n'))
69
+ if (regular.length > 0) $(parent).append(regular.join('\n'))
69
70
  const inline = await collectInline.call(this, 'inlineScript', options)
70
71
  if (inline.length > 0) cmp.scriptBlock.root.push(...inline)
71
72
  const script = await render(locals.page.scriptBlock, { block: cmp.scriptBlock }, { default: 'bajoTemplate.partial:/script-block.html' })
72
- $('body').append(script)
73
+ $(parent).append(script)
73
74
  }
74
75
 
75
76
  export default script
@@ -14,7 +14,7 @@ async function injectElements (options) {
14
14
  let Builder = get(cmp, `widget.${tag}`)
15
15
  if (!isFunction(Builder)) continue
16
16
  if (!isClass(Builder)) Builder = await Builder.call(cmp)
17
- for (const key of ['scripts', 'css', 'links']) {
17
+ for (const key of ['links', 'scripts', 'css']) {
18
18
  let item = Builder[key] ?? []
19
19
  if (isString(item)) item = [item]
20
20
  if (isFunction(item)) item = await item.call(cmp, req)
@@ -32,15 +32,15 @@ async function injectElements (options) {
32
32
  }
33
33
  }
34
34
  }
35
- for (const key of ['scripts', 'css', 'links']) {
35
+ for (const key of ['links', 'scripts', 'css']) {
36
36
  options[key] = rsc[key] ?? []
37
37
  }
38
38
  options.inlineScript = rsc.inlineScript
39
39
  options.inlineCss = rsc.inlineCss
40
40
  await runHook(`${this.ns}:beforeBuildPageInjectElement`, options)
41
+ await injectMeta.call(this, options)
41
42
  await injectLink.call(this, options)
42
43
  await injectCss.call(this, options)
43
- await injectMeta.call(this, options)
44
44
  await injectScript.call(this, options)
45
45
  }
46
46
 
@@ -5,9 +5,9 @@ async function themeFactory () {
5
5
  this.name = name
6
6
  this.css = []
7
7
  this.meta = []
8
+ this.links = []
8
9
  this.moveToEnd = ''
9
10
  this.scipts = undefined
10
- this.links = undefined
11
11
  this.inlineCss = undefined
12
12
  this.inlineScript = undefined
13
13
  this.framework = undefined
@@ -1,4 +1,4 @@
1
- import { handler } from './not-found.js'
1
+ import notFoundHandler from './not-found-handler.js'
2
2
 
3
3
  async function errorHandler (err, req, reply) {
4
4
  const { getPluginFile } = this.app.bajo
@@ -9,7 +9,7 @@ async function errorHandler (err, req, reply) {
9
9
  reply.code(err.statusCode)
10
10
 
11
11
  if (err.message === '_notFound' || err.statusCode === 404) {
12
- return await handler.call(this, req, reply, err)
12
+ return await notFoundHandler.call(this, err, req, reply)
13
13
  }
14
14
  if (err.noContent) return ''
15
15
  const ns = err.ns ?? this.ns
@@ -1,20 +1,22 @@
1
1
  async function loadResource (mod = [], item) {
2
2
  const { breakNsPath, readConfig } = this.app.bajo
3
- const { isArray, isEmpty } = this.app.lib._
3
+ const { isString, isArray, isEmpty } = this.app.lib._
4
4
 
5
5
  if (isEmpty(mod[item])) return []
6
6
  if (!isArray(mod[item])) mod[item] = [mod[item]]
7
7
  const items = []
8
8
  const extItems = []
9
9
  for (const i in mod[item]) {
10
- if (mod[item][i].startsWith('/')) items.push(mod.css[i])
11
- else {
12
- let name = mod[item][i]
13
- if (['$', '^'].includes(name[0])) name = name.slice(1)
14
- const { ns, path, subNs } = breakNsPath(name, undefined, false)
15
- if (subNs === 'load') extItems.push({ ns, path })
16
- else items.push(mod[item][i])
17
- }
10
+ if (isString(mod[item][i])) {
11
+ if (mod[item][i].startsWith('/')) items.push(mod.css[i])
12
+ else {
13
+ let name = mod[item][i]
14
+ if (['$', '^'].includes(name[0])) name = name.slice(1)
15
+ const { ns, path, subNs } = breakNsPath(name, undefined, false)
16
+ if (subNs === 'load') extItems.push({ ns, path })
17
+ else items.push(mod[item][i])
18
+ }
19
+ } else items.push(mod[item][i])
18
20
  }
19
21
  for (const c of extItems) {
20
22
  let emod = await readConfig(`${c.ns}:${c.path}`, { ns: c.ns })
@@ -0,0 +1,20 @@
1
+ async function notFoundHandler (err, req, reply) {
2
+ const welcome = req.url.split('?')[0] === '/'
3
+ const { resolveTemplate } = this.app.bajoTemplate
4
+ const msg = req.t('routeNotFound%s%s', req.url, req.method)
5
+ const error = err ?? this.error(msg)
6
+ if (err) error.message = msg
7
+ error.statusCode = 404
8
+ reply.code(404)
9
+ if (error.noContent) return ''
10
+ const ns = error.ns ?? this.ns
11
+ let tpl = welcome ? `${this.ns}.template:/welcome.html` : `${ns}.template:/404.html`
12
+ try {
13
+ await resolveTemplate(tpl)
14
+ } catch (err) {
15
+ tpl = `${this.ns}.template:/404.html`
16
+ }
17
+ if (reply.view) return await reply.view(tpl, { error })
18
+ }
19
+
20
+ export default notFoundHandler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waibu-mpa",
3
- "version": "2.2.0",
3
+ "version": "2.3.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,14 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-02-09
4
+
5
+ - [2.3.0] Add ```config.page.scriptsAtEndOfBody``` to put scripts at the end of body or not. Defaults to ```true```
6
+ - [2.3.0] Add not found & error handlers
7
+ - [2.3.0] Bug fix on old context
8
+ - [2.3.0] Bug fix on ```loadResource()```
9
+ - [2.3.0] Bug fix on ```<link />``` injection
10
+ - [2.3.0] Bug fix on order of metas, links & scripts
11
+
3
12
  ## 2026-02-05
4
13
 
5
14
  - [2.1.11] Bug fix on rendering ```preconnect```
package/lib/not-found.js DELETED
@@ -1,58 +0,0 @@
1
- function redirSvc (req) {
2
- const { trim, find, get } = this.app.lib._
3
- const { outmatch } = this.app.lib
4
- let match = false
5
- let [prefix, ...args] = trim(req.url, '/').split('/')
6
- args = '/' + args.join('/')
7
- let plugin = find(this.app.getAllNs(), p => {
8
- return get(this, `app.${p}.config.waibu.prefix`) === prefix
9
- })
10
- if (!plugin) {
11
- plugin = 'main'
12
- args = `/${prefix}`
13
- }
14
- const items = get(this, `app.${plugin}.config.waibuMpa.redirect`, {})
15
- for (const k in items) {
16
- const isMatch = outmatch(k)
17
- if (isMatch(args)) {
18
- match = items[k]
19
- break
20
- }
21
- }
22
- return match
23
- }
24
-
25
- export async function handler (req, reply, err) {
26
- const { getMethod } = this.app.bajo
27
- const { resolveTemplate } = this.app.bajoTemplate
28
- let redirectTo = await redirSvc.call(this, req, reply)
29
- if (redirectTo !== false) {
30
- const fn = getMethod(redirectTo, false)
31
- if (fn) redirectTo = await fn(req)
32
- if (redirectTo) return reply.redirectTo(redirectTo)
33
- }
34
- const welcome = req.url.split('?')[0] === '/'
35
- const msg = req.t('routeNotFound%s%s', req.url, req.method)
36
- const error = err ?? this.error(msg)
37
- if (err) error.message = msg
38
- error.statusCode = 404
39
- reply.code(404)
40
- if (error.noContent) return ''
41
- const ns = error.ns ?? this.ns
42
- let tpl = welcome ? `${this.ns}.template:/welcome.html` : `${ns}.template:/404.html`
43
- try {
44
- await resolveTemplate(tpl)
45
- } catch (err) {
46
- tpl = `${this.ns}.template:/404.html`
47
- }
48
- if (reply.view) return await reply.view(tpl, { error })
49
- }
50
-
51
- async function notFound (ctx) {
52
- const me = this
53
- await ctx.setNotFoundHandler(async function (req, reply) {
54
- return await handler.call(me, req, reply)
55
- })
56
- }
57
-
58
- export default notFound