Write PureScript in Vue Single File Components.
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script lang="purs">
import Prelude
import Pue (ref, modifyRef)
setup = do
count <- ref 0
let increment = modifyRef (_ + 1) count
pure { count, increment }
</script>PureScript's type system and algebraic structures meet Vue's reactivity. No code generation, no separate files — just lang="purs" in your <script> block.
npx pue init my-app
cd my-app
npm install
npm run devThis scaffolds a complete project — package.json, vite.config.ts, spago.dhall, source files, and editor config.
npm install pue purescript spagovite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { pue } from 'pue'
export default defineConfig({
plugins: [pue(), vue()],
})Module names are derived from file paths (src/components/Counter.vue → App.Components.Counter). Standalone .purs files in source directories are also compiled.
- The Vite plugin detects
<script lang="purs">blocks - PureScript code is extracted to
.pue/and compiled viaspago build - The
<script>block is rewritten to import the compiled JavaScript - Vue's template bindings work through the
setupconvention
Export a setup function that returns a record. Field names become template bindings — no annotation needed:
setup = do
count <- ref 0
let increment = modifyRef (_ + 1) count
pure { count, increment }Ref a→ reactive values, auto-unwrapped in templatesEffect Unit→ event handlers (@click, etc.)Effect a→ PureScript'sEffectcompiles to() => a, naturally aligning with Vue's deferred-execution APIs
Ref supports <$>, <*>, and algebraic operations — derived refs are created as Vue.computed automatically:
a <- ref 0
b <- ref 0
let total = a + b -- Semiring: computed(() => a.value + b.value)
let doubled = (_ * 2) <$> a -- Functor
let combined = lift2 gcd a b -- Applycelsius <- ref 20
let fahrenheit = focus (\c -> c * 9 / 5 + 32) (\f -> (f - 32) * 5 / 9) celsius
-- Both celsius and fahrenheit are writable; changes propagate in both directionsDeclare props and emits as module-level phantom values. The plugin extracts types from externs; setup accesses them via visible type applications:
import Prelude
import Pue (DefineProps, defineProps, DefineEmits, defineEmits, emit, toRef)
props :: DefineProps { msg :: String, count :: Int }
props = defineProps
emits :: DefineEmits { notify :: Unit }
emits = defineEmits
setup = do
countRef <- toRef @"count" props
let doubled = (_ * 2) <$> countRef
let notify = emit @"notify" emits unit
pure { doubled, notify }DefineComponent consolidates props, emits, model, expose, and slots into a single declaration when you don't need runtime access to the handles:
define :: DefineComponent
( props :: { msg :: String, count :: Int }
, emits :: { notify :: Unit }
)
define = defineComponentComponent options and prop defaults use identity macros:
import Pue (defineOptions, defineDefaults)
options = defineOptions { inheritAttrs: false }
defaults = defineDefaults { count: 0 }Use a vanilla <script> block for child component imports:
<script>
import ChildComponent from './ChildComponent.vue'
</script>
<script lang="purs">
import Prelude
import Pue (provide)
setup = do
provide @"theme" "dark"
pure {}
</script>See docs/api.md for the full API reference.
pue is not a different framework — it is Vue, with PureScript as the scripting language. Most differences are direct consequences of the type system.
What improves:
-- Reactive expressions: no .value, no computed(() => ...)
let total = a + b -- vs computed(() => a.value + b.value)
let label = show <$> count -- vs computed(() => `${count.value}`)
-- Pure vs effectful, visible in syntax
let doubled = (_ * 2) <$> count -- let: pure derivation
count <- ref 0 -- <-: side effect
-- Type-safe provide/inject without InjectionKey boilerplate
provide @"theme" "dark"
theme <- inject @"theme" (pure "light")
-- Bidirectional refs in one line
let fahrenheit = focus (\c -> c * 9.0 / 5.0 + 32.0) (\f -> (f - 32.0) * 5.0 / 9.0) celsiusWhat changes:
-- Props: explicit toRef (no proxy mechanism in PureScript)
countRef <- toRef @"count" props -- vs props.count
-- Emits: phantom handle carries the row constraint
emit @"notify" emits unit -- vs emit('notify')
-- Ref access: explicit functions instead of .value
modifyRef (_ + 1) count -- vs count.value++See docs/comparison.md for the full comparison.
npx pue editor neovim # tree-sitter injection + LSP instructions
npx pue editor vscode # syntax extension
npx pue editor volar # Volar integrationSee docs/editor.md for details.
| Topic | Description |
|---|---|
| API reference | Complete PureScript API for Vue reactivity |
| Compared to Vue JS | What improves and what changes vs vanilla Vue |
| Type system | How Vue's reactivity maps to PureScript's types |
| Architecture | How the Vite plugin works internally |
| Editor support | Neovim, VSCode, Volar setup |
MIT