CLI

The otfw command (from @opentf/web-cli) drives the toolchain. Projects expose it through package.json scripts (bun run dev, bun run build).

CommandDescription
otfw devStart the dev server with on-demand compile and reload-on-save (full-page refresh).
otfw buildCompile and bundle to dist/ — per-route lazy chunks under assets/.
otfw build --ssgBuild and pre-render every route to static HTML.
otfw serveBuild, then run the per-request SSR server (--port to override).
Shell
otfw dev            # development
otfw build --ssg    # production, static-rendered
otfw serve          # production SSR + API routes

Both otfw dev and otfw serve serve your API routes (route.{js,ts} files); otfw build emits the handler bundle to dist/server/api.js.

Scaffolding

Create a new project with the create initializer:

Shell
bun create @opentf/web my-app
npm create @opentf/web@latest my-app
pnpm create @opentf/web my-app
yarn create @opentf/web my-app

The scaffolder (@opentf/create-web) is interactive:

PromptOptions
Project typeSPA (browser-only) · Fullstack (browser + server) · Documentation site · Library — see Installation
LanguageJavaScript or TypeScript (.tsx pages, .ts API routes, tsconfig.json)
StylingPlain CSS or Tailwind CSS (SPA, Fullstack, and Library only)

Before writing files, it resolves every @opentf/* dependency in the generated package.json to ^<latest> from npm. Scaffolding aborts if the registry is unreachable. After the project is written, it runs <pm> install with the package manager that invoked the command.

Next steps

SPA, Fullstack, and Docs projects suggest bun run dev (or the equivalent for your package manager). Library projects suggest bun test — see Library template.

Dev server notes

  • Serves your public/ directory at the site root (e.g. /logo.png).

  • Compiles .css entries (including Tailwind) on request.

  • Surfaces compile errors in an overlay.

Reload on save (not module HMR)

otfw dev watches app/ and, on a supported file change, drops cached route chunks and signals the browser over /__hmr to location.reload() — a full tab refresh, not in-place hot module replacement.

Works on saveLimitations
app/**/*.{jsx,tsx,mdx,md,css} (existing routes)New page.* in a new folder may need a dev-server restart
Imported components, layouts, loadersPlain app/**/*.js (e.g. _meta.js) is not watched
route.js / loader.js API and data bundlesotfw.config.js and index.html need a restart

Module-level HMR (swap a changed module without reloading the page) is on the roadmap.