@route-forge/vue

Route Forge 的 Vue 3 集成:插件 + composable(useForge/useForgeApi/useForgeRoute)+ 组件(ForgeRoute/ForgeLink)/ Vue 3 integration for Laravel named routes: plugin + composables (useForge / useForgeApi / useForgeRoute) + components (ForgeRoute / ForgeLink)
870 1
Install
npm install @route-forge/vue
Latest Version:3.1.0
Vue:3
License:MIT
Last Updated:Sep 17, 2026
Links: GitHub  ·  npm
Maintainer: xyj2156

@route-forge/vue

English | 中文

The Vue 3 integration for Route Forge: a plugin (createRouteForgePlugin), three composables (useForge / useForgeApi / useForgeRoute), and two components (ForgeRoute / ForgeLink) for calling APIs by route name and generating reactive links inside components, with loading and error states managed for you.

All core capabilities (tiered lazy loading, isolated cache, interceptors, request cancellation, type safety) come from @route-forge/core; this package only adds Vue reactivity: levelLoaded is a Ref<boolean>, pending / error are Refs, and URL generation returns a ComputedRef<string>.

Installation

pnpm add @route-forge/vue @route-forge/core

Requires Vue 3.3+ (Vue 2 is not supported).

Quick start

// main.ts
import { createApp } from 'vue'
import { createRouteForgePlugin } from '@route-forge/vue'
import App from './App.vue'

const plugin = createRouteForgePlugin({
  endpoint: '/_forge/routes',
})

// plugin.interceptors is exposed synchronously — register one or more interceptors
// here without awaiting ready() (they only affect later api() calls).
plugin.interceptors.request.use((config) => { /* … */ return config })
plugin.interceptors.response.use((resp) => resp.data, (err) => Promise.reject(err))

const app = createApp(App)
app.use(plugin)

// Option A (recommended): mount the app after ready() (summary + eager levels fully loaded);
// sync methods like route()/hasRoute() are then safe immediately
plugin.ready()
  .then(() => app.mount('#app'))
  .catch((err) => {
    // Always handle this: the summary endpoint may be unreachable
    // (network error / non-2xx / timeout) — avoid a silent blank page
    console.error('[route-forge] init failed', err)
  })

// Option B (equally supported): mount immediately and gate inside the component tree —
// <ForgeReady> holds route-dependent content until ready() resolves, same determinism:
//   app.mount('#app')
// and wrap route content in App.vue (see ForgeReady below)

Plugin options are exactly createRouteForge(options) (endpoint / summary / levels / eager / adapter / cache / interceptors / timeout / baseURL); full options table in the core README. options itself is optional — when the summary is embedded in the page via @forgeSummary (window.__ROUTE_FORGE__), install with a bare createRouteForgePlugin().

Reuse mode: already hold a forge instance outside Vue (SSR entry, singleton module)? Pass it directly — createRouteForgePlugin(instance) reuses it and ignores options, symmetric with React's <RouteForgeProvider forge?>.

Option B — direct mount with a deterministic first frame: wrap route-dependent content in the ForgeReady gate component — it renders the fallback slot until ready, then the default slot; equivalent determinism to plugin.ready().then(mount):

<ForgeReady>
  <template #fallback><Splash /></template>
  <RouterView />
</ForgeReady>

useForge — the core composable

Without level it returns the full RouteForge instance; with level it delegates to forge.use(level, prefix?) (triggering the level load) and returns a VueBoundForge:

import { useForge } from '@route-forge/vue'

// Unbound — full RouteForge instance
const forge = useForge()
forge.api('admin', 'users.show', { user: 1 })     // call with level + route name
forge.ready().then(f => f.use('admin'))           // bind a level once ready

// Bound to a level — callable directly, load triggered automatically
const users = useForge('admin')
users.level                                       // → 'admin'
users.levelLoaded                                 // Ref<boolean>, flips to true once loaded
users('users.show', { user: 1 })                  // callable (= users.api() shorthand)
users.api('users.show', { user: 1 })              // same thing
users.route('users.show', { user: 1 })            // build a URL
users.url('users.show', { user: 1 })              // semantic alias of route()
await users.onLevelLoaded()                       // wait until the level is loaded
users.useRoutePrefix('users')                     // returns a NEW BoundForge with the new prefix

