@opentf/esdev-plugin-afw
v0.9.0
Published
Compile AFW components and serve file-based routes in esdev.
Maintainers
Readme
@opentf/esdev-plugin-afw
Compile AFW components and serve file-based routes in esdev
An Open Tech Foundation project
One line in
esdev.jsongives esdev the AFW compiler:.jsx,.tsx,.mdxand.mdcompile on import inesdev start,esdev buildandesdev test, andapp/becomes a route map.
{
"plugins": ["@opentf/esdev-plugin-afw"],
"build": {
"targets": {
"web": { "entry": "index.html", "outdir": "dist" }
}
}
}// entry.js, loaded by index.html next to an #app element
import { mountApp } from "@opentf/afw";
import { pages, guard, loaderRoutes } from "@afw/routes";
mountApp({ pages, guard, loaders: loaderRoutes, target: document.getElementById("app") });@afw/routes is generated from app/**/{page,layout,404}.{jsx,tsx,mdx,md}, so
adding a file adds a route. CSS, Tailwind and assets go through esdev's own pipeline.
[!NOTE] Alpha. Options can change between minor versions; see the changelog.
Install
pnpm add @opentf/afw
pnpm add -D @opentf/esdev-plugin-afwRequires esdev 0.16 or newer (0.17+ for development refresh). The afwc compiler
comes from @opentf/afw-compiler,
installed as a dependency. esdev create my-app --template=spa generates a project
already configured this way.
Rendering modes
Pass options to pick how pages are rendered:
{
"plugins": [{
"module": "@opentf/esdev-plugin-afw",
"options": { "mode": "ssg", "appDir": "app" }
}]
}The plugin chooses a compiler backend per module from the build's context:
| Context | mode: "spa" (default) | mode: "ssg" / "ssr" |
| --- | --- | --- |
| Server build, esdev start, or a script | ssg | ssg |
| Browser release build | csr | hydrate |
| Browser development or DOM test | csr | csr |
Server builds import framework helpers from @opentf/afw/server, which renders
HTML without browser Custom Elements, so a server target can point straight at
server.js.
Options
| Option | Default | Purpose |
| --- | --- | --- |
| mode | "spa" | spa, ssg or ssr; selects backends as in the table above |
| target | — | Force one backend: csr, ssg or hydrate |
| appDir | "app" | Route folder. A bare name is found next to the importing entry; a path such as "website/app" resolves from the working directory |
| exclude | [] | Folder names to leave out of the route map |
| routes | true | false turns off @afw/routes, for libraries |
| failOnError | true | false replaces a broken module with a diagnostic stub instead of failing the build |
Server
The plugin generates @afw/server for a server target: one Fetch handler that runs the
app's middleware around API routes, server functions
(*.server.js) and loader data, then your files and renderPage steps.
// server.js — a server target, run by `esdev start` with "dev": { "run": "server" }
import { serve } from "runtime:http";
import { createHandler, createPageRenderer } from "@afw/server";
const handle = createHandler({ files: serveStatic, renderPage: createPageRenderer({ shell }), fallback });
serve({ port: 3000 }, handle);createPageRenderer({ shell })exists withmode: "ssr": pages render per request.- Browser builds get fetch stubs for
*.server.jsmodules; server builds keep them. - See Server Setup for a complete entry with static files.
Development refresh
In esdev start, an edited component or page refreshes in place: the host, props
and named $state values survive, while effects and lifecycle hooks are disposed
and re-run. Components rendered by the edited page, layout or component keep their
$state too, matched by tag and position. A page's metadata and headings (toc) refresh with it, and an edited
$expose method is re-installed on the same host. A compile error keeps the previous
view on screen until the fix is saved.
Changes the refresh cannot apply safely fall back to a reload: renamed components,
changed props or $state declarations, a renamed or removed exposed method, and an
edit to a module's other exports (export const SIZES = …), whose importers hold the
old value. Editing only the components in such a module still refreshes in place.
Release, server and test output contains no refresh code.
Source maps
Compiled modules return a version-3 source map that esdev composes into the bundle. Statements, expressions and event callbacks keep their original JSX/TSX positions. Markdown maps to its generated JSX for now.
API
| Export | Purpose |
| --- | --- |
| default / createAfwAppPlugin(options) | Compiler plus @afw/routes and @afw/server: the factory esdev.json uses |
| createAfwServer({ appDir, exclude, getCompiler, pages }) | @afw/server and the .server stubs, for custom pipelines |
| createAfwPlugin({ target, mode, failOnError }) | Compiler only, for custom pipelines and test configs |
| createAfwRoutes({ appDir, exclude }) | Route map only |
| afwPlugin(afwc, { target, failOnError }) | Compiler plugin for a runtime:build script |
| resolveCompiler() | Find afwc: AFWC_BIN, then the packaged binary, then a local Cargo checkout |
| startCompilerServer(afwc, { sourceMap }) | A persistent afwc serve child: { compile, close } |
| closeCompilers() | Stop every compiler child started in this process |
| compileError(payload) | Turn a compiler error reply into an Error with .diag |
import { build } from "runtime:build";
import { afwPlugin, closeCompilers, resolveCompiler } from "@opentf/esdev-plugin-afw";
const { afwc } = await resolveCompiler();
const bundle = await build({ input: "app/page.jsx", plugins: [afwPlugin(afwc, { target: "csr" })] });
const { output } = await bundle.generate({ format: "esm" });
await bundle.close();
await closeCompilers();All modules compile through one long-lived afwc serve process per plugin
instance, so the binary starts once per build.
Documentation
| Doc | Purpose |
| --- | --- |
| Installation | Creating and configuring a project |
| Static generation | mode: "ssg" with a prerender step |
| Server rendering | mode: "ssr" with a server target |
| esdev plugins | How esdev loads and orders plugins |
License
MIT — see LICENSE.
