@ubean/vue 0.2.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/LICENSE +21 -0
- package/README.md +493 -0
- package/README.zh-CN.md +500 -0
- package/dist/generator/index.d.ts +125 -0
- package/dist/generator/index.js +369 -0
- package/dist/index.d.ts +782 -0
- package/dist/index.js +1073 -0
- package/dist/types-VHF1RJu2.d.ts +132 -0
- package/dist/vite.d.ts +164 -0
- package/dist/vite.js +1189 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Soybean
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# @ubean/vue
|
|
2
|
+
|
|
3
|
+
**English | [中文](./README.zh-CN.md)**
|
|
4
|
+
|
|
5
|
+
Lean Vue client kernel & page-routing owner for ubean.
|
|
6
|
+
|
|
7
|
+
`@ubean/vue` owns everything about **page routing** in the ubean ecosystem: file-based route scanning, virtual-module generation, physical route file generation, page cache (keep-alive), page transitions, reload signal, and the `definePage` macro. It ships as a Vue **plugin** (no app factory, no router factory) and depends on **`vue` + `vue-router` only** at runtime.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createApp } from 'vue';
|
|
11
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
12
|
+
import { ubeanVue } from '@ubean/vue';
|
|
13
|
+
import { routes } from 'virtual:ubean-vue-routes'; // generated by @ubean/vue/vite
|
|
14
|
+
|
|
15
|
+
const router = createRouter({ history: createWebHistory(), routes });
|
|
16
|
+
const app = createApp(App);
|
|
17
|
+
app.use(router);
|
|
18
|
+
app.use(ubeanVue, { routes }); // registers Link/PageView/SlotView + seeds page cache
|
|
19
|
+
app.mount('#app');
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Package Positioning](#package-positioning)
|
|
25
|
+
- [Install](#install)
|
|
26
|
+
- [Quick Start](#quick-start)
|
|
27
|
+
- [File-Based Routing](#file-based-routing)
|
|
28
|
+
- [Conventions](#conventions)
|
|
29
|
+
- [`definePage` Macro](#definepage-macro)
|
|
30
|
+
- [Reuse Routes](#reuse-routes)
|
|
31
|
+
- [Special Pages](#special-pages)
|
|
32
|
+
- [Parallel & Intercepting Routes](#parallel--intercepting-routes)
|
|
33
|
+
- [Dynamic Param Matchers](#dynamic-param-matchers)
|
|
34
|
+
- [Markdown Pages (opt-in)](#markdown-pages-opt-in)
|
|
35
|
+
- [Route Output Modes](#route-output-modes)
|
|
36
|
+
- [Vite Plugin API](#vite-plugin-api)
|
|
37
|
+
- [Physical Route File Generator](#physical-route-file-generator)
|
|
38
|
+
- [Virtual Module Surface](#virtual-module-surface)
|
|
39
|
+
- [Vue Plugin](#vue-plugin)
|
|
40
|
+
- [Components](#components)
|
|
41
|
+
- [Composables](#composables)
|
|
42
|
+
- [Page Cache (keep-alive)](#page-cache-keep-alive)
|
|
43
|
+
- [Page Transitions & Reload](#page-transitions--reload)
|
|
44
|
+
- [View Transitions Helpers](#view-transitions-helpers)
|
|
45
|
+
- [Route Matchers API](#route-matchers-api)
|
|
46
|
+
- [Page-Level Head (opt-in)](#page-level-head-opt-in)
|
|
47
|
+
- [Route Pure Functions](#route-pure-functions)
|
|
48
|
+
- [Type Safety](#type-safety)
|
|
49
|
+
- [Architecture Notes](#architecture-notes)
|
|
50
|
+
|
|
51
|
+
## Package Positioning
|
|
52
|
+
|
|
53
|
+
`@ubean/vue` is the **lean client kernel** and the **single owner of page-routing logic** in ubean:
|
|
54
|
+
|
|
55
|
+
| Layer | Responsibility |
|
|
56
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
57
|
+
| `@ubean/vue` (this package) | Page routing: scanning, virtual modules, physical route file generator, matchers, head guard, page cache, transitions, reload — pure client-side |
|
|
58
|
+
| `@ubean/scan` (aggregator) | Depends on `@ubean/vue`; layers server routing (rou3), API route scanning, SSR page handling on top |
|
|
59
|
+
| `@ubean/client` (framework runtime) | App factories (`createUbeanClientApp` / `createUbeanSSRApp`), unhead/SEO, i18n, islands, data layer |
|
|
60
|
+
|
|
61
|
+
**Included:**
|
|
62
|
+
|
|
63
|
+
- `ubeanVue` Vue plugin (the only wiring entry — plugin-first, no factory)
|
|
64
|
+
- `<PageView>` / `<Link>` / `<SlotView>` / `<LayoutChainRenderer>` / `<ErrorBoundary>`
|
|
65
|
+
- Page cache (keep-alive) store with imperative + declarative APIs
|
|
66
|
+
- Page transitions & reload signal (per-page remount with fresh cache)
|
|
67
|
+
- View Transitions helpers (feature detection + wrapper)
|
|
68
|
+
- `definePage` client macro (compile-time extracted, runtime no-op)
|
|
69
|
+
- Dynamic route matchers (`[param=matcher]` syntax + registry + guard)
|
|
70
|
+
- Page-level head (`setupPageHeadGuard` / lazy `createPageHead`)
|
|
71
|
+
- File-routing Vite plugin (`/vite` subpath): multi-dir scan, reuse routes, special pages, parallel/intercepting routes, markdown & head opt-in
|
|
72
|
+
- Physical route file generator (`/generator` subpath): `routes.ts` + `imports.ts` + `typed-router.d.ts` (file mode)
|
|
73
|
+
|
|
74
|
+
**NOT included** (framework runtime `@ubean/client` concerns):
|
|
75
|
+
|
|
76
|
+
- App factories (`createUbeanClientApp` / `createUbeanSSRApp`)
|
|
77
|
+
- unhead / `<Head>` / SEO (only the opt-in head _guard_ lives here)
|
|
78
|
+
- i18n (`Link` renders paths verbatim unless a localizer is provided via `LOCALIZE_PATH_KEY`)
|
|
79
|
+
- Islands (`v-client` directive), SSR state helpers, auto-import presets
|
|
80
|
+
|
|
81
|
+
**Runtime dependency whitelist (main entry): `vue` + `vue-router` only.** No `@ubean/*`, no `node:*`, no `@unhead/vue` static imports.
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm add @ubean/vue vue vue-router
|
|
87
|
+
# build-time (file routing):
|
|
88
|
+
pnpm add -D vite @ubean/vue # the /vite & /generator subpaths are part of this package
|
|
89
|
+
# optional peers (loaded lazily, only when enabled):
|
|
90
|
+
pnpm add @ubean/markdown # markdown pages
|
|
91
|
+
pnpm add @unhead/vue # page-level head (SPA)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Quick Start
|
|
95
|
+
|
|
96
|
+
**1. Vite config** — the plugin scans your pages and generates `virtual:ubean-vue-routes`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// vite.config.ts
|
|
100
|
+
import vue from '@vitejs/plugin-vue';
|
|
101
|
+
import { ubeanVueVite } from '@ubean/vue/vite';
|
|
102
|
+
|
|
103
|
+
export default {
|
|
104
|
+
plugins: [vue(), ubeanVueVite()]
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**2. Bootstrap** — create the router yourself, then install the plugin:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// src/main.ts
|
|
112
|
+
import { createApp } from 'vue';
|
|
113
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
114
|
+
import { ubeanVue } from '@ubean/vue';
|
|
115
|
+
import { routes } from 'virtual:ubean-vue-routes';
|
|
116
|
+
import App from './App.vue';
|
|
117
|
+
|
|
118
|
+
const router = createRouter({ history: createWebHistory(), routes });
|
|
119
|
+
const app = createApp(App);
|
|
120
|
+
app.use(router);
|
|
121
|
+
app.use(ubeanVue, { routes });
|
|
122
|
+
app.mount('#app');
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**3. App shell** — `<PageView />` is the page outlet (keep-alive / transition / reload all take effect there):
|
|
126
|
+
|
|
127
|
+
```vue
|
|
128
|
+
<!-- src/App.vue -->
|
|
129
|
+
<template>
|
|
130
|
+
<nav>
|
|
131
|
+
<Link to="/">Home</Link>
|
|
132
|
+
<Link to="/about">About</Link>
|
|
133
|
+
</nav>
|
|
134
|
+
<main><PageView /></main>
|
|
135
|
+
</template>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A complete runnable example lives at [`examples/client-only-spa`](../../examples/client-only-spa).
|
|
139
|
+
|
|
140
|
+
## File-Based Routing
|
|
141
|
+
|
|
142
|
+
### Conventions
|
|
143
|
+
|
|
144
|
+
Default page extensions are `vue` / `tsx` / `jsx` (override with the `extensions` option). `.ts` is **not** a page extension — script pages must use `.tsx` / `.jsx` or `.vue`. Layouts use `vue` / `ts`.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
src/pages/
|
|
148
|
+
├── index.vue → / Home
|
|
149
|
+
├── about.vue → /about About
|
|
150
|
+
├── users/index.vue → /users Users
|
|
151
|
+
├── users/[id].vue → /users/:id UsersId
|
|
152
|
+
├── users/[id=numeric].vue → /users/:id + matcher (see matchers)
|
|
153
|
+
├── blog/[...slug].vue → /blog/**:slug BlogAllSlug (catch-all)
|
|
154
|
+
├── docs/[[page]].vue → /docs/:page? DocsPageOptional
|
|
155
|
+
├── (marketing)/about.vue → /about About (group stripped)
|
|
156
|
+
├── settings.tsx → /settings Settings (JSX page)
|
|
157
|
+
├── 404.vue → catch-all + NotFound (special page)
|
|
158
|
+
└── guide.md → /guide Guide (markdown, opt-in)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Multiple `pagesDir` / `layoutsDir` are supported (first-seen-wins dedup, so folders can be layered).
|
|
162
|
+
|
|
163
|
+
- Route names are derived from paths in PascalCase (`users/[id]` → `UsersId`).
|
|
164
|
+
- `definePage({ name, path })` can override both.
|
|
165
|
+
- Route groups `(name)` never contribute to the URL or the route name.
|
|
166
|
+
|
|
167
|
+
### `definePage` Macro
|
|
168
|
+
|
|
169
|
+
Declare page metadata at the top of `<script setup>` (or top-level in `.tsx`/`.jsx`/`.reuse.ts`):
|
|
170
|
+
|
|
171
|
+
```vue
|
|
172
|
+
<script setup lang="ts">
|
|
173
|
+
import { definePage } from '@ubean/vue';
|
|
174
|
+
|
|
175
|
+
definePage({
|
|
176
|
+
name: 'UserProfile', // override the file-derived route name
|
|
177
|
+
path: '/u/:id', // override the file-derived path
|
|
178
|
+
cache: true, // keep-alive page cache
|
|
179
|
+
transition: 'fade', // page-level transition name
|
|
180
|
+
layout: 'admin', // layout marker: name, array (outer→inner), or false
|
|
181
|
+
requiresAuth: true, // auth marker → route.meta.requiresAuth
|
|
182
|
+
head: { title: 'Profile' }, // page head (requires plugin `head: true`)
|
|
183
|
+
meta: { custom: 'any' } // arbitrary RouteMeta extensions
|
|
184
|
+
});
|
|
185
|
+
</script>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Dual semantics:
|
|
189
|
+
|
|
190
|
+
- **Build time (recommended):** the `/vite` plugin extracts the argument object, merges it into the generated route table, and strips the call from the output (zero runtime cost).
|
|
191
|
+
- **Runtime fallback:** without the plugin the function is a no-op — no errors, no side effects; pages register with file-derived paths/names. `cache` can be restored at runtime via `enablePageCache(name)`.
|
|
192
|
+
|
|
193
|
+
> Use `meta: { middleware: [...] }` and consume it in your own navigation guard.
|
|
194
|
+
|
|
195
|
+
### Reuse Routes
|
|
196
|
+
|
|
197
|
+
`.reuse.ts` / `.reuse.js` are **pure metadata files** (they only contain `definePage({ reuse })`) that register a new route reusing another page's component:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// src/pages/about2.reuse.ts
|
|
201
|
+
definePage({ reuse: 'About' });
|
|
202
|
+
// → route /about2 renders the About page component
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- `.reuse.vue` is **NOT** part of the convention — a `.vue` file is a real component and is treated as a regular page (route `/xxx.reuse`).
|
|
206
|
+
- Reuse routes that don't declare `cache` explicitly **inherit** the target's cache setting; declare `cache: false` to opt out.
|
|
207
|
+
- The target must be a regular (non-reuse) page.
|
|
208
|
+
|
|
209
|
+
### Special Pages
|
|
210
|
+
|
|
211
|
+
Root-level files in a pages directory are wired to framework slots:
|
|
212
|
+
|
|
213
|
+
| File | Role |
|
|
214
|
+
| ----------------------------- | ---------------------------------------------------------------- |
|
|
215
|
+
| `404.vue` (or `.tsx` / `.md`) | Vue Router catch-all `/:pathMatch(.*)*` named `NotFound` |
|
|
216
|
+
| `loading.vue` | `<Suspense>` fallback while async pages load (via `LOADING_KEY`) |
|
|
217
|
+
| `error.vue` | `<ErrorBoundary>` error component (via `ERROR_KEY`) |
|
|
218
|
+
|
|
219
|
+
Only root-level files are special — `users/404.vue` remains a regular route at `/users/404`.
|
|
220
|
+
|
|
221
|
+
### Parallel & Intercepting Routes
|
|
222
|
+
|
|
223
|
+
- **Parallel routes:** `@slotName/` directories register named views; render them with `<SlotView name="slotName" />`.
|
|
224
|
+
- **Intercepting routes:** `(..)target/`, `(.)target/`, `(...)target/` (one level up / same level / root), Next.js-style.
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
src/pages/dashboard/@analytics/index.vue → parallel slot "analytics"
|
|
228
|
+
src/pages/photos/(..)photo/[id].vue → intercept /photo/:id from /photos/*
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Dynamic Param Matchers
|
|
232
|
+
|
|
233
|
+
`[param=matcherName]` file syntax attaches a validation matcher to a dynamic segment:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
src/pages/users/[id=numeric].vue → /users/:id + meta.matchers = { id: 'numeric' }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Register matchers with `defineMatcher` (see [Route Matchers API](#route-matchers-api)) and enforce them with `createMatcherGuard`. Failed validation redirects to the `NotFound` route.
|
|
240
|
+
|
|
241
|
+
### Markdown Pages (opt-in)
|
|
242
|
+
|
|
243
|
+
Enable with `markdown: true` (`md` + `mdx`), `'md'`, or `'mdx'`. Requires `@ubean/markdown` (optional peer, loaded lazily only when enabled):
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
ubeanVueVite({ markdown: true });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Frontmatter maps onto page metadata (`name` / `path` / `layout` / `cache` / `head`):
|
|
250
|
+
|
|
251
|
+
```md
|
|
252
|
+
---
|
|
253
|
+
name: Guide
|
|
254
|
+
layout: docs
|
|
255
|
+
cache: true
|
|
256
|
+
head:
|
|
257
|
+
title: Guide
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
# Guide content
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Route Output Modes
|
|
264
|
+
|
|
265
|
+
Both modes are owned by `@ubean/vue`:
|
|
266
|
+
|
|
267
|
+
| Mode | Output | Consumers |
|
|
268
|
+
| --------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
269
|
+
| **Virtual** (default) | `virtual:ubean-vue-routes` in-memory module + `typed-router.d.ts` | Lean SPAs; framework's virtual mode |
|
|
270
|
+
| **Physical files** | `routes.ts` + `imports.ts` + `typed-router.d.ts` written to disk | File mode — editable `meta`, `import.meta.glob`-free imports |
|
|
271
|
+
|
|
272
|
+
The framework wires them via `routing.mode` in `ubean.config.ts` (`'virtual'` | `'file'` | `'both'`, see `@ubean/builder`); lean SPAs use the `/vite` plugin directly and may call the `/generator` subpath manually.
|
|
273
|
+
|
|
274
|
+
## Vite Plugin API
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { ubeanVueVite } from '@ubean/vue/vite';
|
|
278
|
+
|
|
279
|
+
ubeanVueVite({
|
|
280
|
+
pagesDir: 'src/pages', // string | string[] (relative to project root or absolute)
|
|
281
|
+
layoutsDir: 'src/layouts', // string | string[]
|
|
282
|
+
extensions: undefined, // default ['vue', 'tsx', 'jsx']; markdown exts appended
|
|
283
|
+
ignore: [], // extra glob ignore patterns
|
|
284
|
+
generateTypes: true, // write typed-router.d.ts
|
|
285
|
+
dtsDir: '.ubean', // dts output dir (relative to root or absolute)
|
|
286
|
+
markdown: false, // true | 'md' | 'mdx'
|
|
287
|
+
head: false // enable page-level head extraction
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The plugin also exposes standalone helpers: `scanPages`, `scanClientPages`, `extractSlotAndIntercept`, `generatePagesModuleSource`, `generateTypedRouter`, `stripDefinePageCalls`, `filePathToRoute`, `parseMatchers`, `stripRouteGroups`, `generateRouteName`, `generateLayoutName`, `extractDefinePage`, `extractDefinePageFromCode`, `extractCallObject`, `normalizePageHead`.
|
|
292
|
+
|
|
293
|
+
## Physical Route File Generator
|
|
294
|
+
|
|
295
|
+
The `/generator` subpath generates editable route files on disk (`@ubean/vue/generator`):
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { generateRouteFiles } from '@ubean/vue/generator';
|
|
299
|
+
|
|
300
|
+
const result = await generateRouteFiles(
|
|
301
|
+
{ pages, layouts }, // GeneratorScanInput — subset of ScanPagesResult
|
|
302
|
+
{
|
|
303
|
+
cwd: process.cwd(),
|
|
304
|
+
srcDir: 'src', // base for the default @/ import paths
|
|
305
|
+
outDir: 'src/router/_generated',
|
|
306
|
+
// dtsPath: '…', // default: <outDir>/typed-router.d.ts
|
|
307
|
+
// generateRoutes / generateImports / generateDts: true,
|
|
308
|
+
routeLazy: true, // or (page) => boolean
|
|
309
|
+
layoutLazy: true,
|
|
310
|
+
getRouteMeta: page => ({ ... }), // extra meta (definePage meta wins)
|
|
311
|
+
getImportPath: page => `@/pages/${page.name}.vue`,
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
// → { routesPath, importsPath, dtsPath, routeCount, layoutCount }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Produces 3 files under `outDir`:
|
|
318
|
+
|
|
319
|
+
- `routes.ts` — flat `RouteRecord[]` (`name` / `path` / `component`(key into `views`) / `layout` / `meta` / `cache` / `requiresAuth`; reuse routes point `component` at their target)
|
|
320
|
+
- `imports.ts` — `views` / `layouts` lazy-loader maps + categorized key types (`RouteKey` / `RouteFileKey` / `RouteReuseKey` / `LayoutKey`)
|
|
321
|
+
- `typed-router.d.ts` — `@ubean/scan` module augmentation (`RouteKey` / `RoutePathMap` / `RouteLayoutKey` / `ReuseRouteKey`) + `vue-router/auto-routes` `RouteNamedMap` (typed `useRoute<Name>(name)` params inference)
|
|
322
|
+
|
|
323
|
+
The `RouteFileGenerator` class is also exported for incremental use (`new RouteFileGenerator(options).generate(scan)`).
|
|
324
|
+
|
|
325
|
+
> The dts still augments the `@ubean/scan` module — that is the framework's established type contract (framework code imports `RouteKey` etc. from `@ubean/scan`); the generator's physical location does not change the artifact. Lean SPAs use the virtual-mode `typed-router.d.ts` from the `/vite` plugin instead.
|
|
326
|
+
|
|
327
|
+
## Virtual Module Surface
|
|
328
|
+
|
|
329
|
+
`virtual:ubean-vue-routes` exports:
|
|
330
|
+
|
|
331
|
+
| Export | Description |
|
|
332
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
333
|
+
| `routes` | Vue Router `RouteRecord[]` (with `meta.cache` / `transition` / `matchers` / `head` etc.) |
|
|
334
|
+
| `pages` / `layouts` | `{ [name]: () => Promise<Component> }` lazy loaders |
|
|
335
|
+
| `pageNames` / `layoutNames` | Name arrays (include `NotFound` when present) |
|
|
336
|
+
| `defaultLayout` | Default layout name or `null` |
|
|
337
|
+
| `resolvePageComponent(name)` / `resolveLayoutComponent(name)` | Component resolvers (reuse routes resolve their target) |
|
|
338
|
+
| `loadingComponent` / `errorComponent` | Special-page components or `null` |
|
|
339
|
+
| `resolveLoadingComponent()` / `resolveErrorComponent()` | Lazy resolvers |
|
|
340
|
+
| `hasNotFoundPage()` / `hasErrorPage()` | Presence checks |
|
|
341
|
+
|
|
342
|
+
## Vue Plugin
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
app.use(ubeanVue, { routes });
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Registers `Link` / `PageView` / `SlotView` as global components and seeds the keep-alive include list from `meta.cache: true` declarations (`initCachedViewsFromRoutes`). This is the **only** wiring entry of the package (main subpath — distinct from the same-named Vite plugin in the `/vite` subpath).
|
|
349
|
+
|
|
350
|
+
## Components
|
|
351
|
+
|
|
352
|
+
### `<PageView>`
|
|
353
|
+
|
|
354
|
+
The page outlet. Wraps the matched component in `<Transition>` + `<KeepAlive>` (+ `<Suspense>` / `<ErrorBoundary>` when loading/error components are provided via injection keys).
|
|
355
|
+
|
|
356
|
+
Props:
|
|
357
|
+
|
|
358
|
+
- `transition: string | boolean` — transition name, `false` disables. Priority: prop > `route.meta.transition` > global `usePageTransition()`.
|
|
359
|
+
- `reloadKey: string | number` — reactive key forcing remount; defaults to `route.fullPath` + per-page reload count.
|
|
360
|
+
- `mode: 'default' | 'out-in' | 'in-out'` — defaults to `'default'` (cross-fade). `'out-in'` is degraded to `'default'` with a dev warning due to a vue@3.5.x KeepAlive upstream bug.
|
|
361
|
+
|
|
362
|
+
On SSR (via `SSR_KEY`), KeepAlive/Transition/Suspense/ErrorBoundary are skipped.
|
|
363
|
+
|
|
364
|
+
### `<Link>`
|
|
365
|
+
|
|
366
|
+
Internal links render via `RouterLink`; external links (`http*`, `//`, `#`) render as native `<a target="_blank" rel="noopener noreferrer">`.
|
|
367
|
+
|
|
368
|
+
Props: `to` (string or location object), `href`, `replace`, `activeClass`, `exactActiveClass`, `noActiveClass`. Path localization is opt-in via `LOCALIZE_PATH_KEY` (the framework runtime provides it; lean SPAs render paths verbatim).
|
|
369
|
+
|
|
370
|
+
### `<SlotView>`
|
|
371
|
+
|
|
372
|
+
`<SlotView name="modal" />` renders the matched component for a parallel-route slot (nothing when no slot matches).
|
|
373
|
+
|
|
374
|
+
### `<LayoutChainRenderer>`
|
|
375
|
+
|
|
376
|
+
Recursively renders a chain of nested layout components (framework factories build the chain; lean SPAs can use it directly via render functions).
|
|
377
|
+
|
|
378
|
+
### `<ErrorBoundary>`
|
|
379
|
+
|
|
380
|
+
Catches render/async/setup errors from descendants and renders the configured error component; resets on route change.
|
|
381
|
+
|
|
382
|
+
## Composables
|
|
383
|
+
|
|
384
|
+
- **`usePage<T>()`** — lean page-data access: returns the `PAGE_KEY` injected data (`props` / `component` / `errors`) as-is, or a shared empty object when nothing was provided. Route state (`url` / `params` / `query` / `meta`) is NOT included — use vue-router's `useRoute()` directly. The framework runtime (`@ubean/client`) layers its own route-aware `usePage` on top for the full PageObject protocol.
|
|
385
|
+
- **`useRouter()`** — not shipped: import vue-router's `useRouter()` directly so `push` / `replace` keep their `RouteNamedMap` typed overloads. (`@ubean/client` re-exports it purely for auto-import convenience.)
|
|
386
|
+
- **`useCacheViews()`** — see [Page Cache](#page-cache-keep-alive).
|
|
387
|
+
- **`usePageTransition()`** — `{ name, set, clear, enabled }` global transition name.
|
|
388
|
+
- **`useReloadSignal()`** — `{ counter, reloading, reload(routeName?, duration?) }`.
|
|
389
|
+
- **`useViewTransition()`** — `{ enabled, supports, options }` for native View Transitions.
|
|
390
|
+
|
|
391
|
+
## Page Cache (keep-alive)
|
|
392
|
+
|
|
393
|
+
Declarative:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
definePage({ cache: true }); // or route meta: { cache: true }
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Imperative:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import {
|
|
403
|
+
enablePageCache, // enablePageCache('DashboardIndex')
|
|
404
|
+
disablePageCache, // remove from include list (prunes the instance)
|
|
405
|
+
invalidatePageCache, // clear a page's (or all) cached instance(s)
|
|
406
|
+
isPageCached,
|
|
407
|
+
excludePageCache, // temporarily exclude (forces prune on next render)
|
|
408
|
+
includePageCache,
|
|
409
|
+
useCacheViews, // reactive store: cachedViews / excludedViews / enable / disable / ...
|
|
410
|
+
resetRouteCache
|
|
411
|
+
} from '@ubean/vue';
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Caching is keyed by the **route name** (`route.meta.pageName`), which the kernel injects as the component `name` at render time so `<keep-alive :include>` matches `<script setup>` SFCs.
|
|
415
|
+
|
|
416
|
+
## Page Transitions & Reload
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { setPageTransition, useReloadSignal } from '@ubean/vue';
|
|
420
|
+
|
|
421
|
+
setPageTransition('fade-slide'); // global default; '' / 'none' disables
|
|
422
|
+
// per-page: definePage({ transition: 'fade' }) or route meta
|
|
423
|
+
|
|
424
|
+
const { reload } = useReloadSignal();
|
|
425
|
+
await reload('DashboardIndex'); // blank → prune stale cache → fresh remount
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`reloadPage(name)` only bumps **that page's** key — other pages' keep-alive entries survive an unrelated reload.
|
|
429
|
+
|
|
430
|
+
## View Transitions Helpers
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import { supportsViewTransitions, withViewTransition, useViewTransitionState, getNavigationType } from '@ubean/vue';
|
|
434
|
+
|
|
435
|
+
if (supportsViewTransitions()) {
|
|
436
|
+
await withViewTransition(
|
|
437
|
+
async () => {
|
|
438
|
+
/* mutate DOM/state */
|
|
439
|
+
},
|
|
440
|
+
{ types: ['slide-left'] }
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Route Matchers API
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import { defineMatcher, createMatcherGuard } from '@ubean/vue';
|
|
449
|
+
|
|
450
|
+
// src/matchers/numeric.ts
|
|
451
|
+
export default defineMatcher('numeric', value => /^\d+$/.test(value));
|
|
452
|
+
|
|
453
|
+
// router setup — matcher failure redirects to NotFound
|
|
454
|
+
router.beforeEach(createMatcherGuard(/* { notFoundRouteName?, onReject? } */));
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Registry helpers: `getMatcher(name)`, `hasMatcher(name)`, `listMatcherNames()`, `clearMatchers()` (tests only), `validateParams(matchers, params)`.
|
|
458
|
+
|
|
459
|
+
## Page-Level Head (opt-in)
|
|
460
|
+
|
|
461
|
+
Enable extraction with `head: true` on the plugin, then:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
import { createPageHead, setupPageHeadGuard } from '@ubean/vue';
|
|
465
|
+
|
|
466
|
+
const head = await createPageHead(); // lazily loads @unhead/vue (optional peer)
|
|
467
|
+
setupPageHeadGuard(router, head); // pushes route.meta.head after each navigation
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Already have a unhead instance? Skip `createPageHead()` and pass the instance directly — `PageHeadClient` is a structural type (`{ push }`), so no third-party dependency enters the core.
|
|
471
|
+
|
|
472
|
+
## Route Pure Functions
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { resolveRoute, isActiveRoute } from '@ubean/vue';
|
|
476
|
+
|
|
477
|
+
resolveRoute({ name: 'UsersId', params: { id: 7 }, query: { tab: 'info' } }, routeMap);
|
|
478
|
+
// → '/users/7?tab=info'
|
|
479
|
+
isActiveRoute('/users/7', '/users', false); // → true (prefix match)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Type Safety
|
|
483
|
+
|
|
484
|
+
- The package augments `vue-router`'s `RouteMeta` with `cache` / `pageName` / `layout` / `requiresAuth` / `transition` / `head` / `matchers` — projects get full type-safety without their own `declare module` block.
|
|
485
|
+
- With `generateTypes: true` (default), the `/vite` plugin writes `ubean-vue-routes.d.ts` + `typed-router.d.ts` into `dtsDir` (default `.ubean`, same convention as the framework's auto-imports dts); the `/generator` subpath writes its own dts for file mode (see above).
|
|
486
|
+
|
|
487
|
+
## Architecture Notes
|
|
488
|
+
|
|
489
|
+
- **Plugin-first:** `ubeanVue` (main subpath, Vue plugin for `app.use`) and `ubeanVueVite` (`/vite` subpath, Vite plugin factory) are the two wiring entries — distinctly named, no more overloading. The kernel ships no app factory and no router factory — you create both with stock `vue` / `vue-router` APIs.
|
|
490
|
+
- **Runtime boundary:** the main entry (`@ubean/vue`) must never statically import `node:*`, `@ubean/*`, `@unhead/vue`, `vite`, or `tinyglobby` (enforced by tests). Build-time-only code lives behind the `/vite` and `/generator` subpaths.
|
|
491
|
+
- **Lazy optional peers:** `@ubean/markdown` (markdown pages) and `@unhead/vue` (head) are optional peers, dynamically imported only when their features are enabled.
|
|
492
|
+
- **Global singletons** (cache store, transition name, reload counters) live on `globalThis` so duplicated module instances (dep re-optimization, HMR `?t=` variants) share one state — prevents SSR hydration mismatches and KeepAlive cache misses.
|
|
493
|
+
- **Ownership:** `@ubean/scan` (aggregator) depends on `@ubean/vue` — never the reverse. The aggregator re-exports page-routing types, functions, and the generator.
|