// Bound level + prefix — route names joined automatically (ambiguity resolved smartly)
const userApi = useForge('admin', 'users')
userApi.prefix                                    // → 'users'
userApi('show', { user: 1 })                      // → forge.api('admin', 'users.show', ...)
userApi.route('show', { user: 1 })                // → forge.route('admin', 'users.show', ...)

// Generic methods: in bound form they act on the bound level (no arguments)
users.load()                                      // load the bound level
users.isLoaded()                                  // check the bound level's cache
users.invalidate()                                // invalidate the bound level's cache
// Global methods: isLoading() / onLoadingChange() / hasRoute(name) / getRoutes() / getLevels()

Note: the unbound full instance may throw the guard error (RF_FE_010) from route() / hasRoute() before auto-discovery completes. Prefer await forge.ready(), or use useForgeRoute for links.

Level binding is static: level (and prefix) are fixed at call time and cannot be switched later — create another useForge call for another level (the overhead is acceptable).

useForgeApi — event-style calls with loading/error state

For imperative scenarios like click handlers: it never throws — errors are written to the error state and returned as { data: undefined, error }:

import { useForgeApi } from '@route-forge/vue'

// Three binding forms (same level semantics as useForge)
const api = useForgeApi()                        // unbound: call(level, name, params)
const admin = useForgeApi('admin')               // bound: call(name, params)
const users = useForgeApi('admin', 'users')      // bound + prefix: call(suffix, params)

const { data, error } = await admin.call('users.show', { user: 1 })
  • pending: Ref<boolean> — reference-counted; concurrent calls keep it true until all settle
  • error: Ref<unknown> — the latest failure (reset to null on success)

useForgeRoute — reactive links in templates

Returns a ComputedRef<string> (auto-unwrapped in templates, no .value needed). It handles the loading state internally: returns '' until the level loads (the render never crashes), then recomputes automatically when the level loads or dependencies change — you never need to look at levelLoaded:

<template>
  <!-- 1. Static URL -->
  <a :href="https://github.com/route-forge/route-forge/blob/HEAD/login">Login</a>

  <!-- 2. Reactive params: URL recomputes when userId changes -->
  <a :href="https://github.com/route-forge/route-forge/blob/HEAD/profile">User home</a>

  <!-- 3. Reactive route name: recomputes when currentName changes -->
  <NuxtLink :to="dynamic">Dynamic entry</NuxtLink>
</template>

<script setup>
import { ref } from 'vue'
import { useForgeRoute } from '@route-forge/vue'

const userId = ref(1)
const currentName = ref('dashboard.show')

// Static level + static route name
const login = useForgeRoute('public', 'login.show')

// params as a getter → tracked reactively; URL follows userId
const profile = useForgeRoute('admin', 'users.show', () => ({ user: userId.value }))

// name as a getter → the route name itself can switch reactively
const dynamic = useForgeRoute('admin', () => currentName.value)
</script>

Contract details:

  • level is a static string binding: fixed once at setup and cannot be switched later — create another useForgeRoute call for another level (same contract as useForge); the name / params getters stay reactive. A non-string level throws TypeError at runtime
  • Render-time errors (unknown route, missing required param…) degrade to '' so rendering never breaks, while a styled console.warn prints the full error (with stack) — visible during development, harmless in production

Components — ForgeRoute / ForgeLink

Both components wrap useForgeRoute, so templates don't repeat the "empty string first, update later" handling. While the level is not loaded (or route resolution fails), the loading slot renders (nothing by default); once loaded, the link renders.

ForgeLink — the convenient one: renders <a :href> directly with slot content as the link text:

<script setup>
import { ForgeLink } from '@route-forge/vue'
</script>

<template>
  <!-- Renders <a href="https://github.com/route-forge/route-forge/blob/HEAD/login">Login</a> once the level is loaded -->
  <ForgeLink level="public" name="login.show">Login</ForgeLink>

  <!-- Reactive params (object or getter form) + attrs passthrough (class / target / …) -->
  <ForgeLink level="admin" name="users.show" :params="{ user: userId }" class="btn">
    View user
  </ForgeLink>

  <!-- Custom placeholder while loading -->
  <ForgeLink level="admin" name="users.show" :params="() => ({ user: userId })">
    View user
    <template #loading>Preparing link…</template>
  </ForgeLink>
</template>

