npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vanilla-way-mvvm

v0.1.0

Published

A tiny MVVM component helper for vanilla-way: model + vm + view wiring with a seed/bind prop rule.

Readme

vanilla-way-mvvm

An MVVM composition layer over vanilla-way. The reactivity core stays a single instance: vanilla-way is a peerDependency, never bundled. Zero runtime dependencies.

It exports one runtime helper:

component({ view, model?, vm? })

which wires a 3-layer MVVM convention into a single typed call site. Type inference flows model -> vm -> view. Only view is required — a view-only component({ view }) is valid; model and vm are optional.

You never hand-write that call. A small zero-dep CLI — vanilla-way-mvvm gen — generates each component's wiring from its folder. See Codegen.

Folder convention

A component is authored as a folder of small, single-responsibility files:

Counter/
  Counter.view.tsx    // REQUIRED: dumb view, reads the vm, returns JSX
  Counter.model.ts    // OPTIONAL: schema + default values, e.g. { count: 0 }
  Counter.vm.ts       // OPTIONAL: derive signals / actions from the store
  Counter.scss        // OPTIONAL: styles (.scss or .css)
  index.ts            // AUTO-GENERATED glue: component({ view, model?, vm? })

<Name>.view.tsx is the only required file — its presence is what marks the folder as a component. Everything else is optional. You author the small files; you do not hand-write index.ts — the codegen CLI writes it for you.

Codegen: vanilla-way-mvvm gen

The package ships a tiny CLI (Node built-ins only — stays zero-dep) that scans a components root, finds every component folder, and writes its index.ts glue.

# one-shot: generate index.ts for every component under the root
npx vanilla-way-mvvm gen

# regenerate on change, for real-time dev alongside a dev server
npx vanilla-way-mvvm gen --watch
  • Commandgen is the only subcommand.
  • Root — defaults to src/components. Override with --root <path> / --root=<path>, or as a positional arg: vanilla-way-mvvm gen packages/ui.
  • What it does — for each folder containing a <Name>.view.tsx, it emits <Name>/index.ts that imports whichever of <Name>.model.ts, <Name>.vm.ts and <Name>.scss/.css exist and calls component({ view, model?, vm? }).
  • Write-only-if-changed — identical output is left untouched, so it never churns git or triggers needless reloads. --watch also ignores its own index.ts output.

Wire it into package.json scripts and run the watcher next to your bundler — one command for real-time dev:

{
  "scripts": {
    "gen": "vanilla-way-mvvm gen",
    "dev": "concurrently \"vanilla-way-mvvm gen --watch\" \"vite\""
  }
}

The generated index.ts is committed and is marked // AUTO-GENERATED by vanilla-way-mvvm. Do not edit. — treat it as build output you keep in the tree, not a file you edit by hand.

The prop rule (seed vs bind)

For each model field, a component instance accepts either a plain value or a signal:

  • A plain value seeds the field — a fresh signal is created, initialised to that value, and owned by the instance (cleaned up on dispose).
  • A signal binds the field — the passed signal is the state. It is shared two-way and is never disposed by the component.
  • Omitting a field seeds it with the model default.
<Counter count={0} />         // seed: independent local state
<Counter count={someSignal} />// bind: shared two-way with someSignal

The discriminator is structural (isReadable from vanilla-way): anything with .get + .subscribe binds; everything else seeds.

Worked example

A full component folder — Counter/ — with model, vm, view and styles. You author these three files (plus Counter.scss):

// Counter.model.ts — schema + defaults
const model = { count: 0 };
export default model;
// Counter.vm.ts — derive signals / actions from the store
import type { StoreOf } from "vanilla-way-mvvm";
type Model = { count: number };

export default (s: StoreOf<Model>) => ({
  ...s,
  doubled: s.count.derive((n) => n * 2),
  inc: () => s.count.set(s.count.get() + 1),
  reset: () => s.count.set(0),
});
// Counter.view.tsx — dumb view over the vm
export default (vm) => (
  <div class="counter">
    <span>{vm.count}</span>
    <span>x2 = {vm.doubled}</span>
    <button onClick={vm.inc}>+</button>
    <button onClick={vm.reset}>reset</button>
  </div>
);

Running vanilla-way-mvvm gen writes the glue for you — imports only the files that exist:

// Counter/index.ts
// AUTO-GENERATED by vanilla-way-mvvm. Do not edit.
import { component } from 'vanilla-way-mvvm';
import view from './Counter.view';
import model from './Counter.model';
import vm from './Counter.vm';
import './Counter.scss';

export const Counter = component({ model, vm, view });

A view-only component is just a folder with a single view file — Hello/:

// Hello.view.tsx
export default () => <div class="hello">Hello from a view-only component.</div>;

which generates the minimal glue (no model, vm, or style import):

// Hello/index.ts
// AUTO-GENERATED by vanilla-way-mvvm. Do not edit.
import { component } from 'vanilla-way-mvvm';
import view from './Hello.view';

export const Hello = component({ view });