tanstack-router-sfc
v1.1.0
Published
TanStack Router for Vue with single-file routes: define the route in a <router lang="ts"> block inside the .vue file that renders it.
Maintainers
Readme
tanstack-router-sfc
TanStack Router for Vue where one .vue file is the whole route: the route
definition lives in a <router lang="ts"> custom block next to the component
that renders it.
<template>
<article>{{ post.title }}</article>
</template>
<script setup lang="ts">
// `Route` is in scope here - no import.
const post = Route.useLoaderData()
</script>
<router lang="ts">
import { createFileRoute, notFound } from '@tanstack/vue-router'
import { getPost } from '@/lib/posts'
export default createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await getPost(params.postId)
if (!post) throw notFound()
return post
}
})
</router>This replaces the x.ts + x.component.vue pair. Both conventions work in the
same routes directory, so migration can be gradual.
Setup
// vite.config.ts
import { tanstackRouterSfc } from 'tanstack-router-sfc'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [...tanstackRouterSfc(), vue()]
})// tsconfig.json - typechecking and IDE support for the block
{
"vueCompilerOptions": {
"plugins": ["tanstack-router-sfc/volar"]
}
}tanstackRouterSfc wraps tanstackRouter from @tanstack/router-plugin, so
it takes the same options (routesDirectory, generatedRouteTree,
autoCodeSplitting, codeSplittingOptions, plugins, ...) plus blockType
(default 'router'). target and addExtensions are managed for you.
Peer dependencies: vite, vue, @tanstack/router-plugin,
@tanstack/vue-router.
Rules
- The block must be
<router lang="ts">and mustexport default createFileRoute('<path>')({ ... })- exactly one, with a string-literal path.__root.vueusescreateRootRoute*instead. - A stale path after a rename is fixed in place by the generator, the same
way it fixes
.tsroute files. Routeis available in<script setup>without an import. It is not available in<template>(it is a<script>binding, like any plain<script>value) - assign it to aconstin setup if the template needs it.- An SFC with a
<template>or<script setup>becomes the route'scomponent, loaded lazily. A block-only.vue(redirect or pathless layout) gets no component. - Piece files (
x.component.vue,x.errorComponent.vue,x.lazy.tsx, ...) and plain.tsroute files are untouched. - Every non-piece
.vuefile under the routes directory must have a block. Use therouteFileIgnorePrefix(-) for helper files.
How it works
Route generation, file watching, formatting, code splitting and route HMR are
the official tanstackRouter plugin's job - this composes it with
addExtensions: true, so the generated tree imports ./routes/x.vue and
TypeScript can see the block's default-exported route as Route. On top of that:
- a generator plugin (
afterTransform) requires every single-file route to have a block, keeps itscreateFileRoute('...')id in sync when the file moves, and scaffolds an empty.vueas an SFC; :route-treerewrites the tree's./routes/x.vueimports at transform time tox.vue.tsr-router.ts- the block alone - so the route tree never pulls a component into the entry chunk;:sfcreplaces the<router>block with a<script>shim that imports the default-exported route asRoutefrom that module, preserving the file's line count so template and setup diagnostics stay accurate;:router-blockservesx.vue.tsr-router.ts: the block's TypeScript, padded to the original line numbers, plusRoute.update({ component: lazyRouteComponent(() => import('./x.vue')) }).
volar.cjs splices the block into the SFC's generated TypeScript so vue-tsc
(and golar, and the VS Code Vue extension) typecheck it in place.
HMR
Editing <template> / <script setup> / <style> hot-updates as usual.
Editing the <router> block triggers a full reload - the router is built once
at startup, so a changed route definition cannot be swapped in place.
Note on autoCodeSplitting
Single-file routes are split by construction, so you do not need it for them.
It is passed through to the official plugin unchanged, which is what still
splits inline component: options in .ts route files.
App kit: tanstack-router-sfc/app
Runtime logic for Vue apps on TanStack Router with Nuxt UI.
It ships no components: the app keeps its design-system shells and wires them
to these. Every function that navigates uses useRouter() from the component
it runs in, or takes a router.
| Area | Exports |
|---|---|
| Nuxt UI on TanStack Router | vueRouterShimOptions(history) - app.use(createRouter(vueRouterShimOptions(createWebHistory()))) |
| URL search state | useQueryParam, useRouteSearchModels, useRouteSearchSync, useRouterPush, searchFlag, defineStaticData |
| Route loading | routeLoadingMode, trackRouteLoading(router) |
| Dialogs opened by URL | createDialogs({ dialogs, host, useOverlay, confirm }) -> useOpenDialog, useConfirmAction, useDialogDeepLink; useDialogHost for the host component |
| Maintenance / API down | createMaintenanceMode({ probe }), parseApiError |
| Forms | formStateFromSchema, useEntityForm, useFormSubmit, useSettingsFormSection, provideSaveScope / useSaveSection, useFormFieldModel, useFieldHighlight, formStateKey, formIdKey |
Toasts and translations are the app's: provide them once.
app.provide(formFeedbackKey, () => {
const toast = useToast()
const { t } = useI18n()
return {
success: (title) => toast.add({ color: 'success', title }),
error: (error, title) =>
toast.add({ color: 'error', title: title ?? t('saveFailed') })
}
})vue-router (the shim) and zod (formStateFromSchema types) are optional
peer dependencies; the kit imports neither at runtime.
Development
bun run test # unit tests + a real `vite build` of test/fixture
bun run typecheck
bun run build # tsdown -> dist/Releasing
Squash PRs with conventional titles (fix:, feat:, or feat!:). Release Please keeps the version and changelog in a release PR; merge that PR to publish with release notes and npm provenance. See release and recovery instructions.