Router integration is zero-dependency and automatic: if vue-router is installed (app.use(router)), ForgeLink renders <RouterLink :to="href"> for in-app navigation; otherwise it renders a native <a>. Nothing to configure. (A RouterLink registered only locally in a parent component is not detected — the global registration from app.use(router) is what's probed.) For those cases — or any custom link component — pass it via the as prop; the injected component receives both href and to props (mirroring @route-forge/react).

ForgeRoute — the flexible one: exposes { href, loaded } through a scoped slot for full control:

<script setup>
import { ForgeRoute } from '@route-forge/vue'
</script>

<template>
  <ForgeRoute level="admin" name="users.show" :params="() => ({ user: userId })">
    <template #default="{ href }">
      <a :href="https://github.com/route-forge/route-forge/blob/HEAD/href">View user</a>
    </template>
    <template #loading>Preparing link…</template>
  </ForgeRoute>
</template>

Shared contract (both components):

  • level is a static string binding (same contract as useForgeRoute; a non-string throws TypeError at runtime); name / params accept both plain values and getter functions and stay reactive
  • loaded = href !== '' — inside the default slot it is always true (the slot only renders once loaded); it exists for symmetry with the React render-prop API
  • Console behavior: while the level is not loaded each instance console.warns once (a normal transient state — no spam); route resolution failures log console.error every time (rendering still never breaks)
  • SSR: the components simply render the loading slot (or nothing) until the level cache is populated — preload the level on the server, or let the link appear after client hydration

About the $forge global property (removed in v3.0.0)

Since v3.0.0 the plugin no longer injects app.config.globalProperties.$forge: it exposed only route() (no api / url / hasRoute) and threw on unready route data, breaking the render — a fragile, misuse-prone entry. Generate links in templates with useForgeRoute / ForgeLink / ForgeRoute; for imperative code use useForge() (unbound calls work directly: forge(level, name, params?)).

Smart parameter resolution

api() supports the same smart resolution as core: flattened path parameters, with query / body / headers as fixed keys; when a path parameter name collides with a fixed key, string|number values are detected as path parameters, and the explicit params key always wins:

// Conflict resolution: route /search/{query} — string `query` → path parameter
users.api('search.show', { query: 'keyword' })

// Explicit params: need BOTH a path param and a query string
users.api('search.show', {
  params: { query: 'keyword' },
  query: { page: 1 },
})

Full rules and the timeout override: core README.

Type safety (optional but recommended)

Once ForgeRouteMap is defined (codegen or module augmentation), level, route names, and params are inferred in useForge / useForgeApi / useForgeRoute:

npx route-forge-codegen --endpoint http://localhost/_forge/routes --out src/types/forge-routes.d.ts
// Typo'd route name / param name → compile error; correct call → autocompletion
const users = useForge('admin', 'users')
await users('show', { user: 1 })      // ✅ params checked at compile time

See the core README "Type safety" section.

Differences vs core / react

Capability @route-forge/core @route-forge/vue @route-forge/react
levelLoaded Promise<void> Ref<boolean> boolean
useForgeApi pending / error — (use LoadingTracker) Ref<boolean> / Ref<unknown> boolean / unknown
URL generation returns string (sync; throws when unready) ComputedRef<string> ('' until ready) string ('' until ready)
useForgeRoute params — getter function plain object (content-compared deps)
Binding signature forge.use(level, prefix?) useForge(level?, prefix?) useForge(level?, prefix?)

FAQ

$forge.route() doesn't exist in a template? $forge was removed in v3.0.0. Use useForgeRoute instead (handles the loading state and degrades to ''), or the composable form useForge('admin').route(...).

useForge() says "must be used inside an app with createRouteForgePlugin() installed"? A composable ran before the plugin was installed, or in a component tree without it; confirm app.use(plugin) happened first.

Need to re-fetch the route table after navigating? Call forge.invalidate() (or users.invalidate() in bound form); the next api() / load() refetches.

Documentation

License

MIT

Related Packages

@bowlinedev/vue

Vue composables and TanStack Vue Query option builders for Bowline clients

992 1
@laravel/stream-vue

Laravel streaming hooks for Vue

196,737 105
@laravilt/support

Foundation package providing base components, utilities, and contracts for all L...

892 0
laravel-elixir-vue-2

'Laravel Elixir Vue 2.0 Integration'

28,012 107
vue-tilt.js

Add tilt-effect to your elements

22,943 52

Version History

Version Released Vue License
3.1.0 ^3.3.0 MIT
3.0.0 ^3.3.0 MIT
2.2.1 ^3.3.0 MIT
2.2.0 ^3.3.0 MIT
2.1.0 ^3.3.0 MIT