- TypeScript 62.5%
- Rust 30.7%
- CSS 4.1%
- JavaScript 1.3%
- HTML 0.9%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
@scope blocks prefixed with the view's own tag can never match — the scoping root already scopes the rules — so seven previews styled blank (drawer, otp-field, scroll-area, slider, tooltip, toggle-group, toolbar). Previews that promised interaction now mirror their components: slider drag and keys, drawer open/Esc/backdrop, tooltip hover delay and sticky pointer, toolbar roving tabindex, checkbox form behaviour, otp-field typing and paste, toggle-group presses. The button page grows a filled primary and a disabled rule that matches the static preview. Sidebar and catalog lost their Drawer triplicate and read alphabetically. Dialog and alert-dialog demos reworked; the radio preview refuses items without a value. |
||
| cli | ||
| docs | ||
| examples | ||
| framework | ||
| library | ||
| spec | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
vweb
A Rust CLI that scaffolds and maintains zero-dependency web-standards SPAs — projects whose entire runtime is native browser API. No framework, no router library, no state library, no CSS framework. The only tooling is Vite+ and TypeScript, both dev-only.
📖 Documentation: docs/index.html — hand-written static
HTML, no build step. Open it in a browser, or serve docs/ anywhere.
cargo install --path cli
vweb new my-app # scaffold
cd my-app
vweb dev # → vp dev
vweb g page about --nav About
vweb g component card --light
vweb g store cart --persist
make install
make check
make test
make build
make fmt
make docs-serve
Working on vweb itself? AGENTS.md is the orientation, and
spec/ is what the project guarantees.
Commands
| Command | What it does |
|---|---|
vweb new <name> |
Scaffold a project. --template framework|<path>|<git-url> |
vweb dev / build / preview / test / fmt / check |
Proxy straight to vp, args forwarded verbatim |
vweb g component <name> |
Custom element as .ts + .html + .css. --light for light DOM + @scope, --test for a test file |
vweb g page <name> |
View (three files) and its route entry. --path /docs/:slug, --nav <label> |
vweb g store <name> |
Typed store slice. --persist wires localStorage |
vweb g test <path> |
Test file next to an existing module |
vweb destroy <kind> <name> |
Reverses a generate — file plus route registration |
vweb add <name>... |
Copy a headless component into src/ui. --list to see them, --force to overwrite |
vweb remove <name> |
Take one back out, pruning core modules nothing else needs |
generate is aliased to g, destroy to d.
Full reference: command map ·
vweb new · vweb generate ·
vweb destroy ·
headless components ·
vweb.toml and passthrough.
The component library
generate writes a blank skeleton; add writes a solved one. The components in
library/ are headless — behaviour, ARIA and keyboard handling, and
no CSS or markup at all (a test fails if a .css or .html file appears in
there). State comes back out as data-* attributes, and that is the whole
styling contract.
vweb add --list
vweb add collapsible dialog tabs switch
They are copied, not installed, because a project with zero runtime
dependencies cannot install a component library — and because source you own
beats a version you upgrade past. Each component declares the shared modules it
needs in a manifest.toml; only those are copied, and vweb remove prunes the
ones nothing is left asking for.
Full reference: headless components.
The framework
framework/ is what vweb new copies out: router, store, base
component, design system, home page and 404. Nothing to delete before you start.
It is embedded in the binary, so scaffolding works offline and the framework
version always matches the CLI version.
--template names it framework, but that is the default, so you only ever
pass it to be explicit. It also accepts a local directory or a git URL. Note
that a git template is someone else's code: vweb prints the resolved source
and refuses to run an install against it unless you pass --install.
How generation works
There is no TypeScript parser and no CSS surgery. Generators write whole files, and where something must be registered they splice text around a marker comment. A project contains exactly one marker:
// src/router/routes.ts
export const routes: RouteDefinition[] = [
// @vweb:routes
]
Component stylesheets declare their own @layer and @scope, so nothing
central registers them, and generated route entries carry a
// @vweb:route:<slug> tag that makes vweb destroy exact — an integration
test asserts routes.ts comes back byte-identical.
The long version: the generation model and why destroy is exact.
Building apps with it
A scaffolded project is documented in two places: README.md inside the project
(it ships with the template, so it works offline) and the runtime half of the
docs site — app anatomy,
components, state,
routing, styling,
platform APIs, and
gotchas and deployment.
Editing the framework
framework/ is a source for the CLI, not a runnable project — it has no
node_modules, so an editor opened on it type-checks with whatever TypeScript
it bundles rather than the pinned 7.x, and reports vitest and vite-plus as
unresolved. Both are expected there and neither appears in a scaffolded project.
Because of that, framework source avoids depending on very recent DOM library
definitions. src/env.d.ts types the slice of the Navigation API the router
uses, and vendor/polyfills/invokers.ts types requestClose locally. Neither is a
polyfill — types only — and both keep the source checking cleanly in an editor
that predates those definitions.
To work on it properly, scaffold from it and edit there:
vweb new /tmp/scratch
Then copy the change back, and into all example apps — they are scaffolded projects and a test holds them to it.
Examples
examples/todo-app is a real project — install it, run it,
break it. It is also what the documentation quotes: cargo test --test docs
fails if a page and the app disagree, so the snippets on the site cannot rot.
examples/minimal is a straight scaffold with no app-level
customizations. It exists to keep one example app byte-close to the framework,
so framework drift is obvious in tests.
The component library is documented on its own pages under
docs/components/, each carrying its whole example.
examples/ is for example apps.
Development
There is no manifest at the repository root — the Rust domain owns its own, so
cargo runs inside cli/:
cd cli
cargo test # unit + integration, all hermetic
cargo build --release
Integration tests drive the real binary: they scaffold into temp directories,
run every generator, assert the results, and verify each destroy restores the
file it touched. No network, no package manager.
Three direct dependencies — clap, include_dir, anyhow. Git operations
shell out to the git binary; temp directories use std::env::temp_dir.
The repository is six domains side by side — cli/,
framework/, library/, docs/,
examples/ and spec/ — with no
manifest at the root: cli/Cargo.toml is the whole Rust domain. The CLI embeds
the two trees that sit outside it, so it is not publishable to crates.io and
ships as a release binary instead.
framework/ and library/ are embedded at compile time. include_dir!
embeds whatever is on disk, so install output in those trees would end up inside
the binary. Working there is fine; the scaffolder and the registry skip
node_modules, build output and lockfiles wherever they came from. Keep both
trees clean when you commit, and expect a release build to warn if you do not.
More, including how a framework change reaches the examples and how to add a generator: contributing.
License
MIT — see LICENSE.