@ubean/vue
| Install | |
|---|---|
npm install @ubean/vue |
|
| Latest Version: | 0.7.0 |
| Vue: | 3 |
| Last Updated: | Oct 22, 2026 |
| Links: | npm |
@ubean/vue
English | 中文
Lean Vue client kernel & page-routing owner for ubean.
@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.
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { ubeanVue } from '@ubean/vue';
import { routes } from 'virtual:ubean-vue-routes'; // generated by @ubean/vue/vite
const router = createRouter({ history: createWebHistory(), routes });
const app = createApp(App);
app.use(router);
app.use(ubeanVue, { routes }); // registers Link/PageView/SlotView/ClientOnly + seeds page cache
app.mount('#app');
Table of Contents
- Package Positioning
- Install
- Quick Start
- File-Based Routing
- Route Output Modes
- Vite Plugin API
- Physical Route File Generator
- Virtual Module Surface
- Vue Plugin
- Components
- Composables
- Page Cache (keep-alive)
- Page Transitions & Reload
- View Transitions Helpers
- Route Matchers API
- Page-Level Head (opt-in)
- Route Pure Functions
- Type Safety
- Architecture Notes
Package Positioning
@ubean/vue is the lean client kernel and the single owner of page-routing logic in ubean:
| Layer | Responsibility |
|---|---|
@ubean/vue (this package) |
Page routing: scanning, virtual modules, physical route file generator, matchers, head guard, page cache, transitions, reload — pure client-side |
@ubean/scan (aggregator) |
Depends on @ubean/vue; layers server routing (rou3), API route scanning, SSR page handling on top |
@ubean/client (framework runtime) |
App factories (createUbeanClientApp / createUbeanSSRApp), unhead/SEO, i18n, islands, data layer |
Included:
ubeanVueVue plugin (the only wiring entry — plugin-first, no factory)<PageView>/<Link>/<SlotView>/<LayoutChainRenderer>/<ErrorBoundary>/<ClientOnly>- Page cache (keep-alive) store with imperative + declarative APIs
- Page transitions & reload signal (per-page remount with fresh cache)
- View Transitions helpers (feature detection + wrapper)
definePageclient macro (compile-time extracted, runtime no-op)- Dynamic route matchers (
[param=matcher]syntax + registry + guard) - Page-level head (
setupPageHeadGuard/ lazycreatePageHead) - File-routing Vite plugin (
/vitesubpath): multi-dir scan, reuse routes, special pages, parallel routes (@slotName/), markdown & head opt-in - Physical route file generator (
/generatorsubpath):routes.ts+imports.ts+typed-router.d.ts(file mode)
NOT included (framework runtime @ubean/client concerns):
- App factories (
createUbeanClientApp/createUbeanSSRApp) - unhead /
<Head>/ SEO (only the opt-in head guard lives here) - i18n (
Linkrenders paths verbatim unless a localizer is provided viaLOCALIZE_PATH_KEY) - Islands (
v-clientdirective), SSR state helpers, auto-import presets
Runtime dependency whitelist (main entry): vue + vue-router only. No @ubean/*, no node:*, no @unhead/vue static imports.
Install
pnpm add @ubean/vue vue vue-router
# build-time (file routing):
pnpm add -D vite @ubean/vue # the /vite & /generator subpaths are part of this package
# optional peers (loaded lazily, only when enabled):
pnpm add @ubean/markdown # markdown pages
pnpm add @unhead/vue # page-level head (SPA)
Quick Start
1. Vite config — the plugin scans your pages and generates virtual:ubean-vue-routes:
// vite.config.ts
import vue from '@vitejs/plugin-vue';
import { ubeanVueVite } from '@ubean/vue/vite';
export default {
plugins: [vue(), ubeanVueVite()]
};
2. Bootstrap — create the router yourself, then install the plugin:
// src/main.ts
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { ubeanVue } from '@ubean/vue';
import { routes } from 'virtual:ubean-vue-routes';
import App from './App.vue';
const router = createRouter({ history: createWebHistory(), routes });
const app = createApp(App);
app.use(router);
app.use(ubeanVue, { routes });
app.mount('#app');
3. App shell — <PageView /> is the page outlet (keep-alive / transition / reload all take effect there):
<!-- src/App.vue -->
<template>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<main><PageView /></main>
</template>
A complete runnable example lives at examples/client-only-spa.
File-Based Routing
Conventions
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.
src/pages/
├── index.vue → / Home
├── about.vue → /about About
├── users/index.vue → /users Users
├── users/[id].vue → /users/:id UsersId
├── users/[id=numeric].vue → /users/:id + matcher (see matchers)
├── blog/[...slug].vue → /blog/**:slug BlogAllSlug (catch-all)
├── docs/[[page]].vue → /docs/:page? DocsPageOptional
├── (marketing)/about.vue → /about About (group stripped)
├── settings.tsx → /settings Settings (JSX page)
├── 404.vue → catch-all + NotFound (special page)
└── guide.md → /guide Guide (markdown, opt-in)
Multiple pagesDir / layoutsDir are supported (first-seen-wins dedup, so folders can be layered).
- Route names are derived from paths in PascalCase (
users/[id]→UsersId). definePage({ name, path })can override both.- Route groups
(name)never contribute to the URL or the route name.
definePage Macro
Declare page metadata at the top of <script setup> (or top-level in .tsx/.jsx/.reuse.ts/.meta.ts):
<script setup lang="ts">
import { definePage } from '@ubean/vue';
definePage({
name: 'UserProfile', // override the file-derived route name
path: '/u/:id', // override the file-derived path
cache: true, // keep-alive page cache
transition: 'fade', // page-level transition name
layout: 'admin', // layout marker: name, array (outer→inner), or false
requiresAuth: true, // auth marker → route.meta.requiresAuth
routable: false, // keep in the page inventory, but register no route
head: { title: 'Profile' }, // page head (requires plugin `head: true`)
meta: { custom: 'any' } // arbitrary RouteMeta extensions
});
</script>
Dual semantics:
- Build time (recommended): the
/viteplugin extracts the argument object, merges it into the generated route table, and strips the call from the output (zero runtime cost). - Runtime fallback: without the plugin the function is a no-op — no errors, no side effects; pages register with file-derived paths/names.
cachecan be restored at runtime viaenablePageCache(name).
Use
meta: { middleware: [...] }and consume it in your own navigation guard.
Reuse Routes
.reuse.ts / .reuse.js are pure metadata files (they only contain definePage({ reuse })) that register a new route reusing another page's component:
// src/pages/about2.reuse.ts
definePage({ reuse: 'About' });
// → route /about2 renders the About page component
.reuse.vueis NOT part of the convention — a.vuefile is a real component and is treated as a regular page (route/xxx.reuse).- Reuse routes that don't declare
cacheexplicitly inherit the target's cache setting; declarecache: falseto opt out. - The target must be a regular (non-reuse) page.
Meta-Only Pages (routable: false)
.meta.ts / .meta.js are pure metadata files — like .reuse.*, they contain only definePage(...) and no component. They exist for entries that belong in the page inventory but must not become routes: menu directories, external-link entries, iframe containers, or any host-specific concept.
// src/pages/plugin/index.meta.ts — the filename already says it: no `routable` needed
definePage({ meta: { menuType: 'directory', title: 'Plugins' } });
- The
.meta.*filename isroutable: false— the field is normalized tofalseat scan time and may be omitted. Declaringroutable: trueon a.meta.*file throws at scan time (a metadata file with no component would produce a broken route). .meta.*files may declarereuse— withreusethe entry is a reuse page (borrows the target component for a new URL); the.metaand.reusesuffixes are semantically equivalent..meta.vueis NOT the convention — it is a real component and is treated as a regular page.
routable: false is a general flag, not tied to .meta.*. Any page — component or metadata — may set it:
<!-- src/pages/hidden.vue — component stays on disk, but no route is registered -->
<script setup lang="ts">
definePage({ routable: false });
</script>
What routable: false removes vs. keeps:
| Removed | Kept |
|---|---|
client route record (routes) |
component loader (_pageLoaders) — .reuse.* routes still borrow it |
PageName / RouteNamedMap entries |
entry in pages (with routable / pageKind / pageMeta) |
| server route registration (Hono, SSR, SSG), prerender candidates | definePage metadata, for hosts that build menus from the inventory |
.meta.* entries get no loader at all (their module has no default export). The pages export carries routable, pageKind and the raw pageMeta, so an application can read its own menu semantics — e.g. treat meta.menuType === 'directory' as a nested menu group and meta.href as an external link.
Special Pages
Root-level files in a pages directory are wired to framework slots:
| File | Role |
|---|---|
404.vue (or .tsx / .md) |
Vue Router catch-all /:pathMatch(.*)* named NotFound |
loading.vue |
<Suspense> fallback while async pages load (via LOADING_KEY) |
error.vue |
<ErrorBoundary> error component (via ERROR_KEY) |
Only root-level files are special — users/404.vue remains a regular route at /users/404.
Parallel Routes
- Parallel routes:
@slotName/directories register named views; render them with<SlotView name="slotName" />.
src/pages/dashboard/@analytics/index.vue → parallel slot "analytics"
Intercepting routes are deliberately not implemented (
(.)target//(..)target//(...)target/). The scanner errors on those marker segments instead of silently degrading them into literal paths like/feed/(.)photo/:id— see ADR-0010 and the rationale insrc/scan-pages.ts. Build shareable dialogs with parallel routes plus your own navigation guard.
Dynamic Param Matchers
[param=matcherName] file syntax attaches a validation matcher to a dynamic segment:
src/pages/users/[id=numeric].vue → /users/:id + meta.matchers = { id: 'numeric' }
Register matchers with defineMatcher (see Route Matchers API) and enforce them with createMatcherGuard. Failed validation redirects to the NotFound route.
Markdown Pages (opt-in)
Enable with markdown: true (md + mdx), 'md', or 'mdx'. Requires @ubean/markdown (optional peer, loaded lazily only when enabled):
ubeanVueVite({ markdown: true });
Frontmatter maps onto page metadata (name / path / layout / cache / head):
---
name: Guide
layout: docs
cache: true
head:
title: Guide
---
# Guide content
Route Output Modes
Both modes are owned by @ubean/vue:
| Mode | Output | Consumers |
|---|---|---|
| Virtual (default) | virtual:ubean-vue-routes in-memory module + typed-router.d.ts |
Lean SPAs; framework's virtual mode |
| Physical files | routes.ts + imports.ts + typed-router.d.ts written to disk |
File mode — editable meta, import.meta.glob-free imports |
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.
Vite Plugin API
import { ubeanVueVite } from '@ubean/vue/vite';
ubeanVueVite({
pagesDir: 'src/pages', // string | string[] (relative to project root or absolute)
layoutsDir: 'src/layouts', // string | string[]
extensions: undefined, // default ['vue', 'tsx', 'jsx']; markdown exts appended
ignore: [], // extra glob ignore patterns
generateTypes: true, // write typed-router.d.ts
dtsDir: '.ubean', // dts output dir (relative to root or absolute)
markdown: false, // true | 'md' | 'mdx'
head: false // enable page-level head extraction
});
The plugin also exposes standalone helpers: scanPages, scanClientPages, extractSlotFromPath, generatePagesModuleSource, generateTypedRouter, generateVirtualModuleDts, stripDefinePageCalls, filePathToRoute, parseMatchers, stripRouteGroups, generateRouteName, generateLayoutName, extractDefinePage, extractDefinePageFromCode, extractCallObject, normalizePageHead.
Physical Route File Generator
The /generator subpath generates editable route files on disk (@ubean/vue/generator):
import { generateRouteFiles } from '@ubean/vue/generator';
const result = await generateRouteFiles(
{ pages, layouts }, // GeneratorScanInput — subset of ScanPagesResult
{
cwd: process.cwd(),
srcDir: 'src', // base for the default @/ import paths
outDir: 'src/router/_generated',
// dtsPath: '…', // default: <outDir>/typed-router.d.ts
// generateRoutes / generateImports / generateDts: true,
routeLazy: true, // or (page) => boolean
layoutLazy: true,
getRouteMeta: page => ({ ... }), // extra meta (definePage meta wins)
getImportPath: page => `@/pages/${page.name}.vue`,
}
);
// → { routesPath, importsPath, dtsPath, routeCount, layoutCount }
Produces 3 files under outDir:
routes.ts— flatRouteRecord[](name/path/component(key intoviews) /layout/meta/cache/requiresAuth; reuse routes pointcomponentat their target)imports.ts—views/layoutslazy-loader maps + categorized key types (RouteKey/RouteFileKey/RouteReuseKey/LayoutKey)typed-router.d.ts—@ubean/scanmodule augmentation (RouteKey/RoutePathMap/RouteLayoutKey/ReuseRouteKey) + avue-router/auto-routesRouteNamedMapdeclaration, which is what makesvue-router's ownuseRoute<Name>()infer path params
The RouteFileGenerator class is also exported for incremental use (new RouteFileGenerator(options).generate(scan)).
The dts still augments the
@ubean/scanmodule — that is the framework's established type contract (framework code importsRouteKeyetc. from@ubean/scan); the generator's physical location does not change the artifact. Lean SPAs use the virtual-modetyped-router.d.tsfrom the/viteplugin instead.
Virtual Module Surface
virtual:ubean-vue-routes exports:
| Export | Description |
|---|---|
routes |
Vue Router RouteRecord[] (with meta.cache / transition / matchers / head etc.) |
pages / layouts |
{ [name]: () => Promise<Component> } lazy loaders |
pageNames / layoutNames |
Name arrays (include NotFound when present) |
defaultLayout |
Default layout name or null |
resolvePageComponent(name) / resolveLayoutComponent(name) |
Component resolvers (reuse routes resolve their target) |
loadingComponent / errorComponent |
Special-page components or null |
resolveLoadingComponent() / resolveErrorComponent() |
Lazy resolvers |
hasNotFoundPage() / hasErrorPage() |
Presence checks |
Vue Plugin
app.use(ubeanVue, { routes });
Registers Link / PageView / SlotView / ClientOnly 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).
Components
<PageView>
The page outlet. Wraps the matched component in <Transition> + <KeepAlive> (+ <Suspense> / <ErrorBoundary> when loading/error components are provided via injection keys).
Props:
transition: string | boolean— transition name,falsedisables. Priority: prop >route.meta.transition> globalusePageTransition().reloadKey: string | number— reactive key forcing remount; defaults toroute.fullPath+ per-page reload count.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.
On SSR (via SSR_KEY), KeepAlive/Transition/Suspense/ErrorBoundary are skipped.
<Link>
Internal links render via RouterLink; external links (http*, //, #) render as native <a target="_blank" rel="noopener noreferrer">.
Props: to (string or location object), href, replace, activeClass, exactActiveClass, noActiveClass, optional locale. Path localization is opt-in via LOCALIZE_PATH_KEY (the framework runtime provides it; lean SPAs render paths verbatim).
<SlotView>
<SlotView name="modal" /> renders the matched component for a parallel-route slot (nothing when no slot matches).
<LayoutChainRenderer>
Recursively renders a chain of nested layout components (framework factories build the chain; lean SPAs can use it directly via render functions).
<ErrorBoundary>
Catches render/async/setup errors from descendants and renders the configured error component; resets on route change.
<ClientOnly>
Renders its content ONLY on the client, after hydration. Useful for browser-only markup or template fragments (the islands v-client.* transform only applies to capitalized component tags).
<template>
<!-- string fallback prop -->
<ClientOnly fallback="loading…">
<BrowserChart />
</ClientOnly>
<!-- fallback slot (reserve space to avoid CLS) -->
<ClientOnly>
<template #fallback>
<div class="skeleton" />
</template>
<BrowserMap />
</ClientOnly>
<!-- plain fragments work too -->
<ClientOnly>Window width: {{ width }}px</ClientOnly>
</template>
Hydration safety: the placeholder (fallback / nothing) is rendered during SSR AND during the client's hydration render; the real content is patched in right after mount. There is no typeof window branching, so no hydration mismatch warnings.
When to use what:
| Scenario | Use |
|---|---|
| Template fragments / plain HTML, same-tree rendering, fallback slots | <ClientOnly> |
| Component-level deferred hydration strategies (idle/visible/media) | islands v-client.* |
| Programmatic islands / client-only wrappers | defineIsland / defineClientComponent (@ubean/islands) |
| File-level server/client split | .server.vue / .client.vue |
Notes: default-slot content is absent from the SSR HTML — don't use
<ClientOnly>for SEO-critical content.<ClientOnly>inside an island placeholder's children is wiped when the island hydrates (islands hydrate via a separate app).
Composables
usePage<T>()— lean page-data access: returns thePAGE_KEYinjected 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'suseRoute()directly. The framework runtime (@ubean/client) layers its own route-awareusePageon top for the full PageObject protocol.useRoute()/useRouter()— not shipped: import both fromvue-routerdirectly (itsRouteNamedMapoverloads come from the generatedtyped-router.d.ts);@ubean/vuedeliberately provides nouseRoute/navigateTo/redirectTowrapper.useCacheViews()— see Page Cache.usePageTransition()—{ name, set, clear, enabled }global transition name.useReloadSignal()—{ counter, reloading, reload(routeName?, duration?) }.useViewTransition()—{ enabled, supports, options }for native View Transitions.
Page Cache (keep-alive)
Declarative:
definePage({ cache: true }); // or route meta: { cache: true }
Imperative:
import {
enablePageCache, // enablePageCache('DashboardIndex')
disablePageCache, // remove from include list (prunes the instance)
invalidatePageCache, // clear a page's (or all) cached instance(s)
isPageCached,
excludePageCache, // temporarily exclude (forces prune on next render)
includePageCache,
useCacheViews, // reactive store: cachedViews / excludedViews / enable / disable / ...
resetRouteCache
} from '@ubean/vue';
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.
Page Transitions & Reload
import { setPageTransition, useReloadSignal } from '@ubean/vue';
setPageTransition('fade-slide'); // global default; '' / 'none' disables
// per-page: definePage({ transition: 'fade' }) or route meta
const { reload } = useReloadSignal();
await reload('DashboardIndex'); // blank → prune stale cache → fresh remount
reloadPage(name) only bumps that page's key — other pages' keep-alive entries survive an unrelated reload.
View Transitions Helpers
import { supportsViewTransitions, withViewTransition, useViewTransitionState, getNavigationType } from '@ubean/vue';
if (supportsViewTransitions()) {
await withViewTransition(
async () => {
/* mutate DOM/state */
},
{ types: ['slide-left'] }
);
}
Route Matchers API
import { defineMatcher, createMatcherGuard } from '@ubean/vue';
// src/matchers/numeric.ts
export default defineMatcher('numeric', value => /^\d+$/.test(value));
// router setup — matcher failure redirects to NotFound
router.beforeEach(createMatcherGuard(/* { notFoundRouteName?, onReject? } */));
Registry helpers: getMatcher(name), hasMatcher(name), listMatcherNames(), clearMatchers() (tests only), validateParams(matchers, params).
Page-Level Head (opt-in)
Enable extraction with head: true on the plugin, then:
import { createPageHead, setupPageHeadGuard } from '@ubean/vue';
const head = await createPageHead(); // lazily loads @unhead/vue (optional peer)
setupPageHeadGuard(router, head); // pushes route.meta.head after each navigation
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.
Route Pure Functions
import { resolveRoute, isActiveRoute } from '@ubean/vue';
resolveRoute({ name: 'UsersId', params: { id: 7 }, query: { tab: 'info' } }, routeMap);
// → '/users/7?tab=info'
isActiveRoute('/users/7', '/users', false); // → true (prefix match)
Type Safety
- The package augments
vue-router'sRouteMetawithcache/pageName/layout/requiresAuth/transition/head/matchers— projects get full type-safety without their owndeclare moduleblock. - With
generateTypes: true(default), the/viteplugin writesubean-vue-routes.d.ts+typed-router.d.tsintodtsDir(default.ubean, same convention as the framework's auto-imports dts); the/generatorsubpath writes its own dts for file mode (see above).
Architecture Notes
- Plugin-first:
ubeanVue(main subpath, Vue plugin forapp.use) andubeanVueVite(/vitesubpath, 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 stockvue/vue-routerAPIs. - Runtime boundary: the main entry (
@ubean/vue) must never statically importnode:*,@ubean/*,@unhead/vue,vite, ortinyglobby(enforced by tests). Build-time-only code lives behind the/viteand/generatorsubpaths. - Lazy optional peers:
@ubean/markdown(markdown pages) and@unhead/vue(head) are optional peers, dynamically imported only when their features are enabled. - Global singletons (cache store, transition name, reload counters) live on
globalThisso duplicated module instances (dep re-optimization, HMR?t=variants) share one state — prevents SSR hydration mismatches and KeepAlive cache misses. - Ownership:
@ubean/scan(aggregator) depends on@ubean/vue— never the reverse. The aggregator re-exports page-routing types, functions, and the generator.
Related Packages
Version History
| Version | Released | License |
|---|---|---|
| 0.7.0 | ||
| 0.6.9 | ||
| 0.6.8 | ||
| 0.6.7 | ||
| 0.6.6 | ||
| 0.6.5 | ||
| 0.6.4 | ||
| 0.6.3 | ||
| 0.6.2 | ||
| 0.6.1 | ||
| 0.6.0 | ||
| 0.5.2 | ||
| 0.5.1 | ||
| 0.5.0 | ||
| 0.4.10 | ||
| 0.4.9 | ||
| 0.4.8 | ||
| 0.4.7 | ||
| 0.4.6 | ||
| 0.4.5 | ||
| 0.4.4 | ||
| 0.4.3 | ||
| 0.4.2 | ||
| 0.4.1 | ||
| 0.4.0 | ||
| 0.3.7 | ||
| 0.3.6 | ||
| 0.3.5 | ||
| 0.3.4 | ||
| 0.3.3 |
Showing the latest 30 of 36. See every release on npm
Pre-releases (7)
| Version | Released | License |
|---|---|---|
| 0.6.1-beta.3 | ||
| 0.6.1-beta.2 | ||
| 0.6.0-beta.5 | ||
| 0.6.0-beta.4 | ||
| 0.6.0-beta.3 | ||
| 0.6.0-beta.2 | ||
| 0.6.0-beta.1 |