This chapter tells you what each release asks of your project when you
upgrade, newest first. Most releases need nothing from you; the ones that
do say exactly what to change, and wf check points at every place.
The full notes for every release are in
RELEASE_NOTES.md. This chapter keeps only what an
upgrade asks of you.
In this chapter:
- Install a newer version
- Upgrade a project
- The notes for each release, from 5.12 back to WebFluent 2
| Release | What to do |
|---|---|
| 5.12 | Run wf check (E118, E102); rename a store member called valueOf or toString |
| 5.11 | Run wf check (C07, T22), then wf test --update once you have read the new snapshots |
| 5.10 | Run wf check and wf test --update; check Column spans, Menu labels and bound checkboxes with their own on change |
| 5.9 | Separate style declarations with a newline or ;, never , |
| 5.8 | Nothing; a state that reads a live value may now warn X06 |
| 5.7, 5.6.2, 5.6.1 | Nothing |
| 5.6 | Rename any declaration that takes a name the language has (E118) |
| 5.5 | Handle null where an OpenAPI value may be null (T04) |
| 5.0 | Run wf check and fix, or lower, the new errors |
| 4.2 | Run wf migrate to replace external; check which .js files under src/ now ship |
| 4.1 | Nothing; rotate any secret a 4.0 build put in env |
| 4.0 | Run wf migrate, and read the list of names it makes public |
| 3.x | The current grammar; nothing beyond 4.0 |
| WebFluent 2 | Run wf migrate |
To upgrade the compiler, run the installer again — it replaces the binary in place — or, if you installed with Rust:
cargo install webfluent --forcewf --version says which version you have. Update wf-lsp and the editor
extension at the same time: the language server is built from the same
compiler, and an older one reports what the new wf does not
(Troubleshooting).
After installing, bring each project forward:
- Read this chapter's notes for every release between the version the project last built with and the new one.
- Run
wf check. It writes nothing, and every finding names its code, its place and what to do. - Run
wf test. If snapshots changed, look at each difference, then runwf test --update. - Build, and load the result with
wf verify.
5.12 adds names in any script and translation by a project's own words, and makes every name a program binds its own. Your project may notice these:
- A store member may not be named like what every object has.
valueOf,toString,constructorand the other names every JavaScript object has areE118as a store's member, because the page threw as soon as it drew one. Rename the member. The same names now work as a component's props;__proto__as a prop isE118. - A test file may not declare a name the project declares. The two
were one name on the test's page, and every browser test failed with a
SyntaxError. It is now
E102at the test's declaration. Rename the test's own. T06is reported at the member's name. "Storehas no memberx" underlinesx, notStore. A tool that reads the line and column sees the new place.output_type: "static"pre-renders every page, asssg: truedoes. It used to build a single-page app. A project that set it and relied on that sets"spa"instead.- WebP is lossy at
build.media.quality, and a file no smaller than its original is not written. Pictures are smaller; a width that saved nothing is served from the original. - A name that clashed now works. A program's own name that was one of the generated code's, or a browser global's, used to read the wrong value; it reads its own now. A page that leaned on the wrong value changes.
Run wf check, then wf test.
5.11 adds to the language and fixes what an editor built on it found. Your project may notice these:
- A gesture on a component needs one element to land on.
drag:,dropValue:,on receive(d),on longpressand the rest, written on a component's call, used to throw at run time. They now land on the component's root element. A component whose body is anif, afor, amatchor several elements has no root, and the call is the new errorC07. To fix it, wrap the body in oneStackorRow. - A handler names at most what its event sends.
on menu(key, x, y)now reads each value that a component'sevent menu(…)sends. A name past the last one isT22. A DOM event sends only the event, soon click(e, x)isT22too. - A test file's declarations are its own. A
const, a component or a type written beside the tests in one file undertests/is no longer seen by another file's tests. Two files may now declare the same name. A file that used another file'sconstdeclares it itself, or moves it undersrc/. - A test names a control as a reader hears it. What is
aria-hidden(a menu item's shortcut, a glyph) is not part of the name, and a submenu's entry is its label without its items.click "Paste Ctrl+V"is nowclick "Paste", andclick "File"opens aMenu.Sub(label: "File").A15no longer counts anaria-hiddenglyph as part of a control's visible label either. - A block handed to a component reads the names where it was written
in
wf testand the template engine, as the live page and the pre-rendered page already did. A snapshot that showed the inner component's value of a shared name changes. - A
Menu.Item'sclass:is added beside the menu's own, where it used to replace it, so the item keeps its padding and its shortcut column. A style that made up for the lost class can go. - A method of a receiver's own wins over a helper of the same name.
x.last(k)on a value with alastmethod of its own calls that method with every argument, where it used to call the list helper. A store's action named like a helper (preview,first,take…), called from another store, runs that action.
Run wf check, then wf test --update once you have read what the new
snapshots show.
5.10 makes pages, pre-rendered pages and tests do what this guide says.
Your project may notice the changes below. Run wf check after upgrading.
- Column spans leave room for the gap. A
Columnin aRowwith a gap is narrower by its share of the gap, and spans that add up to 12 sit on one line instead of wrapping the last. A layout that made up for the old behaviour, such asspan: 8besidespan: 3, now leaves a space; give it back its4. The spans read the gap from--wf-gap, whichgap:sets, so a gap your own CSS gives aRow(style { gap: 4px }) needs--wf-gap: 4pxbeside it. A PDF now lays aColumnout at itsspan:, and reads acalc()of a percentage and points against the box it lands in, as a browser does. - The
Navbartakes the theme's surface and text colours, so a dark or custom theme no longer gets a white bar. - A keyed list slides only the items whose index changes. An item that keeps its index while its place on screen moves now jumps there.
- A
Splitter'sreset:is written as given, not clamped tomin:andmax:, and the first drag or key press moves from the edge as drawn.
- A
Menu'slabel:names its list, not its button. The button says whattrigger:says.Menu(label: "File")with neithertrigger:norvisible:now draws a button that says Menu; writetrigger: "File". A menu thatvisible:opens no longer draws a button. ADropdown'slabel:is still its button. - A
Formwithoutbind:asks thevalidaterules of its page or component beforeon submit, and shows their messages instead of submitting while one fails. Each copy of a component withForm(bind: …)has its own form, where all its copies shared one. - A bound
CheckboxorRadiowith its ownon changewrites its state first. The binding's write used to be lost, so a handler that wrote the state itself now writes it a second time, and one that toggles it (agreed = !agreed) undoes the click: take that write out. ASwitch'son change, which never ran, now runs. - Text that reads state follows it. Control labels, a
Table'scaption:, tab titles, tooltip text, a modal'stitle:, a menu's trigger, anAvatar, aSkeleton's sizes and anIcon's positional name kept the value they were drawn with; they now change with it. ACheckbox(label: t("…"))changes language withsetLocale. - An
Avatarwhosesrc:is a run-timedata:orblob:address gets an emptysrc, as any othersrc:does. - Built-ins write what they are given.
role:,tabindex:,title:,hidden:,aria-*,data-*andonhandlers on aVirtualList,Splitter,Tabs,Sidebar,Breadcrumb,Slider,DatePicker,FileUpload, the layers and the rest now reach their element, where some were dropped.label:on aVirtualList,SplitterorCarouselis written as itsaria-label, not as text in it.disabled: trueon aSlider,DatePickerorFileUploaddisables it. ASidebar'sid:is its id. Pre-rendered HTML andwf testsnapshots carry these too: look at the difference and runwf test --update.
- The Escape that closes an open
ModalorDialogstays there: a page'son key("Escape")no longer runs for it. - An anchored layer hides with its anchor when the anchor scrolls out
of view, and stays open. Style it through
[data-anchor-hidden]if you need to. - A modal's Escape and title. Over an open popover, the first Escape closes the modal and the next closes the popover. A press inside the modal no longer closes the popover beneath it. In the pre-rendered page, a modal's title is its heading. A tooltip under the pointer lets Escape go on to the page.
setLocaleis remembered. The choice is kept in the reader'slocalStorageunderwf:locale, and the next page opens in it; a?lang=address still wins. Apersiststore kept for the same purpose still works beside it.- Stores: a store's derived values keep following their state after
the first page that read the store has gone, and one that reads a
derivedwritten below it follows it. The built script declares a store after the stores ituses. - A local of a member's name is the action's own. A store action that
wrote a member through a
letof the same name now writes the local. data-x: truepassed through a component is"true", where it was"", so a selector on[data-x=""]stops matching.Portalis a built-in's name: a project's own component or store calledPortalis reported.- Markdown reads backslash escapes. A text with
\*,\_,\#,\[and the like now shows the character alone, and the character takes no part in emphasis or a link;\\is one backslash. A backslash at the end of a paragraph's last line is shown, where it was dropped. A link or picture whose address has a scheme a page refuses (javascript:,data:) is drawn as its words. - Fewer warnings and errors: an
Iconnaming one of the 23 new icons draws it instead of warningV08;on auxclick,on focusinandon focusoutno longer warnV09;on blurandon focusoutside an element compile, where they were an error; and() => valuefits a function of no arguments, where it wasT01.
- A site that frames its own page, with a
Framewhosesrc:is a site-relative string ("/preview/","/embed/{id}"), is servedX-Frame-Options: SAMEORIGINandframe-ancestors 'self'. One whose same-site address comes from a variable is still servedDENY, so the browser still refuses it. Write the address as a string, such as"/embed/{id}".
- Pre-rendered HTML matches the live page. A
MenuorDropdownis its button and a hidden list, so its entries no longer show before the script runs, and a menu that state opens is its list alone. ACheckbox,RadioorSwitchhas its<input>, with the control'sid:on it. A part written with flags carries their classes. AColumnwith a literalspan:carrieswf-col--N.disabled,requiredandcontrolsare left off when false, or when the build cannot know them. wf testsnapshots change where they hold aMenu, aDropdown, aColumnwith aspan:, akeyLabel, a count in a locale with more plural forms than English, or adisabled:,required:orcontrols:that is false: look at the difference and runwf test --update. Anexpectfor aMenu'slabel:text no longer finds it (it is anaria-label), and one for itstrigger:does.expectreads'as an apostrophe, and in a test that acts compares with white space collapsed. Aclickmay now find a tab, a menu item or a radio button it used to miss.- Tests read as a reader does. An
expectthat counted on a space between two elements that touch ("live : …") now reads them touching. A test run in a browser has the project's own CSS, so CSS that hides or changes text changes what the test sees, and a stylesheet naming a file not inpublic/fails it. Snapshots losedata-*attributes that werefalse, and gain a call'sdata-*,aria-*,idandclass:on a component's root. Runwf test --updateafter looking at the difference.
- On Windows, type
webfluent(orwf.exe) wherever this guide sayswf. Each earlier.msihad anUpgradeCodeof its own, so the new one cannot replace it: uninstall the old one from Apps, or both stay installed and onPATH. - A tool built on the compiler adds the new fields where it writes a
struct literal:
doconStateDecl,DerivedDeclandActionDecl,frames_itselfonBuildConfig,only_routeonBuildOptionsandpages_deferredonBuildReport. It adds aLength::Mix(..)arm where it matchespaged::style::Length, and passes a locale totesting::render_testas its fourth argument.print::statementtext for a documented member starts at its///, so replace fromprint::written_start.check_project_viastarts threads, and calls aProject'sfile_ofandsource_ofup front.
5.9 holds a few things to what this guide already said. Your project may notice them:
- A comma between two style declarations is an error (
E002).style { width: {w}px, flex: none }compiled to one declaration whose value was the rest of the line, so the browser dropped both. Write each declaration on its own line, or separate them with;. A comma inside a value (font-family: a, b) is untouched. - A store's
persist … key:is kept. It was dropped, so the value was kept under no key; now it follows the key, as a page's does. What is already stored is found under the same names. - A refined type's bounds are checked as the reader types where a
validateblock guards the state:Number(0..10)no longer takes 10, andDate(after: …)andbefore:are held. A field that accepted its open end now says Too large. Card(animate: .Drop)plays a project's own animation on an element that stands still. It did nothing before, so a page may now move where it did not.- A count of exactly 0 shows
key.zerowherever the translation file has one, in any language, and the pre-rendered page picks each count's form by the default locale's rules rather than English's. Azeromessage that never showed now does. - A site whose default locale is right to left is written
<html lang="ar" dir="rtl">from the first paint.meta.langnames a region of the default locale's language, and is otherwise the default locale. - New warnings:
A17(aFramewith no title) andV12(a long list that is not windowed).wf checknames each with its fix.
Nothing to change.
- 5.8: a build may now warn
X06on astatewhose first value reads a live one, such asnow. Making itderivedis the fix. A function written only to get a script's value onto a page can go back to being the value. - 5.7: what a page did to live through a refresh can go — a sheet or a
form kept outside the
matchwith a snapshot of the data, or aderivedcopy held so the arm would not draw again. A page that wants a spinner on every refresh shows one forname.refreshing. A hand-writtenWF.matcharm is handed its value as before, and an accessor of it as a second argument;WF.eachkeeps a keyed item's nodes under a new value withlive: true, which is what a compiledfor … byasks for. - 5.6.2: a wrapper written only to keep a nested branch apart — a
Stackstyleddisplay: contentsaround a tab'smatch, or a branch flattened into oneif letover aderivedvalue — can go. - 5.6.1: a
derivedvalue written only to make a resource's label live can go back into the label.
5.6 refuses a name the language already has as a program's own
(E118): a
state env, a loop's t, a persist theme, an action named update.
Each compiled before, and read the language's value where the program
meant its own — a page's state env showed [object Object].
Run wf check. The message names the declaration and suggests a name of
your own; renaming it and its reads is the whole change. The usual
renames:
| Was | Rename to |
|---|---|
a loop's or a lambda's t (for t in todos, items.filter(t => …)) |
todo, item |
state query |
search |
persist theme |
scheme |
a route parameter or state env |
target, stage |
an action named update |
edit |
A value an OpenAPI specification marks as nullable is now T?, so a read
that ignored null — Text(e.reason.toUpperCase()) — is T04 where it
compiled before. Use ?., ?? or if let to say what to do when it is
null. A response field with a format is now a DateTime, a Url and
so on, and a String assigned to one is T01.
5.0 makes the compiler refuse much more of what used to compile and then fail in the browser. A project that built under 4.x may stop on errors it always had. Start with a check, which writes nothing:
wf checkEvery finding names its code, its place and what to do. wf explain CODE
prints the code's entry in Diagnostics. Where the fix
is known, your editor offers it as a quick fix, and wf check --format json lists it as edits.
New errors. Each is a program that cannot work as written:
- Types (
T): a field, member or method that does not exist; a value of the wrong type; a value that may benullread as if it were not; amatchthat misses a case; arithmetic on what is not a number; a comparison that is always the same; anawaitwhere it cannot run; a name nothing declares. - Writing what cannot change (
X01): aconst, aderivedvalue, a prop, a route parameter, a loop variable. - Components (
C): a required prop left out, a prop the component does not declare (C02), a part outside its owner. - Routes (
R): a link to a path no page matches (R01), a:paramwith no page parameter, anappwith noRouteror with two; two pages on one path (S04). - Forms (
F):bind:to something that cannot be written, or to a state the control cannot hold; avalidateno control can satisfy. - Reactivity (
X): an effect that writes what it reads every run, aderivedvalue that calls an action,useof a store nothing declares. - Data and translations (
D,I): a persisted value storage cannot keep; at("key")no translation has, a placeholder the call does not pass, a locale the project does not list. - Literals and styles: a date that does not exist, money with too many
decimals, a key combination no keyboard sends, a
$tokenthe theme does not declare. - The security policy: a page's
head { script(src: …) }from an origin the config does not declare (E902).
Lower what you cannot fix today. "lints": { "R01": "warn" } in the
config lowers a code or a family. An error may be lowered only when it
ships no broken page: R01, C02, S04, I01, I02, I04. To accept
one finding on one line, write // wf-allow(CODE)
(Diagnostics).
What the compiler now writes for you instead of reporting:
- A change inside a state's value —
form.name = v,items[i].done = v,items.sort()— compiles to the state given an updated copy, so what reads it repaints andpersistkeeps it. - A value spliced into an address —
to:,navigate(),fetch()— is encoded where it stands for a value. - A
matchexpression that covers every case of an enum needs noelse.
Behaviour that changed:
wf serveanswers a missing fetch with a 404. A request that is not a page navigation, for a file the build did not write, used to get the single-page shell — a page of HTML handed toJSON.parse. It is a404now, underwf verifyandwf testtoo, andwf serveprints a line for each. Point the page at a server that has the API, or put a file inpublic/.- An empty path parameter is not sent.
Backend.user(id: "")withat "users/:id"used to requestusers/, the whole collection. It now ends as.aborted, and a resource over it staysloading. - One list item that throws no longer blanks the list: the rest draws,
and under
wf servethe item says what it threw. - An effect that never settles is stopped after 100 runs, instead of overflowing the stack.
- Each build keeps what its persisted values start as in
.wf-cache/, so the next build can warn about a shape that changed (D04), andwf verify --returning-visitorcan load your pages as a returning reader. Keep.wf-cache/between CI runs if you can.
New in 5.0: wf check, wf explain, --format json|sarif|github and
--deny-warnings on wf build and wf check, quick fixes in the editor,
persist … { key: id } for one stored value per instance, and wf verify
visiting :param routes. Command line describes each.
external is gone. Run wf migrate. It turns a remote module into a
meta.scripts entry, a local module into a plain script under src/ with
its signatures as JSDoc, and an external element call into
Element("tag", …). A call into a remote module that external typed is
Any afterwards.
Every .js file under src/ now ships, linked on every page before
the compiled code. wf migrate lists the ones it finds; move any that are
not meant for the browser out of src/.
New in 4.2:
- Your own JavaScript: a plain
.jsfile undersrc/, its top-level functions in scope by name, typed by its JSDoc (JavaScript interop). meta.scriptsloads a library from a CDN, imports a module withas:, and names itsglobals.mount:,update:,cleanup:on any element, and awf:renderevent for a script that finds elements itself.Element("tag-name", …)places any custom element.class:takes a map and a list:{ "is-done": done },["card", tone, if open { "is-open" }]. Anifwith noelseisnullwhen its condition fails (Styling).V04warns about aclass:that borrows one of the built-ins'wf-classes.
Fixed in 4.2: what an if branch, a match arm, a list item or a slot
fill made — a timer, an effect, a Host's cleanup — now goes when its
page does. A route change used to leave it running.
Nothing to change in your code. New in 4.1:
- Offline: name
offlinein the config for a service worker, writes kept for later, and an update flow (Offline). - Peers:
peer link = rtc(signal: …), a WebRTC data channel to another page (Realtime). envfrom a.envfile and the shell, on top of the config (Environments).- Unknown config keys are reported, with the key they probably meant.
- Pagination:
paginate: .page,.items,.loadMore(),.hasMore. ?lang=opens a page in that locale, as thehreflangalternates promise.class-exported libraries work throughexternal: a class is constructed when called.- Every standard DOM event is accepted in
on ….
Fixed in 4.1, and worth knowing because they could have affected you:
Caution
A private env value no longer reaches the bundle. 4.0 refused a page
that read a private name, but still wrote the whole env map into
app.js. If you deployed 4.0 or 4.0.1 with a secret in env, rotate it.
- An action's parameters are its own:
action move(by: Number)readbyas a signal and threw. head { }tags may read a page'sderivedvalues.- A resource's
state,dataanderror, and a socket'smessages, show their values in text. navigatorandlocationcompile as the browser's.- Two declarations on one line —
theme T { color-primary: #0F766E radius-md: 14px }— are two, not one. S04reports two pages on one route;A12counts anh1in each branch of anifas one.
4.0 changed what the compiler allows. wf migrate handles the mechanical
part and names every place that needs a decision. Preview what it would
change, then apply it:
wf migrate --check
wf migrateenvis public or private. A page may only read a name beginningPUBLIC_or listed inpublic_env. The migration adds every name your pages already read topublic_envand prints the list. Read the list: public means anyone who opens the site can read the value.- An
on*attribute is an error (onclick: "…"); writeon click { }. - A URL a browser would run is refused —
javascript:,data:,vbscript:,blob:,file:inhref:,src:,to:,poster:ornavigate(). - Stores are built on first read, not at boot. A store whose set-up
must run anyway takes
eager: true. - A
persistvalue follows other tabs.sync: falseopts out. WF.storeandWF.hostwere renamed in the runtime's functions.- Layouts stay as written: a
Rowno longer turns into a column on a narrow screen by itself. The migration adds.stacksto keep the old behaviour.
WebFluent 3 introduced the grammar this guide describes: lowercase
declarations, one positional argument, flags after a dot, on click { }
handlers, and style { } blocks of CSS. A 3.x project moves forward with
the 4.0 notes above.
wf build refuses a WebFluent 2 file (Page Home (path: "/") { … }) and
points you to the migration. wf migrate rewrites every .wf under
src/ into the current grammar. It is a change of spelling only: the
migrated project builds to what it built before. wf migrate --wfx writes
the result in the indented layout.
Contributing: how the compiler is put together, how to build and test it, and how to change the language, a built-in or this guide.