Skip to content
weftGitHub

LearnGet started

Installation

This page takes you from an empty folder to a running weft app, then walks through what you got. You need Node 26 and npm. Once weft is published, Bun or Deno can take Node's place: the CLI, weft dev and weft build all run on either one, with no Node installed.

Rust isn't needed to use weft. The compiler comes with it as WebAssembly, which runs anywhere, and a native addon for your platform installs by itself and makes builds faster. Rust only comes in if you choose to write Rust server functions. The next page, Editor and TypeScript setup, says more about the two builds of the compiler.

Create an app

weft isn't on npm yet, so for now an app is made from a checkout of the repo, and building that checkout's compiler does take Rust (cargo). Once weft is published, npm create @weft-js replaces the first two steps and no Rust is involved.

1. Get the repo

bash
git clone https://github.com/Ravonus/weft.git

2. Create the app

Run this from the folder that holds weft. It builds what's out of date, packs each package as a tarball and creates my-app beside the repo, wired to those tarballs.

bash
node weft/packages/create-weft/scripts/local.mjs new my-app --yes --db none

--db none leaves out the database so there's nothing to set up. Leave off --yes and it asks you each question in turn, with the default already picked: arrow keys move, Enter chooses.

Once weft is published, the same app is one command, and it takes the same flags:

bash
npm create @weft-js -- my-app --yes --db none

bun create @weft-js and deno run -A npm:@weft-js/create will do the same for those runtimes.

3. Start it

bash
cd my-appnpx weft dev

Open the address it prints. You'll see a card with a button that counts your clicks.

4. Change something

Open src/routes/index.tsx. Add derived to the import from @weft-js/core, and work out a new value under count:

tsx
const count = state(0)const doubled = derived(() => count.value * 2)

Show it above the button:

tsx
<p>Doubled: {doubled.value}</p>

Save, and the page updates. Click the button and both numbers change. You didn't tell doubled what to watch: it read count, so it follows count.

Every option

Each question the generator asks has a flag, so an agent or a CI job never sees a prompt. With no terminal, or with --yes, every question you didn't answer takes its default.

FlagValuesDefaultWhat it does
[dir]a folder nameasked, my-weft-app with --yesWhere the app goes. It must be missing or empty
--uiwarp, headless, tailwind, nonewarpThe UI kit. headless gives you warp's behaviour and tokens under your own markup
--dbsqlite, postgres, mysql, nonesqliteA database, and a todo page that uses it
--layerdrizzle, prisma, nativedrizzleHow the app talks to the database. native is SQL migrations with no ORM
--auth, --no-authon when there's a databaseSign-in with Better Auth, and a login page
--auth-providerbetter-auth, custombetter-authcustom writes a stub for your own sign-in instead
--runtimenode, bun, denowhichever is running the generatorWhat weft build targets
--configts, json, yamltsThe format of weft.config
--pmnpm, pnpm, bun, deno, yarnthe one you ran it withThe package manager for the install
--git, --no-gitonStart a git repository
--install, --no-installonInstall the dependencies
--namea package namethe folder's nameThe name in package.json
--yes, -yTake the defaults for anything not given

--auth keeps its users in the database, so it needs --db sqlite, postgres or mysql; with --db none the generator stops and says so. A few combinations, to give you the idea:

bash
# a static-looking site with Tailwind and no server datanode weft/packages/create-weft/scripts/local.mjs new site --yes --ui tailwind --db none# Postgres through Prisma, no sign-in, built for Bunnode weft/packages/create-weft/scripts/local.mjs new shop --yes --db postgres --layer prisma --no-auth --runtime bun

local.mjs new always installs with npm, because that's what the tarballs are wired up for: --pm and --install are for the published generator.

What's in a new app

Here is the app --yes makes, with every default: warp, SQLite through Drizzle, and Better Auth.

