Skip to content

v3.4.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 14:41

A minor. If you are coming from 3.2.0 — which is everyone, because 3.3.0 was prepared in the
repository and never published
; everything written for it ships here — this release has three parts:
the documentation was rebuilt from the ground up, a handful of things the framework did in silence now
say something, and two compiler scans stopped being a way to hang your build.

The documentation was rebuilt, not edited

Every Learn page now has the same four things: what the thing is, something running you can press, the
situations you will actually meet, and what to do when it breaks. That last one was the gap — a page
could describe a happy path at length and carry a single warning at the end, which is not what using a
framework feels like.

  • Live demos in Learn went from 2 to 36. Not screenshots of code: components that run on the page.
  • Failure sections everywhere — all 19 Learn pages, all 41 UI pages, 45 Examples pages, both
    Reference guides — quoting the real message, not a paraphrase of it.
  • The generated API reference grew from 174 to 349 exports. The CDK's 102 and nine published entry
    points (runtime/serialize, /resume, /adopt, /graph, /server, /document, router/files,
    forms/schema, ui/testing) had no page at all.
  • Static generation now lists every reason a component can refuse to resume — eleven of them, with
    what to write instead. It named three, and four of the seven real refusals measured across 622
    components were caused by constructs it never mentioned.
  • The Performance page was removed. It compared by proxy against a standing rule not to, and its
    numbers came from a single dated run.

Three instruments keep it that way, and they are in CI: audit-docs.mjs (dead imports, API coverage,
template and CLI surface), audit-scenarios.mjs (per page: API named, messages shown, page shape), and
verify-prose / verify-markdown / verify-resume-reasons, which caught a broken markdown parser, one
spelling used two ways across 145 files, and eleven refusal reasons against three documented ones.

Things the framework used to do in silence

  • A URL that executes code reaching a link. href={{ url }} reads like any other attribute, so an
    author can write it without knowing that a javascript: value runs on click. Weave now says so and
    still sets the attribute — the value is normalized first, because a browser strips tabs and control
    characters before reading a scheme, which is the same bypass the <Icon> sanitizer had below.
  • An HTML entity in a template. A template is text: &mdash; reaches the reader as those seven
    characters. Ten of them went into three demos in one afternoon before anyone noticed.
  • {{ … }} inside <textarea> or <title>. A browser reads those as text, so the placeholder the
    runtime writes became six literal characters in the value and in the browser tab.
  • weave check reads your project's own .d.ts files, resolves on:x on a component to its onX
    prop, and --fix finishes the job in one run instead of one repair per invocation.

Two compiler scans could be made to hang on hostile input

Both read text whose length you control — a component's own script, and the prose inside a template —
and both backtracked polynomially. import followed by 8,000 spaces and no from took 59 seconds
inside the compiler's import scan; 16,000 did not finish in two minutes. 120 KB of @A( took 5.7
seconds inside the text lint. One file was enough to stall a build or an editor.

Both are fixed and held by a gate that asserts the same inputs complete in milliseconds. Reported by
GitHub code scanning as js/polynomial-redos. No action needed on your side — nothing about the
API or the output changed.

Testing your components

@weave-framework/ui/testing gives you mount, press, click, overlay(), focused() and tick
the parts of driving a Weave component that are awkward to write yourself, chiefly finding what a
dialog or menu rendered into the overlay container rather than under your element.

mount derives which names your template reads, so a component that gains a binding does not also need
its tests edited. There is no query language and no assertions; your test runner has those.

Two things that quietly cost you resumability

Neither was a feature you asked for and both were taxes on writing ordinary code.

Omitting setup's return. The compiler writes that return for you — but the resumable analysis read
the raw file, so a component relying on it looked like it returned nothing, and a use: action could
not be shown to survive to the client. The subtree was client-rendered instead.

A comment inside the returned object. The reader that works out what a component hands out gave up
on any return containing a note beside a value, with the same consequence.

Together, across four real applications, they account for resumable refusals falling from 29 to 12.

Four security fixes

Found by auditing the codebase, not by a report — and none of them needed a novel trick.

  • A state snapshot could choose an object's prototype. deserialize assigned keys, and __proto__
    is not an ordinary key. The result was invisible: Object.keys and JSON.stringify showed nothing
    while a property nobody serialized read back as true. If your app stores or transmits state, this
    one mattered to you.
  • weave migrate could be made to run a command by the repository being migrated. Its install
    grammar allowed shell operators, because semver ranges use the same characters.
  • Two <Icon svg={…}> sanitizer bypasses — a scheme split by a tab, and a javascript: URL
    installed by an <animate> after the scrub.

