Skip to content

v4.1.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 13:15
· 279 commits to master since this release

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),
    then list.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=ar opens a page in that locale — the address the build's
    hreflang alternates always pointed at.
  • Every standard DOM event in on …: dblclick, pointerdown,
    scroll, paste, drop and 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 external calls it,
    so Chart.js and most modern libraries work through external + Host.
  • wf init writes a .gitignore; wf generate page gives 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 a derived before it existed.
  • Text("{rows.state}") showed a signal's source; a resource's state,
    data, error and a socket's messages are now read as their values.
  • navigator and location compiled as signals.
  • app { state … } never declared its state.
  • now(every: 1.seconds) called a now nothing declared.
  • An action handed over as a value, addEventListener("scroll", track), was
    read as a signal.
  • A type an external declares could not annotate a value.
  • Backend.avatar.progress was 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.
  • S04 never reported two pages on one route; A12 counted an h1 in each
    branch of an if as 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 — finds wf-lsp.exe on Windows, and
    downloads the server from the latest release when it is not on PATH.

Full Changelog: v4.0.1...v4.1.0