Getting Started
Install
pnpm add @neutro/view
pnpm add -D esbuild typescript tsxjsdom is a runtime dependency of @neutro/view — it is used by the esbuild plugin internally to parse .nv templates at build time. You do not need to install it separately.
Project structure
my-app/
├── src/
│ ├── Counter.nv
│ └── main.ts
├── index.html
└── build.tsCounter.nv
const Counter = $component(() => {
$script(() => {
const count = signal(0)
})
$render(() => html`
<span id="count">${count}</span>
<button @click="${() => count = count + 1}">+</button>
`)
})main.ts
Note: Any
.tsfile that imports directly from a.nvfile must have// @ts-nocheckas its first line..nvmodules have no TypeScript declarations — they are processed exclusively by esbuild + nvPlugin at build time.
// @ts-nocheck
import { Counter } from './Counter.nv'
Counter.mount(document.getElementById('app'), document)index.html
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./dist/main.js"></script>
</body>
</html>build.ts
import * as esbuild from 'esbuild'
import { nvPlugin } from '@neutro/view/renderer/plugin'
await esbuild.build({
entryPoints: ['src/main.ts'],
bundle: true,
outdir: 'dist',
format: 'esm',
platform: 'browser',
target: 'es2022',
plugins: [nvPlugin()],
})Note on the runtime entry: Emitted bundles import
mountfrom@neutro/view/renderer/runtime, not from the main renderer barrel. The runtime entry is slim — it excludes the parser and TypeScript compiler that the build-time barrel co-exports. You do not need to import from this entry directly; the esbuild plugin handles the retarget automatically.
Run the build:
npx tsx build.tsServe and open
ES modules require a server
Opening index.html directly from the filesystem (file://) will fail — browsers block ES module imports from file:// origins. You must serve the directory over HTTP.
npx serve .Open http://localhost:3000. You should see a counter with a + button; clicking it increments the number.
Tagged-template path (no build step)
The tagged-template path is a first-class authoring surface, not a fallback. Import createHtmlTag and mount directly — no compiler plugin, no esbuild integration. Use it when you prefer working in plain TypeScript, when a build plugin is not available, or when you want the simplest possible setup.
The explicit-thunk rule
Thunks are required — no erasure happens at runtime
In .nv files the compiler rewrites bare signal reads and assignments automatically. In the tagged template there is no such erasure. Every reactive value in a template hole must be a thunk. The runtime throws if you forget:
[nv/html] Expression at hole N is not a function. Wrap reactive values in thunks: ${() => signal()} not ${signal()}.Side-by-side comparison:
// .nv (compiler erases bare reads):
${count} // → count() (auto-erased)
@click="${() => count = count + 1}" // → count.set(count() + 1) (auto-erased)
// tagged template (explicit — no erasure):
${() => count()} // thunk required
@click="${() => count.set(count() + 1)}" // explicit .set()Example
import { createHtmlTag, mount } from '@neutro/view/renderer'
import { signal } from '@neutro/view/core'
const html = createHtmlTag(document) // bind the tag to a document once
const count = signal(0)
const view = html`
<div>
<p>${() => count()}</p>
<button @click="${() => count.set(count() + 1)}">increment</button>
</div>
`
mount(view, document.getElementById('app')!, document)mount takes three arguments: (ir, parent, doc) — the third argument is the document instance.
API signatures
// from @neutro/view/renderer
export function createHtmlTag(document: Document): (strings: TemplateStringsArray, ...exprs: unknown[]) => TemplateIR
export function mount(ir: TemplateIR, parent: Element, doc: Document): () => void
// signals from @neutro/view/coreHow to run
No build.ts or esbuild plugin required. Serve the entry file with any TypeScript-capable dev server. For example, with Vite:
pnpm add -D vite
npx vitePoint index.html at your entry file as a type="module" script. Vite handles TypeScript and ES modules out of the box — no config needed for a basic app.
Because createHtmlTag needs a live document, the code runs in a browser (or jsdom for tests) — not in Node directly.
See also:
- Rendering guide —
each(), conditionals,classes(),slots() - API Reference — full tagged-template signatures
Next steps
- Guides — the design model and all template features
- Authoring .nv — full .nv syntax reference
- API Reference — all exported functions