@inglorious/vite-plugin-vue

A Vite plugin that transforms Vue-like template syntax into lit-html templates.
1,234 22
Install
npm install @inglorious/vite-plugin-vue
Latest Version:1.0.5
License:MIT
Last Updated:Oct 23, 2026
Links: GitHub  ·  npm
Maintainer: iceonfire

@inglorious/vite-plugin-vue

Transform Vue-like template syntax into lit-html templates for @inglorious/web.

Installation

npm install @inglorious/vite-plugin-vue

Setup

In your vite.config.js:

import { defineConfig } from "vite"
import { vue } from "@inglorious/vite-plugin-vue"

export default defineConfig({
  plugins: [vue()],
})

Usage

Create .vue files with <template> and optional <script> sections:

<template>
  <div class="counter">
    <h2>Count: {{ count }}</h2>
    <button @click="increment">+</button>
  </div>
</template>

<script>
const count = 0

const increment = (entity) => {
  entity.count++
}
</script>

This compiles to a standard @inglorious/web component:

import { html } from "@inglorious/web"

export const Counter = {
  create(entity) {
    entity.count = 0
  },

  increment(entity) {
    entity.count++
  },

  render(entity, api) {
    return html`
      <div class="counter">
        <h2>Count: ${entity.count}</h2>
        <button @click=${() => api.notify(`#${entity.id}:increment`)}>+</button>
      </div>
    `
  },
}

Syntax: Vue vs lit-html

Interpolation

Vue:

<p>Hello {{ name }}!</p>
<p>Result: {{ x + y }}</p>

lit-html:

html`
  <p>Hello ${entity.name}!</p>
  <p>Result: ${entity.x + entity.y}</p>
`

Property Binding

Vue:

<input :value="text" :disabled="isLoading" />
<div :class="activeClass"></div>

lit-html:

html`
  <input .value=${entity.text} .disabled=${entity.isLoading} />
  <div class=${entity.activeClass}></div>
`

Note: class, id, and kebab-case attributes use regular binding. CamelCase properties use .property syntax.

Event Handlers

Vue:

<button @click="handleClick">Click</button> <input @input="handleInput" />

lit-html:

html`
  <button @click=${() => api.notify(`#${entity.id}:handleClick`)}>Click</button>
  <input @input=${() => api.notify(`#${entity.id}:handleInput`)} />
`

Note: Method names are automatically wrapped with api.notify(). For inline handlers or parameters, use arrow functions: @click="(e) => customHandler(entity, e)"

Conditionals

Vue:

<div v-if="isLoading">Loading...</div>
<div v-else-if="hasError">Error</div>
<div v-else>Success</div>

lit-html:

import { when } from "@inglorious/web/directives/when"

html`
  ${when(
    entity.isLoading,
    () => html`<div>Loading...</div>`,
    () =>
      when(
        entity.hasError,
        () => html`<div>Error</div>`,
        () => html`<div>Success</div>`,
      ),
  )}
`

Lists

Vue:

<ul>
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>

lit-html:

import { repeat } from "@inglorious/web/directives/repeat"

html`
  <ul>
    ${repeat(
      entity.items,
      (item) => item.id,
      (item) => html`<li>${item.name}</li>`,
    )}
  </ul>
`

Component Structure

Vue files compile to standard @inglorious/web components:

Script Section Parsing

The plugin analyzes your <script> section to separate:

  • State variables: const/let declarations with primitive values → create(entity) method
  • Methods: const/let with function values → methods on the type
<script>
// State variables
const count = 0
const items = []

// Methods
const increment = (entity) => entity.count++
const addItem = (entity, item) => entity.items.push(item)
</script>

Compiles to:

export const Component = {
  // State initialization
  create(entity) {
    entity.count = 0
    entity.items = []
  },

  // Methods become event handlers
  increment(entity) {
    entity.count++
  },

  addItem(entity, item) {
    entity.items.push(item)
  },

  // Template becomes render
  render(entity, api) {
    return html`...`
  },
}

Event Handler Transformation

  • Method references (@click="increment") → api.notify(\#${entity.id}:increment`)`
  • Inline expressions (@click="(e) => handler(entity, e)") → passed through as-is

Template Interpolation

  • In templates: Just use the variable name: {{ count }}
  • Compiled output: Automatically prefixed: ${entity.count}
  • In v-for loops: Loop variables are NOT prefixed: {{ item.name }} → ${item.name}

TypeScript Support

Use lang="ts" in the script section:

<template>
  <div>{{ message }}</div>
</template>

<script lang="ts">
interface Props {
  initialMessage: string
}

const message: string = "Hello"

export default {
  message,
}
</script>

Examples

Counter

<template>
  <div class="counter">
    <h2>Count: {{ count }}</h2>
    <button @click="increment" :disabled="count >= 10">+</button>
    <button @click="decrement" :disabled="count <= 0">-</button>
    <button @click="reset" v-if="count > 0">Reset</button>
  </div>
</template>

<script>
const count = 0

const increment = (entity) => entity.count++
const decrement = (entity) => entity.count--
const reset = (entity) => (entity.count = 0)
</script>

Todo List

<template>
  <div class="todos">
    <ul v-if="todos.length > 0">
      <li v-for="todo in todos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.done"
          @change="() => toggle(entity, todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button @click="() => remove(entity, todo.id)">×</button>
      </li>
    </ul>
    <p v-else>No todos yet!</p>
  </div>
</template>

<script>
const todos = []

const toggle = (entity, id) => {
  const todo = entity.todos.find((t) => t.id === id)
  if (todo) todo.done = !todo.done
}

const remove = (entity, id) => {
  entity.todos = entity.todos.filter((t) => t.id !== id)
}
</script>

Form Handling

<template>
  <form @submit="handleSubmit">
    <input
      type="email"
      :value="email"
      @input="(e) => updateEmail(entity, e)"
      :class="error ? 'error' : ''"
    />
    <span v-if="error">{{ error }}</span>
    <button type="submit" :disabled="!isValid(entity)">Submit</button>
  </form>
</template>

<script>
const email = ""
const error = ""

const updateEmail = (entity, e) => {
  entity.email = e.target.value
  entity.error = ""
}

const isValid = (entity) => {
  return entity.email.includes("@") && entity.email.length > 5
}

const handleSubmit = (entity, e) => {
  e.preventDefault()
  if (!isValid(entity)) {
    entity.error = "Invalid email"
    return
  }
  console.log("Submitted:", entity.email)
}
</script>

Why Vue Syntax?

Vue templates offer:

  • Familiar directives (v-if, v-for) instead of function calls
  • Cleaner interpolation ({{ }} vs ${})
  • Explicit binding syntax (:prop, @event)
  • Separation of template and logic

All while compiling to the same efficient lit-html output!

License

MIT

Related Packages

@vitejs/plugin-vue

The official plugin for Vue SFC support in Vite.

333,357,380 686
vite-plugin-checker

Vite plugin that runs TypeScript type checker on a separate process.

133,975,562 1,242
@vitejs/plugin-vue-jsx

Provides Vue 3 JSX & TSX support with HMR.

93,663,827 686
vite-plugin-ssr

Like Next.js/Nuxt but as do-one-thing-do-it-well Vite plugin.

550,634 18
vite-plugin-md

Markdown for Vite

496,869 640

Version History

Version Released License
1.0.5 MIT
1.0.4 MIT
1.0.3 MIT
1.0.2 MIT
1.0.1 MIT