text
my-app/├── AGENTS.md                 what an AI agent needs to know about this app (generated)├── package.json              scripts that call `weft`├── weft.config.ts            the app's settings: site metadata, plugins├── tsconfig.json             strict TypeScript, TSX for weft, the editor plugin├── drizzle.config.ts         where Drizzle finds the schema├── .env                      DATABASE_URL and the auth secret (not committed)├── .env.example              the same keys, without the secret├── .gitignore├── .vscode/settings.json     tells VS Code where the editor plugin is├── .weft/plugins.lock.json   the plugins you approved, and what they may do└── src/    ├── routes/    │   ├── _layout.tsx       wraps every page: the nav and <main>    │   ├── index.tsx         the home page, at /    │   ├── about.tsx         /about    │   ├── login.tsx         sign in and sign up    │   └── todos/    │       ├── _layout.tsx   sends you to /login when signed out    │       └── index.tsx     the todo list, at /todos    ├── server/    │   ├── todos.ts          the queries and mutations the todo page calls    │   └── session.ts        me, signUp, signIn, signOut    ├── db/    │   ├── schema.ts         the tables    │   └── index.server.ts   the database connection    ├── auth.server.ts        Better Auth, on the same database    ├── context.server.ts     what every server function gets as `ctx`    └── env.server.ts         checks the environment when the server starts

With --db none there's no .env, drizzle.config.ts, src/server, src/db or .server.ts file: only the config files and three routes. The parts worth knowing on day one:

  • src/routes is your pages. A file is a route: about.tsx is /about, todos/index.tsx is /todos, and a _layout.tsx wraps the pages beside and below it. Routing and layouts covers the rest.
  • src/server holds server functions: query and mutation. A component imports them like any module and calls them; the code inside only ever runs on the server. Server functions explains how.
  • A .server.ts file never reaches the browser. The database, the auth secret and the environment live in those.
  • weft.config.ts is short on purpose. site gives every page its title pattern and description, and plugins lists what extends weft, here warp(). The config reference lists every key.
  • package.json's scripts are all weft commands: dev, build, check, typecheck (which is weft tsc), start, and db:generate when there's a database. A script that runs something other than weft gets a P002 warning, because an agent can't see what it does. Project commands go in tasks in weft.config.ts instead.
  • AGENTS.md is written by weft agents-md right after the install, from the weft you installed. Don't edit it: Working with AI agents explains what it holds and how to regenerate it.

A .weft/ folder also appears the first time you run weft dev or weft check. It holds files weft generates, such as .weft/map.md, and only the plugin lock in it is committed.

Run, check and build

These are the commands you'll use every day. Each one is npx weft <command> with npm.

CommandWhat it does
weft devThe dev server, with the page updating as you save. --port 3000 picks the port
weft checkReads the whole app and reports mistakes, each with a code, a reason and a fix
weft tscType-checks the project, with the prop types weft works out for you
weft buildThe production build, into .output/
weft startRuns .output/
weft db generateA migration for what changed in src/db/schema.ts. It applies on the next request

An app with a database needs one migration before its first run:

bash
npx weft db generatenpx weft dev

Then, before you ship:

bash
npx weft checknpx weft buildnpx weft start

weft check is the one to run most. It compiles every file in check mode, and also checks your tests, your config, your design tokens and accessibility, and each route's page metadata. Diagnostics and weft check is about reading what it says.

weft --help lists every command. You'll meet most of them later: weft add adds a feature to an app you already have (weft add tailwind, weft add db postgres, weft add auth, weft add rust), and weft doctor says which compiler and runtime you're on.

Node, Bun or Deno

An app builds for and runs on all three, and the CLI runs on whichever one starts it, with none of the others installed.

NodeBunDeno
Run a commandnpx weft devbunx --bun weft devdeno run -A npm:@weft-js/cli dev
Build for itweft build --runtime nodeweft build --runtime bunweft build --runtime deno

weft build targets the runtime key in weft.config.ts. When that isn't set, it targets whichever runtime ran it, and --runtime overrides both. weft start runs the build on the same runtime. The generator's --runtime flag writes the key for you. Runtimes and deploying covers hosts and presets.

Add weft to an existing Vite project

You don't need @weft-js/start to use weft. A Vite app can use weft's components and reactivity alone, with no server routes, and the same checks work on it.

You need Vite 8, and two packages: @weft-js/core (the runtime and its Vite plugin) and @weft-js/cli as a dev dependency (the weft command). Until they're published, add them to your package.json by name and let local.mjs point them at your checkout's tarballs:

json
{  "dependencies": { "@weft-js/core": "0.1.0" },  "devDependencies": { "@weft-js/cli": "0.1.0", "vite": "8.3.5" }}
bash
node weft/packages/create-weft/scripts/local.mjs refresh my-vite-app