Built assets carry their version in the name

main-<hash>.js and app-<hash>.css instead of main.js?v=…. A query busts a cache correctly, but a
file whose name never changes can never be served as immutable — every repeat visit still asks. The
split chunks were already named this way; the entry and the stylesheet have caught up.

If you referenced those filenames — a preload, a service worker, a CSP hash, a deploy rule — read
them from the emitted index.html, which is what the browser does.

The build tells you it did not type-check

weave check is the gate and weave build never ran it, so a build could succeed on code the checker
refuses — and say nothing. Now it says so, and weave build --check runs the checker first and writes
nothing if it finds errors.

The default is unchanged on purpose: making the check mandatory would turn a green pipeline red on
code nobody touched.

A component with typed props works everywhere a component is accepted

Component describes a component being called, where props are optional. Used for a parameter it
says the opposite of what is meant, and the ordinary case — a component whose template declares props —
was refused at five separate doors: a route, mountComponent (every app's bootstrap),
defineCustomElement, lazy's loading fallback, and renderComponent for SSR.

Two of those had already been fixed one at a time. Two real applications hitting the same shape on the
same day is what turned the third point fix into one shared type used at every entrance.

A component with typed props goes into a dialog

component(X, props) — how you put a component into a dialog or a sheet — refused every component that
declares typed props. Parameters are contravariant, so a component written as
(props: TheseProps) => Node was not assignable to the general Component type the helper asked for,
and the normal case failed to type-check at the exact call the documentation shows. It now accepts any
props shape, the same way lazy() already did for routed pages.

This was found by pointing weave check at a real 61-template application, where it was 57 of the 59
errors
reported. Nothing in this repository could have found it: no template here puts a typed
component inside an imperative overlay.

Also from that run: <Icon> now takes a class, which thirty of the other forty-five components
already did.

A malformed template tells you where, instead of crashing

Two shapes made the parser fail without saying anything you could act on. Thousands of unclosed tags or
blocks overflowed the stack — Maximum call stack size exceeded, about a file, with no position. And an
unterminated <!-- quietly swallowed the rest of the file, surfacing as Empty template fragment: a
true sentence about the wrong thing.

Both are now located errors. Nesting is capped at 500 levels, a bound measured rather than guessed — the
stack gives out around 2,500, and the deepest template in the Weave repository nests 25.

A third shape joined them here: an empty block body. @if (cond) { } — and any template a stray }
turns into one, because the brace closes the block before its content — threw the same
Empty template fragment, so the message described fragments while the mistake was a brace. It now
carries the offset of the block it is about.

A two-way binding declares its signal

weave check --fix and the editor lightbulb could already declare a name your template asks for, when
the markup left exactly one answer — a name bound to on:click can only be () => void. bind: now
qualifies too, and for the same kind of reason rather than a looser one: the runtime writes a specific
type BACK into the signal, and your markup settles which one.

you wrote you get
bind:checked={{ done }} const done = signal(false);
bind:value={{ age }} on type="number" or range const age = signal(0);
bind:value={{ tags }} on <select multiple> const tags = signal<string[]>([]);
bind:value={{ name }} anywhere else const name = signal('');

The signal import comes with it, in the same single edit — joining your existing
@weave-framework/runtime import if you have one, opening one if you do not.

Two shapes are still refused, and the refusals are deliberate. bind:group writes back in whatever type
the signal already holds, so a fresh declaration has no forced type at all; an <input> whose type is
itself a binding is a string one render and a number the next. {{ total }} and @for (t of items())
stay refused for the same reason they always were: an element type of unknown makes every use of t
an error, which is worse than silence.

The editor offers it in a .weave too

An SFC keeps its script inside the same file, so the edit had to be shifted by where that script begins.
Without the shift the lightbulb simply declined, and every .weave author was left with
weave check --fix in the terminal. Both authoring forms now behave the same.

Editor plugins: VS Code 0.6.7, WebStorm 0.23.7.

Upgrading

Nothing to do. No API moved, no behaviour of a running app changed, and weave check reports the same
set of things it reported in 3.2.0 — it can now repair more of them.