@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.
@@ -0,0 +1,500 @@
1
+ # @ubean/vue
2
+
3
+ **[English](./README.md) | 中文**
4
+
5
+ ubean 的精简 Vue 客户端内核 & 页面路由唯一所有者。
6
+
7
+ `@ubean/vue` 拥有 ubean 生态中**页面路由**的一切:文件路由扫描、虚拟模块生成、实体路由文件生成、页面缓存(keep-alive)、页面过渡、重载信号与 `definePage` 宏。以 Vue **插件**形式接入(无应用工厂、无 router 工厂),运行时**仅依赖 `vue` + `vue-router`**。
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'; // 由 @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 }); // 注册 Link/PageView/SlotView + 播种页面缓存
19
+ app.mount('#app');
20
+ ```
21
+
22
+ ## 目录
23
+
24
+ - [@ubean/vue](#ubeanvue)
25
+ - [目录](#目录)
26
+ - [包定位](#包定位)
27
+ - [安装](#安装)
28
+ - [快速开始](#快速开始)
29
+ - [文件路由](#文件路由)
30
+ - [约定](#约定)
31
+ - [definePage 宏](#definepage-宏)
32
+ - [reuse 路由](#reuse-路由)
33
+ - [特殊页](#特殊页)
34
+ - [并行路由与拦截路由](#并行路由与拦截路由)
35
+ - [动态参数 matcher](#动态参数-matcher)
36
+ - [Markdown 页面(opt-in)](#markdown-页面opt-in)
37
+ - [路由输出模式](#路由输出模式)
38
+ - [Vite 插件 API](#vite-插件-api)
39
+ - [实体路由文件生成器](#实体路由文件生成器)
40
+ - [虚拟模块导出面](#虚拟模块导出面)
41
+ - [Vue 插件](#vue-插件)
42
+ - [组件](#组件)
43
+ - [`<PageView>`](#pageview)
44
+ - [`<Link>`](#link)
45
+ - [`<SlotView>`](#slotview)
46
+ - [`<LayoutChainRenderer>`](#layoutchainrenderer)
47
+ - [`<ErrorBoundary>`](#errorboundary)
48
+ - [组合式函数](#组合式函数)
49
+ - [页面缓存(keep-alive)](#页面缓存keep-alive)
50
+ - [页面过渡与重载](#页面过渡与重载)
51
+ - [View Transitions 工具](#view-transitions-工具)
52
+ - [路由 Matcher API](#路由-matcher-api)
53
+ - [页面级 head(opt-in)](#页面级-headopt-in)
54
+ - [路由纯函数](#路由纯函数)
55
+ - [类型安全](#类型安全)
56
+ - [架构说明](#架构说明)
57
+
58
+ ## 包定位
59
+
60
+ `@ubean/vue` 是**精简客户端内核**,也是 ubean 中**页面路由逻辑的唯一所有者**:
61
+
62
+ | 层 | 职责 |
63
+ | ----------------------------- | -------------------------------------------------------------------------------------------------- |
64
+ | `@ubean/vue`(本包) | 页面路由:扫描、虚拟模块、实体路由文件生成器、matcher、head 守卫、页面缓存、过渡、重载 —— 纯客户端 |
65
+ | `@ubean/scan`(聚合层) | 依赖 `@ubean/vue`;叠加服务端路由(rou3)、API 路由扫描、SSR 页面处理 |
66
+ | `@ubean/client`(框架运行时) | 应用工厂(`createUbeanClientApp` / `createUbeanSSRApp`)、unhead/SEO、i18n、islands、数据层 |
67
+
68
+ **包含:**
69
+
70
+ - `ubeanVue` Vue 插件(唯一接线入口 —— 插件优先,无工厂)
71
+ - `<PageView>` / `<Link>` / `<SlotView>` / `<LayoutChainRenderer>` / `<ErrorBoundary>`
72
+ - 页面缓存(keep-alive)store:命令式 + 声明式 API
73
+ - 页面过渡与重载信号(按页重载 + 缓存新鲜重建)
74
+ - View Transitions 工具(特性检测 + 包装器)
75
+ - `definePage` 客户端宏(构建期提取,运行时 no-op)
76
+ - 动态路由 matcher(`[param=matcher]` 语法 + 注册表 + 守卫)
77
+ - 页面级 head(`setupPageHeadGuard` / 懒加载 `createPageHead`)
78
+ - 文件路由 Vite 插件(`/vite` 子路径):多目录扫描、reuse 路由、特殊页、并行/拦截路由、markdown 与 head 按需开启
79
+ - 实体路由文件生成器(`/generator` 子路径):`routes.ts` + `imports.ts` + `typed-router.d.ts`(file 模式)
80
+
81
+ **不包含**(框架运行时 `@ubean/client` 的职责):
82
+
83
+ - 应用工厂(`createUbeanClientApp` / `createUbeanSSRApp`)
84
+ - unhead / `<Head>` / SEO(仅 opt-in 的 head _守卫_ 在这里)
85
+ - i18n(`Link` 原样渲染路径,除非通过 `LOCALIZE_PATH_KEY` 注入 localizer)
86
+ - Islands(`v-client` 指令)、SSR 状态助手、自动引入预设
87
+
88
+ **运行时依赖白名单(主入口):仅 `vue` + `vue-router`。** 无 `@ubean/*`、无 `node:*`、无 `@unhead/vue` 静态导入。
89
+
90
+ ## 安装
91
+
92
+ ```bash
93
+ pnpm add @ubean/vue vue vue-router
94
+ # 构建期(文件路由):
95
+ pnpm add -D vite @ubean/vue # /vite 与 /generator 子路径是本包的一部分
96
+ # 可选 peer(懒加载,仅启用时加载):
97
+ pnpm add @ubean/markdown # markdown 页面
98
+ pnpm add @unhead/vue # 页面级 head(SPA)
99
+ ```
100
+
101
+ ## 快速开始
102
+
103
+ **1. Vite 配置** —— 插件扫描页面并生成 `virtual:ubean-vue-routes`:
104
+
105
+ ```ts
106
+ // vite.config.ts
107
+ import vue from '@vitejs/plugin-vue';
108
+ import { ubeanVueVite } from '@ubean/vue/vite';
109
+
110
+ export default {
111
+ plugins: [vue(), ubeanVueVite()]
112
+ };
113
+ ```
114
+
115
+ **2. 引导** —— 自行创建 router,然后安装插件:
116
+
117
+ ```ts
118
+ // src/main.ts
119
+ import { createApp } from 'vue';
120
+ import { createRouter, createWebHistory } from 'vue-router';
121
+ import { ubeanVue } from '@ubean/vue';
122
+ import { routes } from 'virtual:ubean-vue-routes';
123
+ import App from './App.vue';
124
+
125
+ const router = createRouter({ history: createWebHistory(), routes });
126
+ const app = createApp(App);
127
+ app.use(router);
128
+ app.use(ubeanVue, { routes });
129
+ app.mount('#app');
130
+ ```
131
+
132
+ **3. 应用外壳** —— `<PageView />` 是页面出口(keep-alive / 过渡 / 重载都在这里生效):
133
+
134
+ ```vue
135
+ <!-- src/App.vue -->
136
+ <template>
137
+ <nav>
138
+ <Link to="/">首页</Link>
139
+ <Link to="/about">关于</Link>
140
+ </nav>
141
+ <main><PageView /></main>
142
+ </template>
143
+ ```
144
+
145
+ 完整可运行示例见 [`examples/client-only-spa`](../../examples/client-only-spa)。
146
+
147
+ ## 文件路由
148
+
149
+ ### 约定
150
+
151
+ 默认页面扩展名为 `vue` / `tsx` / `jsx`(可用 `extensions` 选项覆盖)。`.ts` **不是**页面扩展名 —— 脚本页面请使用 `.tsx` / `.jsx` 或 `.vue`。布局使用 `vue` / `ts`。
152
+
153
+ ```
154
+ src/pages/
155
+ ├── index.vue → / Home
156
+ ├── about.vue → /about About
157
+ ├── users/index.vue → /users Users
158
+ ├── users/[id].vue → /users/:id UsersId
159
+ ├── users/[id=numeric].vue → /users/:id + matcher (见 matcher)
160
+ ├── blog/[...slug].vue → /blog/**:slug BlogAllSlug(catch-all)
161
+ ├── docs/[[page]].vue → /docs/:page? DocsPageOptional
162
+ ├── (marketing)/about.vue → /about About(组名剥离)
163
+ ├── settings.tsx → /settings Settings(JSX 页面)
164
+ ├── 404.vue → catch-all + NotFound (特殊页)
165
+ └── guide.md → /guide Guide(markdown,opt-in)
166
+ ```
167
+
168
+ 支持多个 `pagesDir` / `layoutsDir`(先到先得去重,可分层叠加目录)。
169
+
170
+ - 路由名由路径按 PascalCase 派生(`users/[id]` → `UsersId`)。
171
+ - `definePage({ name, path })` 可覆盖两者。
172
+ - 路由组 `(name)` 不参与 URL 与路由名。
173
+
174
+ ### definePage 宏
175
+
176
+ 在 `<script setup>` 顶层(或 `.tsx`/`.jsx`/`.reuse.ts` 顶层)声明页面元信息:
177
+
178
+ ```vue
179
+ <script setup lang="ts">
180
+ import { definePage } from '@ubean/vue';
181
+
182
+ definePage({
183
+ name: 'UserProfile', // 覆盖文件派生的路由名
184
+ path: '/u/:id', // 覆盖文件派生的路径
185
+ cache: true, // keep-alive 页面缓存
186
+ transition: 'fade', // 页面级过渡名
187
+ layout: 'admin', // 布局标记:名、数组(外→内)或 false
188
+ requiresAuth: true, // 鉴权标记 → route.meta.requiresAuth
189
+ head: { title: 'Profile' }, // 页面 head(需插件 `head: true`)
190
+ meta: { custom: 'any' } // 任意 RouteMeta 扩展
191
+ });
192
+ </script>
193
+ ```
194
+
195
+ 双重语义:
196
+
197
+ - **构建期(推荐)**:`/vite` 插件提取参数对象,合并进生成的路由表,并把调用从产物中剥除(零运行时开销)。
198
+ - **运行时兜底**:无插件环境下函数是 no-op —— 不报错、无副作用,页面按文件派生的路径/名称注册;`cache` 可在运行时用 `enablePageCache(name)` 补齐。
199
+
200
+ > 需要按路由声明守卫时用 `meta: { middleware: [...] }` 并在自己的导航守卫中消费。
201
+
202
+ ### reuse 路由
203
+
204
+ `.reuse.ts` / `.reuse.js` 是**纯元数据文件**(只含 `definePage({ reuse })`),注册一条复用另一页面组件的新路由:
205
+
206
+ ```ts
207
+ // src/pages/about2.reuse.ts
208
+ definePage({ reuse: 'About' });
209
+ // → /about2 路由渲染 About 页面组件
210
+ ```
211
+
212
+ - `.reuse.vue` **不属于**该约定 —— 带 `.vue` 扩展名的是真实组件,按普通页面处理(路由 `/xxx.reuse`)。
213
+ - 未显式声明 `cache` 的 reuse 路由**继承**目标页的缓存设置;声明 `cache: false` 可关闭。
214
+ - 目标必须是常规(非 reuse)页面。
215
+
216
+ ### 特殊页
217
+
218
+ 页面目录根级文件接入框架插槽:
219
+
220
+ | 文件 | 角色 |
221
+ | ------------------------------ | ---------------------------------------------------------- |
222
+ | `404.vue`(或 `.tsx` / `.md`) | Vue Router catch-all `/:pathMatch(.*)*`,名为 `NotFound` |
223
+ | `loading.vue` | 异步页面加载时的 `<Suspense>` fallback(经 `LOADING_KEY`) |
224
+ | `error.vue` | `<ErrorBoundary>` 的错误组件(经 `ERROR_KEY`) |
225
+
226
+ 仅根级文件是特殊页 —— `users/404.vue` 仍是 `/users/404` 的常规路由。
227
+
228
+ ### 并行路由与拦截路由
229
+
230
+ - **并行路由**:`@slotName/` 目录注册命名视图,用 `<SlotView name="slotName" />` 渲染。
231
+ - **拦截路由**:`(..)target/`、`(.)target/`、`(...)target/`(上一级 / 同级 / 根级),Next.js 风格。
232
+
233
+ ```
234
+ src/pages/dashboard/@analytics/index.vue → 并行插槽 "analytics"
235
+ src/pages/photos/(..)photo/[id].vue → 从 /photos/* 拦截 /photo/:id
236
+ ```
237
+
238
+ ### 动态参数 matcher
239
+
240
+ `[param=matcherName]` 文件语法为动态段附加校验 matcher:
241
+
242
+ ```
243
+ src/pages/users/[id=numeric].vue → /users/:id + meta.matchers = { id: 'numeric' }
244
+ ```
245
+
246
+ 用 `defineMatcher` 注册 matcher(见[路由 Matcher API](#路由-matcher-api)),用 `createMatcherGuard` 强制执行;校验失败重定向到 `NotFound` 路由。
247
+
248
+ ### Markdown 页面(opt-in)
249
+
250
+ 用 `markdown: true`(`md` + `mdx`)、`'md'` 或 `'mdx'` 开启。需要 `@ubean/markdown`(optional peer,仅开启时懒加载):
251
+
252
+ ```ts
253
+ ubeanVueVite({ markdown: true });
254
+ ```
255
+
256
+ frontmatter 映射到页面元信息(`name` / `path` / `layout` / `cache` / `head`):
257
+
258
+ ```md
259
+ ---
260
+ name: Guide
261
+ layout: docs
262
+ cache: true
263
+ head:
264
+ title: Guide
265
+ ---
266
+
267
+ # Guide 内容
268
+ ```
269
+
270
+ ## 路由输出模式
271
+
272
+ 两种模式均由 `@ubean/vue` 所有:
273
+
274
+ | 模式 | 产物 | 消费方 |
275
+ | ---------------- | --------------------------------------------------------- | ------------------------------------------------- |
276
+ | **虚拟**(默认) | 内存模块 `virtual:ubean-vue-routes` + `typed-router.d.ts` | 精简 SPA;框架 virtual 模式 |
277
+ | **实体文件** | 落盘的 `routes.ts` + `imports.ts` + `typed-router.d.ts` | file 模式 —— `meta` 可编辑、免 `import.meta.glob` |
278
+
279
+ 框架经 `ubean.config.ts` 的 `routing.mode`(`'virtual'` | `'file'` | `'both'`,见 `@ubean/builder`)接线;精简 SPA 直接用 `/vite` 插件,也可手动调用 `/generator` 子路径。
280
+
281
+ ## Vite 插件 API
282
+
283
+ ```ts
284
+ import { ubeanVueVite } from '@ubean/vue/vite';
285
+
286
+ ubeanVueVite({
287
+ pagesDir: 'src/pages', // string | string[](相对项目根或绝对路径)
288
+ layoutsDir: 'src/layouts', // string | string[]
289
+ extensions: undefined, // 默认 ['vue', 'tsx', 'jsx'];markdown 扩展自动追加
290
+ ignore: [], // 额外 glob ignore
291
+ generateTypes: true, // 生成 typed-router.d.ts
292
+ dtsDir: '.ubean', // 类型声明产物目录(相对项目根或绝对路径)
293
+ markdown: false, // true | 'md' | 'mdx'
294
+ head: false // 开启页面级 head 提取
295
+ });
296
+ ```
297
+
298
+ 插件还导出独立工具函数:`scanPages`、`scanClientPages`、`extractSlotAndIntercept`、`generatePagesModuleSource`、`generateTypedRouter`、`stripDefinePageCalls`、`filePathToRoute`、`parseMatchers`、`stripRouteGroups`、`generateRouteName`、`generateLayoutName`、`extractDefinePage`、`extractDefinePageFromCode`、`extractCallObject`、`normalizePageHead`。
299
+
300
+ ## 实体路由文件生成器
301
+
302
+ `/generator` 子路径在磁盘上生成可编辑的路由文件(`@ubean/vue/generator`):
303
+
304
+ ```ts
305
+ import { generateRouteFiles } from '@ubean/vue/generator';
306
+
307
+ const result = await generateRouteFiles(
308
+ { pages, layouts }, // GeneratorScanInput —— ScanPagesResult 的结构子集
309
+ {
310
+ cwd: process.cwd(),
311
+ srcDir: 'src', // 默认 @/ 导入路径的基准
312
+ outDir: 'src/router/_generated',
313
+ // dtsPath: '…', // 默认: <outDir>/typed-router.d.ts
314
+ // generateRoutes / generateImports / generateDts: true,
315
+ routeLazy: true, // 或 (page) => boolean
316
+ layoutLazy: true,
317
+ getRouteMeta: page => ({ ... }), // 额外 meta(definePage meta 优先)
318
+ getImportPath: page => `@/pages/${page.name}.vue`,
319
+ }
320
+ );
321
+ // → { routesPath, importsPath, dtsPath, routeCount, layoutCount }
322
+ ```
323
+
324
+ 在 `outDir` 下产出 3 个文件:
325
+
326
+ - `routes.ts` —— 扁平 `RouteRecord[]`(`name` / `path` / `component`(`views` 的 key)/ `layout` / `meta` / `cache` / `requiresAuth`;reuse 路由的 `component` 指向目标页)
327
+ - `imports.ts` —— `views` / `layouts` 懒加载映射 + 分类 key 类型(`RouteKey` / `RouteFileKey` / `RouteReuseKey` / `LayoutKey`)
328
+ - `typed-router.d.ts` —— `@ubean/scan` 模块增强(`RouteKey` / `RoutePathMap` / `RouteLayoutKey` / `ReuseRouteKey`)+ `vue-router/auto-routes` 的 `RouteNamedMap`(类型化 `useRoute<Name>(name)` 参数推断)
329
+
330
+ 同时导出 `RouteFileGenerator` 类供增量使用(`new RouteFileGenerator(options).generate(scan)`)。
331
+
332
+ > dts 仍增强 `@ubean/scan` 模块 —— 这是框架层的既有类型契约(框架代码从 `@ubean/scan` 导入 `RouteKey` 等);生成器的物理位置不改变产物。精简 SPA 使用 `/vite` 插件的虚拟模式 `typed-router.d.ts`,不经过本生成器。
333
+
334
+ ## 虚拟模块导出面
335
+
336
+ `virtual:ubean-vue-routes` 导出:
337
+
338
+ | 导出 | 说明 |
339
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
340
+ | `routes` | Vue Router `RouteRecord[]`(含 `meta.cache` / `transition` / `matchers` / `head` 等) |
341
+ | `pages` / `layouts` | `{ [name]: () => Promise<Component> }` 懒加载器 |
342
+ | `pageNames` / `layoutNames` | 名称数组(存在 404 时包含 `NotFound`) |
343
+ | `defaultLayout` | 默认布局名或 `null` |
344
+ | `resolvePageComponent(name)` / `resolveLayoutComponent(name)` | 组件解析器(reuse 路由解析目标页) |
345
+ | `loadingComponent` / `errorComponent` | 特殊页组件或 `null` |
346
+ | `resolveLoadingComponent()` / `resolveErrorComponent()` | 懒加载解析器 |
347
+ | `hasNotFoundPage()` / `hasErrorPage()` | 存在性检查 |
348
+
349
+ ## Vue 插件
350
+
351
+ ```ts
352
+ app.use(ubeanVue, { routes });
353
+ ```
354
+
355
+ 注册 `Link` / `PageView` / `SlotView` 全局组件,并从 `meta.cache: true` 声明播种 keep-alive include 列表(`initCachedViewsFromRoutes`)。这是本包主入口**唯一**的接线入口(与 `/vite` 子路径的同名 Vite 插件是两个不同入口)。
356
+
357
+ ## 组件
358
+
359
+ ### `<PageView>`
360
+
361
+ 页面出口。将匹配的组件包裹在 `<Transition>` + `<KeepAlive>` 中(通过注入键提供 loading/error 组件时再加 `<Suspense>` / `<ErrorBoundary>`)。
362
+
363
+ Props:
364
+
365
+ - `transition: string | boolean` —— 过渡名,`false` 禁用。优先级:prop > `route.meta.transition` > 全局 `usePageTransition()`。
366
+ - `reloadKey: string | number` —— 强制重挂载的响应式 key;默认 `route.fullPath` + 按页重载计数。
367
+ - `mode: 'default' | 'out-in' | 'in-out'` —— 默认 `'default'`(交叉过渡)。因 vue@3.5.x KeepAlive 上游 bug,`'out-in'` 降级为 `'default'` 并给出 dev 警告。
368
+
369
+ SSR 场景(经 `SSR_KEY`)跳过 KeepAlive/Transition/Suspense/ErrorBoundary。
370
+
371
+ ### `<Link>`
372
+
373
+ 内部链接经 `RouterLink` 渲染;外部链接(`http*`、`//`、`#`)渲染为原生 `<a target="_blank" rel="noopener noreferrer">`。
374
+
375
+ Props:`to`(字符串或位置对象)、`href`、`replace`、`activeClass`、`exactActiveClass`、`noActiveClass`。路径本地化经 `LOCALIZE_PATH_KEY` opt-in(框架运行时提供;精简 SPA 原样渲染路径)。
376
+
377
+ ### `<SlotView>`
378
+
379
+ `<SlotView name="modal" />` 渲染并行路由插槽匹配到的组件(无匹配时不渲染)。
380
+
381
+ ### `<LayoutChainRenderer>`
382
+
383
+ 递归渲染嵌套布局链(框架工厂构建链;精简 SPA 可在渲染函数中直接使用)。
384
+
385
+ ### `<ErrorBoundary>`
386
+
387
+ 捕获后代组件的渲染/异步/setup 错误并渲染配置的错误组件;路由变更时自动重置。
388
+
389
+ ## 组合式函数
390
+
391
+ - **`usePage<T>()`** —— 精简版页面数据访问:原样返回 `PAGE_KEY` 注入的数据(`props` / `component` / `errors`),未注入时返回共享空对象。**不含**路由态(`url` / `params` / `query` / `meta`)—— 请直接用 vue-router 的 `useRoute()`。框架运行时(`@ubean/client`)在其上叠加自己的路由感知 `usePage` 提供完整 PageObject 协议。
392
+ - **`useRouter()`** —— 请直接导入 vue-router 的 `useRouter()`,`push` / `replace` 保留 `RouteNamedMap` 类型化重载(`@ubean/client` 仅作纯 re-export)。
393
+ - **`useCacheViews()`** —— 见[页面缓存](#页面缓存keep-alive)。
394
+ - **`usePageTransition()`** —— `{ name, set, clear, enabled }` 全局过渡名。
395
+ - **`useReloadSignal()`** —— `{ counter, reloading, reload(routeName?, duration?) }`。
396
+ - **`useViewTransition()`** —— `{ enabled, supports, options }` 原生 View Transitions。
397
+
398
+ ## 页面缓存(keep-alive)
399
+
400
+ 声明式:
401
+
402
+ ```ts
403
+ definePage({ cache: true }); // 或路由 meta: { cache: true }
404
+ ```
405
+
406
+ 命令式:
407
+
408
+ ```ts
409
+ import {
410
+ enablePageCache, // enablePageCache('DashboardIndex')
411
+ disablePageCache, // 从 include 列表移除(剪除实例)
412
+ invalidatePageCache, // 清除某页(或全部)缓存实例
413
+ isPageCached,
414
+ excludePageCache, // 临时排除(下次渲染强制剪除)
415
+ includePageCache,
416
+ useCacheViews, // 响应式 store:cachedViews / excludedViews / enable / disable / ...
417
+ resetRouteCache
418
+ } from '@ubean/vue';
419
+ ```
420
+
421
+ 缓存以**路由名**(`route.meta.pageName`)为 key —— 内核在渲染时把它注入为组件 `name`,使 `<keep-alive :include>` 能匹配 `<script setup>` SFC。
422
+
423
+ ## 页面过渡与重载
424
+
425
+ ```ts
426
+ import { setPageTransition, useReloadSignal } from '@ubean/vue';
427
+
428
+ setPageTransition('fade-slide'); // 全局默认;'' / 'none' 禁用
429
+ // 页面级:definePage({ transition: 'fade' }) 或路由 meta
430
+
431
+ const { reload } = useReloadSignal();
432
+ await reload('DashboardIndex'); // 空白 → 剪除旧缓存 → 新鲜重挂载
433
+ ```
434
+
435
+ `reloadPage(name)` 只变更**该页**的 key —— 无关页面重载不会使其他页面的 keep-alive 条目失效。
436
+
437
+ ## View Transitions 工具
438
+
439
+ ```ts
440
+ import { supportsViewTransitions, withViewTransition, useViewTransitionState, getNavigationType } from '@ubean/vue';
441
+
442
+ if (supportsViewTransitions()) {
443
+ await withViewTransition(
444
+ async () => {
445
+ /* 变更 DOM/状态 */
446
+ },
447
+ { types: ['slide-left'] }
448
+ );
449
+ }
450
+ ```
451
+
452
+ ## 路由 Matcher API
453
+
454
+ ```ts
455
+ import { defineMatcher, createMatcherGuard } from '@ubean/vue';
456
+
457
+ // src/matchers/numeric.ts
458
+ export default defineMatcher('numeric', value => /^\d+$/.test(value));
459
+
460
+ // router 设置 —— matcher 失败重定向到 NotFound
461
+ router.beforeEach(createMatcherGuard(/* { notFoundRouteName?, onReject? } */));
462
+ ```
463
+
464
+ 注册表工具:`getMatcher(name)`、`hasMatcher(name)`、`listMatcherNames()`、`clearMatchers()`(仅测试)、`validateParams(matchers, params)`。
465
+
466
+ ## 页面级 head(opt-in)
467
+
468
+ 插件开启 `head: true` 提取后:
469
+
470
+ ```ts
471
+ import { createPageHead, setupPageHeadGuard } from '@ubean/vue';
472
+
473
+ const head = await createPageHead(); // 懒加载 @unhead/vue(optional peer)
474
+ setupPageHeadGuard(router, head); // 每次导航后 push route.meta.head
475
+ ```
476
+
477
+ 已有 unhead 实例?跳过 `createPageHead()` 直接传实例 —— `PageHeadClient` 是结构化类型(`{ push }`),核心不引入任何第三方依赖。
478
+
479
+ ## 路由纯函数
480
+
481
+ ```ts
482
+ import { resolveRoute, isActiveRoute } from '@ubean/vue';
483
+
484
+ resolveRoute({ name: 'UsersId', params: { id: 7 }, query: { tab: 'info' } }, routeMap);
485
+ // → '/users/7?tab=info'
486
+ isActiveRoute('/users/7', '/users', false); // → true(前缀匹配)
487
+ ```
488
+
489
+ ## 类型安全
490
+
491
+ - 本包增强了 `vue-router` 的 `RouteMeta`(`cache` / `pageName` / `layout` / `requiresAuth` / `transition` / `head` / `matchers`)—— 项目无需自写 `declare module` 即可获得完整类型安全。
492
+ - `generateTypes: true`(默认)时,`/vite` 插件把 `ubean-vue-routes.d.ts` + `typed-router.d.ts` 生成到 `dtsDir`(默认 `.ubean`,与框架 auto-imports dts 同一约定);`/generator` 子路径为 file 模式生成自己的 dts(见上文)。
493
+
494
+ ## 架构说明
495
+
496
+ - **插件优先**:`ubeanVue`(主入口,Vue 插件,`app.use`)与 `ubeanVueVite`(`/vite` 子路径,Vite 插件工厂)是两个独立命名的接线入口。内核不产出应用工厂与 router 工厂 —— 两者都用原生 `vue` / `vue-router` API 自行创建。
497
+ - **运行时边界**:主入口(`@ubean/vue`)禁止静态导入 `node:*`、`@ubean/*`、`@unhead/vue`、`vite`、`tinyglobby`(由测试强制)。构建期代码全部在 `/vite` 与 `/generator` 子路径之后。
498
+ - **懒加载可选 peer**:`@ubean/markdown`(markdown 页面)与 `@unhead/vue`(head)为 optional peer,仅在对应功能开启时动态导入。
499
+ - **全局单例**(缓存 store、过渡名、重载计数)挂在 `globalThis` 上,使重复模块实例(依赖重新优化、HMR `?t=` 变体)共享同一状态 —— 避免 SSR 水合不匹配与 KeepAlive 缓存失效。
500
+ - **所有权方向**:`@ubean/scan`(聚合层)依赖 `@ubean/vue` —— 永不反向。聚合层 re-export 页面路由类型、函数与生成器。
@@ -0,0 +1,125 @@
1
+ import { a as ScannedLayout, o as ScannedPage } from "../types-VHF1RJu2.js";
2
+ //#region src/generator/index.d.ts
3
+ /**
4
+ * 实体路由文件生成器(页面路由实体文件模式的唯一所有者,原
5
+ * `@ubean/scan/generator`,已下沉到页面路由所有者 `@ubean/vue`)。
6
+ *
7
+ * 聚合层 `@ubean/scan` 的 `./generator` 子路径 re-export 本文件保持
8
+ * 向后兼容;框架层(`packages/builder`)经 `routing.mode = 'file' | 'both'`
9
+ * 触发,精简 SPA 也可直接使用。
10
+ *
11
+ * 输入只依赖页面扫描结果的 `pages` / `layouts` 子集(`ScanPagesResult`
12
+ * 结构性满足),与聚合扫描(`ScanResult`)解耦。
13
+ */
14
+ /**
15
+ * 生成器输入 —— 页面扫描结果的结构子集。
16
+ *
17
+ * `ScanPagesResult`(`@ubean/vue/vite` 的 `scanPages` 返回值)与聚合层的
18
+ * `ScanResult`(`@ubean/scan` 的 `scanProject`)都结构性满足本接口。
19
+ */
20
+ interface GeneratorScanInput {
21
+ pages: ScannedPage[];
22
+ layouts: ScannedLayout[];
23
+ }
24
+ /**
25
+ * Options for the physical route file generator.
26
+ *
27
+ * The generator produces 3 files under `outDir`:
28
+ * - `routes.ts` — flat route records (`RouteRecord[]`) with name/path/component/layout/meta
29
+ * - `imports.ts` — lazy `views` and `layouts` records (for `import.meta.glob`-free consumption)
30
+ * - `typed-router.d.ts` — type definitions:
31
+ * - `@ubean/scan` 模块增强:`RouteKey`、`RoutePath`、`RouteLayoutKey`、`ReuseRouteKey`、`RoutePathMap`
32
+ * - `vue-router/auto-routes` 模块增强:`RouteNamedMap`(name → `RouteRecordInfo<Name, Path, ParamsRaw, Params>`)
33
+ * - `vue-router` 模块增强:`TypesConfig.RouteNamedMap` 引用上面的 `RouteNamedMap`
34
+ *
35
+ * `RouteNamedMap` 让 vue-router 的 `useRoute<Name>(name)` 能根据路由名推断
36
+ * `route.params` 的类型(动态路由 `:id` / `:id?` 自动类型化),无需用户手动断言。
37
+ * 这与 elegant-router 的 `typed-router.d.ts` 行为对齐。
38
+ *
39
+ * 注:`typed-router.d.ts` 仍增强 `@ubean/scan` 模块 —— 这是框架层的既有
40
+ * 类型契约(框架代码从 `@ubean/scan` 导入 `RouteKey` 等);生成器物理位置
41
+ * 在 `@ubean/vue` 不改变产物契约。精简 SPA 用虚拟模式的 `typed-router.d.ts`
42
+ * (`@ubean/vue/vite` 的 `generateTypedRouter`),不经过本生成器。
43
+ */
44
+ interface GeneratorOptions {
45
+ /** Project root directory (absolute). */
46
+ cwd: string;
47
+ /**
48
+ * Source directory (absolute, defaults to `<cwd>/src`).
49
+ *
50
+ * Used by the default `getImportPath` / `getLayoutImportPath` to compute
51
+ * import specifiers relative to `srcDir` (the conventional target of the
52
+ * `@/` path alias). When the alias differs, supply custom `getImportPath` /
53
+ * `getLayoutImportPath` callbacks.
54
+ */
55
+ srcDir?: string;
56
+ /** Output directory for `routes.ts` / `imports.ts` (absolute or relative to `cwd`). */
57
+ outDir: string;
58
+ /** Path for `typed-router.d.ts` (absolute or relative to `cwd`). Default: `<outDir>/typed-router.d.ts`. */
59
+ dtsPath?: string;
60
+ /** Whether to generate `routes.ts`. Default: `true`. */
61
+ generateRoutes?: boolean;
62
+ /** Whether to generate `imports.ts`. Default: `true`. */
63
+ generateImports?: boolean;
64
+ /** Whether to generate `typed-router.d.ts`. Default: `true`. */
65
+ generateDts?: boolean;
66
+ /** Use lazy `() => import(...)` for views. Default: `true`. */
67
+ routeLazy?: boolean | ((page: ScannedPage) => boolean);
68
+ /** Use lazy `() => import(...)` for layouts. Default: `true`. */
69
+ layoutLazy?: boolean | ((layout: ScannedLayout) => boolean);
70
+ /**
71
+ * Compute extra `meta` for a route. The returned object is merged
72
+ * on top of the page's existing `pageMeta.meta` (from `definePage` / frontmatter).
73
+ * Return `null` to skip. Existing keys are NOT overwritten.
74
+ */
75
+ getRouteMeta?: (page: ScannedPage) => Record<string, unknown> | null;
76
+ /**
77
+ * Compute the import specifier for a page file. Receives the absolute
78
+ * `fullPath` of the page; should return a module specifier usable from
79
+ * `imports.ts`. Defaults to a `srcDir`-relative path prefixed with `@/`
80
+ * (e.g. `@/pages/about.vue`), matching the common `@/` → `src/` alias.
81
+ */
82
+ getImportPath?: (page: ScannedPage) => string;
83
+ /** Same as `getImportPath` but for layouts. */
84
+ getLayoutImportPath?: (layout: ScannedLayout) => string;
85
+ /**
86
+ * Header comment prepended to every generated file. Default: a
87
+ * `// @generated by @ubean/vue/generator — do not edit` notice.
88
+ */
89
+ headerComment?: string;
90
+ }
91
+ interface GeneratorResult {
92
+ routesPath?: string;
93
+ importsPath?: string;
94
+ dtsPath?: string;
95
+ routeCount: number;
96
+ layoutCount: number;
97
+ }
98
+ declare const DEFAULT_HEADER_COMMENT: string;
99
+ declare function generateRouteFiles(scan: GeneratorScanInput, options: GeneratorOptions): Promise<GeneratorResult>;
100
+ declare class RouteFileGenerator {
101
+ private readonly opts;
102
+ constructor(options: GeneratorOptions);
103
+ generate(scan: GeneratorScanInput): Promise<GeneratorResult>;
104
+ renderRoutesFile(scan: GeneratorScanInput): string;
105
+ private renderRouteRecord;
106
+ private computeMeta;
107
+ renderImportsFile(scan: GeneratorScanInput): string;
108
+ private renderLayoutImport;
109
+ private renderViewImport;
110
+ renderDtsFile(scan: GeneratorScanInput): string;
111
+ /**
112
+ * 渲染单条路由的 `RouteRecordInfo<Name, Path, ParamsRaw, Params>`。
113
+ *
114
+ * 参数类型映射:
115
+ * - 必需参数(`/users/:id`) → `{ id: ParamValue<true> }` / `{ id: ParamValue<false> }`
116
+ * - 可选参数(`/users/:id?`) → `{ id?: ParamValueZeroOrOne<true> }` / `{ id?: ParamValueZeroOrOne<false> }`
117
+ * - 无参数(`/about`) → `Record<never, never>`
118
+ *
119
+ * `ParamValue<true>` 表示原始输入类型(必需为 string),`ParamValue<false>` 表示
120
+ * 解析后类型(已编码/解码,可能为 `string | undefined`)。这是 vue-router 的约定。
121
+ */
122
+ private renderRouteRecordInfo;
123
+ }
124
+ //#endregion
125
+ export { DEFAULT_HEADER_COMMENT, GeneratorOptions, GeneratorResult, GeneratorScanInput, RouteFileGenerator, generateRouteFiles };