refresh works with npm only, and stops if it finds another package manager's lockfile.

Add the plugin to vite.config.ts. It compiles .tsx and .jsx files with weft's compiler:

<!-- example: skip a Vite config, which lives outside src; the scratch app is a @weft-js/start app with its own -->

ts
import { defineConfig } from 'vite'import weft from '@weft-js/core/vite'export default defineConfig({ plugins: [weft()] })

Tell TypeScript that TSX is weft's. These are the keys that matter; keep the rest of your tsconfig.json:

json
{  "compilerOptions": {    "jsx": "preserve",    "jsxImportSource": "@weft-js/core",    "types": ["@weft-js/core/client"],    "plugins": [{ "name": "@weft-js/cli/ts-plugin" }]  }}

Then mount a component from your entry module with render from @weft-js/core/dom:

tsx
// src/main.tsximport { state } from '@weft-js/core'import { render } from '@weft-js/core/dom'function App() {  const count = state(0)  return (    <button type="button" onClick={() => count.value++}>      Clicked {count.value} times    </button>  )}const root = document.getElementById('root')if (root) render(() => <App />, root)

render returns a function that removes what it rendered. Run the app with vite as before, and check it with npx weft check.

A few things to know about this setup:

  • weft dev, weft build and weft start are for @weft-js/start apps. They need src/routes. A Vite-only app keeps using vite and vite build.
  • weft check, weft fix, weft tsc and weft docs all work. With no weft.config.ts, weft check reads every .tsx and .jsx file in the project.
  • Your vite scripts get P002 warnings, since they aren't weft commands. That's advice for agents, not an error: move them into tasks in a weft.config.ts when you want them listed by weft tasks.
  • In dev, the plugin adds weft's devtools to every page, so weft browser and the in-page agent work here too.
  • A production build drops CSS nothing can reach. weft({ purge: { keep: ['knob'] } }) keeps a name only built at run time, and weft({ purge: false }) turns it off.

Coming from React

Most of what you write in TSX carries over. weft accepts React's prop names on native elements: className, htmlFor, tabIndex, camelCase SVG attributes, a style object, and onChange firing on every keystroke as it does in React. The From React reference lists each one.

What doesn't carry over is the model underneath. A component runs once, state is a box you read with .value, and the compiler wires each read to the DOM. So the habits that come with hooks have no place. weft check reports most of them; the hooks themselves are in the catalog but not reported yet, so look for those yourself:

A React habitWhat weft reportsThe weft way
useState, useEffect, useMemo, useRef, useCallback, a custom useX() hook, a dependency arrayW020–W029 (in the catalog, not reported yet)state, watch, derived, ref={el}, plain functions
useState(props.value) written as state(props.value)W005: the box never follows the propstate.linked(() => props.value)
Computing from a prop in the component bodyW001: the body runs oncederived(() => …), or compute in the JSX
{count} for a state valueW015: that renders the box{count.value}
data-open={true} on an elementW043: weft writes data-open="", not "true"data-open for a flag, data-open="true" for the word
key={crypto.randomUUID()}W044: the key differs on server and clientA stable id from the data
bind:value on an <input>W061: bind: is for component propsvalue and onChange

Thinking in weft explains the model, and Components run once shows why the body isn't a render function.

Deep diveWhy weft keeps React's prop names

Code that agents and people write is full of React's names, and most of them have one obvious meaning. Rejecting className would only add a step. The compiler resolves the names it can see while it compiles, so they cost nothing at run time, and a {...spread} resolves its keys only in an app that has one. Hooks are different: they assume a component re-runs, and in weft it doesn't, so a hook would silently do the wrong thing. That's why one is an error and the other just works.

Recap

  • local.mjs new my-app makes an app from a checkout today; npm create @weft-js will once weft is published. Both take the same flags, and --yes takes every default.
  • src/routes is your pages, src/server your server functions, and .server.ts files never reach the browser.
  • weft dev runs the app, weft check checks it, weft build builds it and weft start runs the build.
  • Node, Bun and Deno all work: runtime in the config picks what the build targets.
  • A Vite app can use weft alone: the weft() plugin, jsxImportSource, and render from @weft-js/core/dom.
  • Rust is only for code you choose to write in Rust.

Next, set up your editor and TypeScript in Editor and TypeScript setup.