Repository navigation
v4.1.0
Security
A private env value no longer reaches the bundle
4.0 made a page that reads a non-public env name a compile error — and
then wrote the whole env map into app.js as const env = {…} anyway, so
a value like STRIPE_SECRET in webfluent.app.json was downloaded by every
reader. The bundle now carries only the names a page may read: those
beginning PUBLIC_ and those public_env lists. If a site built with 4.0
or 4.0.1 had a secret in env, rotate it.
Added
Offline
Name offline in the config and wf build writes a service worker that
stores the site for when the network is gone — the shell, the routes
precache names with their own chunks, and a fallback page for the rest —
and fetches what it did not write by the policies cache names. It is
versioned by a hash of everything the build wrote, so a deploy that changes a
byte is a new version; the page reads update.available and calls
update.apply(), which takes it and reloads once. With sync, a write
made with no network is kept in IndexedDB and sent in order when the
connection returns — by Background Sync where the browser has it, so a
closed tab still sends it — and network.queued says how many wait. Under
wf serve the worker takes itself away.
It is held to a real Chrome in CI (tests/browser/offline.mjs, 24 checks
across a pre-rendered and a single-page build): the site stored, a stored
route served with the server gone and the fallback for one that is not, a
write kept, counted and received exactly once, and an update offered and
taken with one reload.
Peers
peer link = rtc(signal: …) opens a WebRTC data channel straight to another
page, with the handle and the match of a socket — connecting, open,
closed(c), error(e), .send, .messages, .last. The language supplies
the channel, not a server: the offer, the answer and the routes each side
can be reached by go out through signal:, over whatever the app already
has, and what the other side sent is handed to link.signal(m). One side is
the initiator; ice: names STUN or TURN servers. A page that leaves
closes its peer, and the other side reads closed at once. Held to two
pages in a real Chrome in CI (tests/browser/peer.mjs).
The documentation, rewritten
The guide is 48 chapters in six parts — Start, the basics, Building,
Shipping, Reference and Help — with a tutorial that ends in a deployed
site, chapters for forms, real-time, offline, accessibility, testing,
deploying, environments, performance and JavaScript interop, and reference
chapters for every command, config key, diagnostic, built-in, design token,
runtime function and keyword. It is held to the compiler: every example is
compiled, every page example is run and clicked through, every diagnostic
example draws its code, every link lands, and a test fails when the language
has something the guide does not name.
A name nothing declares is a compile error
Button(huge) and on click { cuont = count + 1 } built, and threw a
ReferenceError in the browser the first time the page ran. A name that no
state, const, prop, action, store, api, external or browser global
declares is now T13, reported where it is written, with the spelling to
check. A template's names are its data's, so wf render is not held to it.
wf render is held to what a build is
A template that named a component nothing declares rendered
<!-- unknown component -->, and a flag a component does not take was
dropped, both with exit status 0 — an invoice with a typo went out that way.
The render now stops on everything a build stops on except T13. It also
takes --token NAME=VALUE for a design token over the theme's, reads an
indented .wfx template, and finds a data file beside the template, as
the guide always said it did.
The Node binding, as webfluent on npm
npm install webfluent — a thin wrapper around wf render, at the
compiler's version, published by the release once the repository has an npm
token. withTokens() reached nothing before and does now; withTheme() is
only sent when set, instead of naming a default theme; renderSlides() is
new; a template that does not compile throws what wf said. Its tests run
in CI against the wf the job built.
A title that looks its entry up
title: "{posts.find(p => p.slug == slug)?.title ?? slug} — Blog" was
written into every file as those characters: only a bare parameter name was
filled in. A title may splice any expression over the route's parameters
and the program's constants and data; the build works it out for each file
and the router again on each visit.
env from a .env file and the shell
env.NAME reads the config's env map, then a .env file beside it, then
the shell — so a pipeline can give staging and production their own values
without editing a file. The shell supplies a name the config or .env
declares, or a public one.
A config key nothing reads is reported
"defaultLocale" in webfluent.app.json was dropped without a word; the
build now warns and names the key it meant (default_locale). wf init -t static itself wrote the misspelling, and so did the documentation site.
And
- Pagination:
resource list = Api.items(page: page, paginate: .page),
thenlist.items,list.loadMore(),list.hasMore. The compiler emitted
the option and the checker refused it; the runtime now has it. - A route's parameters in its title:
title: "{slug} — Blog"gives each
pre-rendered file, and the live tab, that route's value. ?lang=aropens a page in that locale — the address the build's
hreflangalternates always pointed at.- Every standard DOM event in
on …:dblclick,pointerdown,
scroll,paste,dropand the rest, without a warning. on open { }on a socket, as a peer and a channel had it.- A class a module exports is constructed when an
externalcalls it,
so Chart.js and most modern libraries work throughexternal+Host. wf initwrites a.gitignore;wf generate pagegives the page a
description.
Fixed
The favicon on a pre-rendered page
meta.favicon was never linked from a pre-rendered page, and the
single-page shell linked it as written, so under a base_path it pointed at
the host's root. It is linked from every page, resolved like any other
asset, and typed so an SVG icon is read as one. meta.touch_icon is new:
the 180×180 PNG a phone puts on its home screen, which iOS reads instead of
an SVG.
A sidebar's items on a phone, and in Arabic
A Sidebar is a column of fixed height that scrolls, and its items shrank
to fit instead: with more than a screenful, every link was squeezed to 18px,
too small to tap. They keep their size and the column scrolls. On a phone a
closed sidebar waits off its own edge — in a right-to-left page it slid the
wrong way and covered half the screen.
An action's parameters
action move(by: Number) { n = n + by } on a page or a component compiled
by as a signal, _by(), which does not exist: every such action threw the
first time it ran. A store's actions were never affected.
What the guide's examples found
Writing every example out as a real page and running it found programs the
checks accepted and the browser rejected:
head { meta(content: post?.title) }read aderivedbefore it existed.Text("{rows.state}")showed a signal's source; a resource'sstate,
data,errorand a socket'smessagesare now read as their values.navigatorandlocationcompiled as signals.app { state … }never declared its state.now(every: 1.seconds)called anownothing declared.- An action handed over as a value,
addEventListener("scroll", track), was
read as a signal. - A type an
externaldeclares could not annotate a value. Backend.avatar.progresswas refused by the checker.- Two declarations on one line —
theme T { color-primary: #0F766E radius-md: 14px }— were read as one, setting the first token to both. S04never reported two pages on one route;A12counted anh1in each
branch of anifas two.- A test that clicks failed in any project with a dark theme.
- The CSP check took a code sample's
onClick=for an attribute. - Warnings printed as
Warning: Error: …. - The Node binding did not look where the installer puts
wf.
A site_url with the subpath in it
meta.site_url written as the site's whole address — https://x.github.io/docs
beside a base_path of /docs — gave every canonical link, link preview and
sitemap entry the base path twice: /docs/docs/. It is read once now, so
either spelling means one address.
A browser value in text
Text("{viewport.width}"), a clock on now, network.online — any text
that read one of the browser's values was computed once and never moved.
Only conditions were drawn live: the check that decides whether a text
follows its values knew signals and translations and none of these.
text.lines() on a live page
Every runtime module shares one closure, and the network module declared a
lines of its own — the reader for a streamed response. Wherever that
module was built in, the later declaration won: text.lines() handed back
an async generator, so the static paint showed the lines and the live page
replaced them with undefined. The stream reader has its own name now.
Escapes in a splice
A string written inside a splice keeps its own escapes:
"n={("a\"b").length}" refused to build, because the escape was undone
before the splice was read. And the older spelling {a ?? \"x\"}, which
the compiler has always said it accepts, was shown as text rather than
read as a splice.
url.query() and url.with(…) paint at build time
They painted nothing until the script ran. The static paint now computes
both as the browser's URL does — values decoded, a key replaced where it
stood, the search written back form-encoded.
Held to each other
Behind these, three checks: every method's value at build time is compared
with the runtime's, in Node, for 61 samples — anything the build cannot
know says why; every runtime function the compiler can call must exist;
and no two runtime modules may declare the same name.
The documentation site
Written for a phone first: every rule is the phone's, and md, lg and
xl add to it. The search is in the phone's menu, every link and control a
reader taps is at least 24px (44px in a drawer), a wide table or code block
scrolls inside its own box, and no page scrolls sideways at any width from
320px to 1920px, in English or Arabic — each checked on every route in a
real browser. It has a favicon, a home-screen icon and a link-preview card,
drawn as SVG in site/art/ and exported by scripts/site-art.sh. The search index is its own file, fetched the first time the
search box is used, instead of 20 kB carried by every page.
Editors
- Zed: the grammar reads WebFluent 4 —
api, connections,validate,
refined types, the language's literals — where it used to mark them as
errors, and a call with a bare-name argument (remove(i)) no longer
fails to parse. It is held to every example in the guide. A string that
begins"/// …"— a sample of code — is text, not a doc comment. - Neovim, Helix and Emacs: the guide's CLI chapter has the configuration
for each. - VS Code 0.3.0: attached to this release as a
.vsix. It is bundled —
the 0.2.0 package could not start — findswf-lsp.exeon Windows, and
downloads the server from the latest release when it is not onPATH.
Full Changelog: v4.0.1...v4.1.0