lunx.docs
DocsFrameworksVue

Vue Adapter

Vue 3 with Composition API, script setup, Vue Router, Pinia state management, and SFC (Single File Component) support — all zero-config.

Vue 3 · Composition API · Pinia
Vue 3 SFCs

Single File Components with <template>, <script setup>, and <style> blocks compiled natively.

Vue HMR

Component-level hot reload with official @vue/runtime-dom HMR API. State preserved on update.

Composition API

Full Composition API support including composables, provide/inject, and reactive refs.

script setup

<script setup> single-file syntax for concise component authoring with auto-exposed bindings.

Volar-compatible

TypeScript types via Volar. Configure tsconfig with @vue/language-tools for IDE support.

Auto-import

Use @lunx/plugin-auto-import to eliminate import statements for Vue APIs and Pinia stores.

Setup

bash
npm create lunx-dev@latest my-vue-app -- --framework vue --ts
cd my-vue-app && npm install && npm run dev
typescriptlunx.config.ts
import { defineConfig } from 'lunx'

export default defineConfig({
  framework: 'vue',
})

A Vue component with script setup

htmlsrc/components/Counter.vue
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{
  initialValue?: number
  label?: string
}>()

const count    = ref(props.initialValue ?? 0)
const doubled  = computed(() => count.value * 2)
const canReset = computed(() => count.value !== 0)
</script>

<template>
  <div class="flex flex-col items-center gap-4 p-6 rounded-xl border border-zinc-800">
    <h2 class="text-lg font-semibold text-white">{{ label ?? 'Counter' }}</h2>
    <p class="text-4xl font-bold tabular-nums text-blue-400">{{ count }}</p>
    <p class="text-sm text-zinc-500">doubled: {{ doubled }}</p>
    <div class="flex gap-2">
      <button @click="count--" class="px-4 py-2 rounded-lg bg-zinc-800 text-white">−</button>
      <button @click="count++" class="px-4 py-2 rounded-lg bg-blue-600 text-white">+</button>
      <button @click="count = 0" :disabled="!canReset" class="px-4 py-2 rounded-lg bg-zinc-900 text-zinc-400 disabled:opacity-30">Reset</button>
    </div>
  </div>
</template>

<style scoped>
/* Scoped styles only apply to this component */
button { transition: opacity 0.2s; }
</style>

Vue Router 4

bash
npm install vue-router@4
typescriptsrc/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

export const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path:      '/',
      component: () => import('@/pages/Home.vue'),  // code-split automatically
    },
    {
      path:      '/about',
      component: () => import('@/pages/About.vue'),
    },
    {
      path:      '/users/:id',
      component: () => import('@/pages/UserProfile.vue'),
      props: true,
    },
    {
      path:      '/:pathMatch(.*)*',
      component: () => import('@/pages/NotFound.vue'),
    },
  ],
})

State management with Pinia

bash
npm install pinia
typescriptsrc/stores/useUserStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const user    = ref<{ id: number; name: string; email: string } | null>(null)
  const loading = ref(false)

  // Getters
  const isLoggedIn   = computed(() => user.value !== null)
  const displayName  = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email: string, password: string) {
    loading.value = true
    try {
      const res  = await fetch('/api/auth/login', {
        method: 'POST',
        body:   JSON.stringify({ email, password }),
        headers: { 'Content-Type': 'application/json' },
      })
      user.value = await res.json()
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
  }

  return { user, loading, isLoggedIn, displayName, login, logout }
})

Composables pattern

typescriptsrc/composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'

export function useFetch<T>(url: Ref<string> | string) {
  const data    = ref<T | null>(null)
  const loading = ref(true)
  const error   = ref<string | null>(null)

  watchEffect(async (onCleanup) => {
    const controller = new AbortController()
    onCleanup(() => controller.abort())

    loading.value = true
    error.value   = null
    try {
      const res  = await fetch(typeof url === 'string' ? url : url.value, {
        signal: controller.signal,
      })
      data.value    = await res.json()
    } catch (e) {
      if ((e as Error).name !== 'AbortError') {
        error.value = (e as Error).message
      }
    } finally {
      loading.value = false
    }
  })

  return { data, loading, error }
}
Enable Vue DevTools
Install the Vue DevTools browser extension to inspect component trees, Pinia stores, router history, and timeline events. Lunx enables devtools in development mode automatically.