@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 +2 -2
- package/src/client.ts +43 -9
- package/src/handler.ts +28 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakery-framework/plugin-vue",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
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:
|
|
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
|
-
|
|
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.
|