@bakery-framework/plugin-vue 2.2.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/plugin-vue",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "description": "Bakery vue plugin.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -43,7 +43,7 @@
43
43
  "vue": "^3.5.38"
44
44
  },
45
45
  "dependencies": {
46
- "@bakery-framework/core": "^2.2.2"
46
+ "@bakery-framework/core": "^2.4.0"
47
47
  },
48
48
  "engines": {
49
49
  "bun": ">=1.4.0"
package/src/client.ts CHANGED
@@ -45,6 +45,12 @@ type StampedRoute = {
45
45
  export type LayoutNavigation = {
46
46
  /** Path segments under the base: `[]` on the bare directory. */
47
47
  readonly segments: Ref<string[]>
48
+ /**
49
+ * The query string, as `location.search` spells it: `''`, or `?` and the
50
+ * pairs. Updated with every navigation, a query-only one included, which
51
+ * changes no segment.
52
+ */
53
+ readonly search: Ref<string>
48
54
  /** The URL prefix this page owns. */
49
55
  readonly base: string
50
56
  /** Navigate within the subtree; segments or a path, `/`-prefixed or not. */
@@ -63,18 +69,39 @@ export type LayoutListener = (
63
69
  cause: 'click' | 'navigate' | 'history',
64
70
  ) => boolean | undefined | void
65
71
 
72
+ /**
73
+ * `path` without its query and hash: the part a route is matched on. A link
74
+ * to `/campus/ma/students?standing=pending` used to keep its query in the
75
+ * last segment and render a page named `students?standing=pending`.
76
+ */
77
+ function pathOnly(path: string): string {
78
+ const cut = path.search(/[?#]/)
79
+ return cut < 0 ? path : path.slice(0, cut)
80
+ }
81
+
66
82
  /** Is `path` the base itself or inside it? Prefix-safe: `/admin` ≠ `/admini`. */
67
83
  export function isUnderBase(base: string, path: string): boolean {
68
- if (base === '') return path.startsWith('/')
69
- return path === base || path.startsWith(`${base}/`)
84
+ const pathname = pathOnly(path)
85
+ if (base === '') return pathname.startsWith('/')
86
+ return pathname === base || pathname.startsWith(`${base}/`)
70
87
  }
71
88
 
72
89
  /** The segments of `path` below `base`: `[]` for the base itself. */
73
90
  export function segmentsUnder(base: string, path: string): string[] {
74
- const rest = base === '' ? path : path.slice(base.length)
91
+ const pathname = pathOnly(path)
92
+ const rest = base === '' ? pathname : pathname.slice(base.length)
75
93
  return rest.split('/').filter(Boolean)
76
94
  }
77
95
 
96
+ /** The query of `path`, `''` when it has none, as `location.search` has it. */
97
+ function searchOf(path: string): string {
98
+ const hash = path.indexOf('#')
99
+ const head = hash < 0 ? path : path.slice(0, hash)
100
+ const query = head.indexOf('?')
101
+ // A bare `?` is no query: `location.search` reads `''` for it.
102
+ return query < 0 || query === head.length - 1 ? '' : head.slice(query)
103
+ }
104
+
78
105
  function pathFor(base: string, to: string | string[]): string {
79
106
  if (Array.isArray(to)) {
80
107
  const joined = to.filter(Boolean).join('/')
@@ -110,11 +137,11 @@ export function defineLayout(): LayoutNavigation {
110
137
  return next.length > 0 && claimed.has(next[0])
111
138
  }
112
139
 
113
- const initial =
114
- typeof location !== 'undefined'
115
- ? segmentsUnder(base, location.pathname)
116
- : []
117
- const segments = ref<string[]>(initial)
140
+ const here = typeof location !== 'undefined'
141
+ const segments = ref<string[]>(
142
+ here ? segmentsUnder(base, location.pathname) : [],
143
+ )
144
+ const search = ref(here ? location.search : '')
118
145
 
119
146
  function fire(next: string[], cause: Parameters<LayoutListener>[2]): boolean {
120
147
  const prev = segments.value
@@ -141,8 +168,11 @@ export function defineLayout(): LayoutNavigation {
141
168
  return
142
169
  }
143
170
  if (!fire(next, cause)) return
171
+ // The whole path goes into history, query and hash included: a reload
172
+ // has to land on the same view.
144
173
  if (typeof history !== 'undefined') history.pushState(null, '', path)
145
174
  segments.value = next
175
+ search.value = searchOf(path)
146
176
  }
147
177
 
148
178
  if (typeof document !== 'undefined') {
@@ -167,12 +197,14 @@ export function defineLayout(): LayoutNavigation {
167
197
  url.pathname === location.pathname &&
168
198
  url.search === location.search
169
199
  ) {
200
+ // Only the hash differs: the browser's own same-document scroll.
201
+ if (url.hash && url.hash !== location.hash) return
170
202
  event.preventDefault()
171
203
  return
172
204
  }
173
205
 
174
206
  event.preventDefault()
175
- go(url.pathname + url.search, 'click')
207
+ go(url.pathname + url.search + url.hash, 'click')
176
208
  })
177
209
 
178
210
  window.addEventListener('popstate', () => {
@@ -190,11 +222,13 @@ export function defineLayout(): LayoutNavigation {
190
222
  }
191
223
  fire(next, 'history') // observable, not cancellable. See `on`
192
224
  segments.value = next
225
+ search.value = location.search
193
226
  })
194
227
  }
195
228
 
196
229
  return {
197
230
  segments,
231
+ search,
198
232
  base,
199
233
  navigate: to => go(to, 'navigate'),
200
234
  on(listener) {
package/src/handler.ts CHANGED
@@ -2,6 +2,7 @@ import { readdirSync } from 'node:fs'
2
2
  import { LRUCache } from '@bakery-framework/core/cache/lru'
3
3
  import { Bakery, hostKey } from '@bakery-framework/core/core/bakery'
4
4
  import type { Handler } from '@bakery-framework/core/handlers'
5
+ import { routeOf } from '@bakery-framework/core/router'
5
6
  import {
6
7
  beginPageRoute,
7
8
  DynamicErrorHandler,
@@ -991,8 +992,11 @@ async function sharedHandler(
991
992
 
992
993
  // `base` is the URL prefix the page owns: the file's directory. For
993
994
  // `wiki/[...page!].vue` that is `/wiki`; for a root-level catch-all it is
994
- // the empty string, which `defineLayout` treats as "everything".
995
+ // the empty string, which `defineLayout` treats as "everything". On a host
996
+ // that serves `/wiki` at its root, it is spelled the way that host's URLs
997
+ // are: see `baseOnHost`.
995
998
  const catchAll = Boolean(info.catchAll)
999
+ const routed = routeOf(req)
996
1000
  return VueHandler.handleHtml(
997
1001
  id,
998
1002
  finalParams,
@@ -1001,7 +1005,10 @@ async function sharedHandler(
1001
1005
  parsed,
1002
1006
  {
1003
1007
  catchAll,
1004
- base: routePath.slice(0, routePath.lastIndexOf('/')),
1008
+ base: baseOnHost(
1009
+ routePath.slice(0, routePath.lastIndexOf('/')),
1010
+ routed.mount,
1011
+ ),
1005
1012
  param: info.params.length ? info.params[info.params.length - 1] : null,
1006
1013
  // Catch-all pages only: `defineLayout()` refuses every other page, so
1007
1014
  // on those the claims would be sibling names published in the HTML with
@@ -1011,11 +1018,29 @@ async function sharedHandler(
1011
1018
  // The page's own URL routes back to this component; see `assetPath`. An
1012
1019
  // error render is the exception: its URL is the one that failed, and it
1013
1020
  // would route to the error, not to the error page's component.
1014
- errorData ? routePath : url.pathname,
1021
+ //
1022
+ // The URL as the app names it, which on a mounted host is not the one in
1023
+ // the address bar: `/kit/forms` for a page at `/forms`. The stylesheet and
1024
+ // root script requests are not page requests, so the router serves them
1025
+ // from the app's root, where only the app's spelling reaches the page.
1026
+ errorData ? routePath : routed.path,
1015
1027
  built,
1016
1028
  )
1017
1029
  }
1018
1030
 
1031
+ /**
1032
+ * `base` as a host serving `mount` at its root spells it: `/kit/forms` is
1033
+ * `/forms` there, and `/kit` itself is `''`, which `defineLayout()` reads as
1034
+ * the whole host. A base outside the mount (a catch-all above it, answering a
1035
+ * path the subtree has no page for) has no spelling without the prefix, and
1036
+ * is left as it is.
1037
+ */
1038
+ function baseOnHost(base: string, mount: string | null): string {
1039
+ if (!mount) return base
1040
+ if (base === mount) return ''
1041
+ return base.startsWith(`${mount}/`) ? base.slice(mount.length) : base
1042
+ }
1043
+
1019
1044
  /**
1020
1045
  * The exports of the server blocks of the components a built page reaches,
1021
1046
  * keyed by route path, for the page's HTML.