Skip to content

Releases: weave-framework/weave

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 30 Jul 22:20

One small addition, from a real screen that needed it. A minor: new optional surface with a safe
default, nothing existing touched.

tooltip takes a class, so one tooltip can look different from the rest. The bubble renders into the CDK
overlay container at the top of the document, not inside the component that asked for it — so component-scoped
CSS never reaches it, and neither does a token set on an ancestor of the host. Until now the only lever was
.weave-tooltip itself, which is every tooltip in the app.

The class lands on the same element that carries .weave-tooltip, so your rule sets that component's own
tokens and nothing else needs to know:

<span use:tooltip={{ { text: message(), class: 'tooltip-error' } }}>!</span>
.tooltip-error {
  --weave-tooltip-background: var(--weave-color-error);
  --weave-tooltip-text: var(--weave-color-surface);
}

A field's validation bubble reads as an error while the submit button's tooltip on the same screen stays
neutral. Omitting class leaves the panel exactly as before.

v2.2.1

Choose a tag to compare

@github-actions github-actions released this 30 Jul 18:18

A router fix, reported from a real app. A patch by the rule in
VERSIONING.md: behaviour moves toward what was documented and intended, and no API shape
changes — the behaviour being replaced was a blank page, not something an app could have worked against.

guard and redirect on path: '/' no longer fire for every other route. Matching is prefix-based and
/ — like an index child '' — compiles to zero segments, so it is a prefix of every URL at its level.
Policy was evaluated on such a candidate before anything checked whether a child consumed the remainder, so a
redirect meant for the index fired on unrelated paths, sent the router back to a path that matched the same
route again, and the 16-hop cap returned an empty chain: nothing rendered, nothing threw, nothing was
logged. A menu-driven route table with { path: '/', guard: () => '/login' } simply looked like a router that
does not work.

Resolution is now two passes — match structurally, then apply redirect/guard outside-in over the chain
that actually resulted — so a layout's auth check still decides before its child is consulted, and path: '/'
is usable both as the home route and as a root layout with children. The same change fixes a subtler case: a
route with children whose guard used to run for a sub-path none of them matched.

false still means "block this branch", and now does so at every depth: the blocked route is struck out and
matching runs again, so the next route matching that URL gets its turn. That is what the docs already
described; at the top level the code did not do it.

Two silent failures now speak. A redirect loop that exhausts its 16 hops logs the cycle it followed
(/users → /login → /users → …) instead of rendering nothing, and a matched route with no component — which
blanks the outlet and stops any nested <RouterView> from mounting — is named in a warning.

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 12:43

A feature release built around one large addition — a migration path into Weave — plus a crash in the reactive
core that any large list could reach. Everything is backward-compatible; 2.x code keeps working.

weave migrate — assisted migration of an Angular app into Weave (RFC 0011). Run it
from inside the Weave app you are migrating into. It reads your Angular project (only ever reads it), builds a
map of facts, writes a migration-plan.md you read before anything changes, and then — only if you say yes —
writes the converted code into src/. An existing file in the target is never overwritten.

It converts @Componentsetup() plus a sibling template · @Injectable({providedIn:'root'})store(),
and a scoped one → createContext + provide/inject · @Pipe → a plain function · @Directive → a use:
action · *ngIf/*ngFor/*ngSwitch@if/@for/@switch · [prop]/(event)/[(ngModel)]
.prop/on:/bind:value · <ng-template>/*ngTemplateOutlet@snippet/@render · <ng-content>
<slot> · reactive forms → @weave-framework/forms · route guards → beforeEach · HttpClient
@weave-framework/data · Angular Material → the Weave UI library. RxJS is translated, not described: chains
are rewritten by folding the operators over the shape of their source, so an app that finishes a migration is not
left importing rxjs. It is an assistant, not a magic button — everything it cannot do confidently is written
into the plan as needs you, with the reason.

<Table> gained a per-column filter row and a virtual body. headerRow={{ (col) => … }} renders a second
row inside <thead>, directly under the headers, each cell inheriting its column's width, alignment and sticky
treatment — the one place a filter row can live and stay aligned when a column auto-sizes. virtual renders only
the rows in view: first render of a 1000×20 grid was ~480 ms of build and ~850 ms laid out, and a viewport holds
20–40 rows however long the data is. It needs maxHeight and a uniform rowHeight; the two configurations it
cannot honour are refused at setup rather than left to show the wrong rows.

A signal written during a render no longer recurses per item. flush() guarded only against batching, so a
write that happened while the effect queue was draining started a second drain inside the effect still running.
Writing a signal from an effect is ordinary — ref={{ el }} alone does it, on every component that takes a ref —
so a long list of such components nested one stack frame per item and ended in RangeError: Maximum call stack size exceeded, with the render abandoned mid-list and the DOM left half-updated. Reported against a large
<Table> where a column change tipped it over. batch could not help and was measured not to.

A component's type now says it returns a Node. Both places that synthesize a component's default export —
weave check (and through it the editor plugin) and the built @weave-framework/ui .d.ts — said unknown, so
every imperative call site needed a cast. That is most of the composition surface: a <Table> column's cell, an
<Expansion> panel body, anything typed to take a Node. Pure narrowing, no runtime change.

Overlays follow their trigger when the page rearranges. A connected panel — a Select listbox, a Menu, an
Autocomplete panel, a Tooltip — only repositioned on window scroll/resize, so it stayed where it was placed when a
splitter was dragged, a sidebar collapsed, or a drawer animated open. It now observes the origin, its containing
block and the panel.

Also: a component library can declare a weave.config.ts (it needs one for styleLang, and resolving a
config used to reject anything without root/entry) · weave build no longer fails when publicDir is left at
its default, and no longer copies an undeclared project directory — sources, node_modules, .env — into dist/
· a comment between the pieces of a split template is no longer read as a non-static template · the ui
publish build no longer breaks on a component declaring propDefaults or extend.

Security. brace-expansion is pinned past CVE-2026-14257 (the pin's old target would have let a future
resolution land back on a vulnerable version), which also moved the VS Code extension to
vscode-languageclient@10 — raising its floor to VS Code ^1.91. Four code-scanning alerts are closed, one of
which was a gate quietly not gating: a word-boundary regex written inside a template literal accepted a different
symbol than the one it was checking for.

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 12:35

A feature release driven by real-app use, plus an internal refactor of the resume engine. Everything is
backward-compatible — 2.x code keeps working.

Input masking (use:mask, @weave-framework/ui/cdk). A headless CDK primitive that formats a text input
as the user types — phone, card, date templates, and a numeric mode for money that fills from the right,
groups thousands, and keeps a canonical decimal string as the value. Two fixes landed with it from the
dogfooding app: use:mask now binds the inner control of <Input> (not its wrapper <div>), and the numeric
mode exists because a positional template genuinely cannot carry an amount (RFC 0010 records why).

openDialog can host a live component. Pass [Component, props] (or component(C, p)) and the dialog
mounts it under an owner and disposes it on close — reactivity inside a dialog now works and the graph no
longer leaks. Node / string / factory content is unchanged.

App-wide date/time picker defaults. provideDateTimeDefaults() sets adapter, locale, first day of week,
display format, translated chrome and 12/24h once on the owner tree, instead of repeating them on every
<Datepicker> / <DateRangePicker> / <Timepicker>. Resolution is instance prop → context → built-in, and a
settings change now reaches an already-mounted field.

infiniteScroll in the CDK. A load-more sentinel (use:infiniteScroll) that requests the next page as the
list nears the end — never overlapping a request in flight, re-arming when a filter resets, and chaining even
when a page is too short to fill the viewport. Composes with virtualScroll (that decides what to render, this
when to fetch).

Resume engine — the adopt navigation is now one cursor program (internal). The client-side DOM adoption
that makes a resumed page interactive without re-rendering was rebuilt around a single sequential AdoptCursor
walk, replacing the old absolute build-time index math and its post-block special cases. No public API or
template behaviour changed and the non-resumable output is byte-for-byte identical; a @let after a block now
resumes instead of client-rendering, and a resumed page carries ~0.9 KB more JS (the navigation logic moved
into the runtime — a deliberate one-time cost that should fall when per-island splitting lands). A plain
client-only SPA is unaffected.

Also: a proxied stream in weave dev could take the whole dev server down — fixed. The one open axios
Dependabot alert (a transitive of the build tooling, never shipped) is closed via a scoped override.

v2.0.1

Choose a tag to compare

@github-actions github-actions released this 19 Jul 21:15

A same-day fix to 2.0.0: npm create weave was still scaffolding projects onto 1.x.

The template pinned its dependencies at ^1.0.0, and a caret range does not cross a major — so a brand-new
project created minutes after 2.0.0 shipped installed 1.8.0 and got none of it, including both security
fixes. Exactly the people least able to notice.

The ranges now follow the release major, and a gate (verify:template-ranges) fails the build if they ever
drift again. Nothing else changed.

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 21:01

A release made entirely of things that were already wrong. An external audit went through the whole
codebase and produced 24 defects; GitHub code scanning had 11 open alerts. All 35 are closed here, each
one reproduced before it was fixed and each fix pinned by a test that fails without it.

Why the major. Nothing was removed, nothing was renamed, no signature changed — code written against
1.x still compiles. But four fixes change how existing code behaves, and Weave's versioning promise says
plainly that a changed default behaviour is a major. Paying that cost now is the honest option; the
CHANGELOG lists all four at the top of the 2.0.0 section.

🔒 Two security fixes

A stored XSS in static builds. renderDocument wrote the page title, lang and the client-entry URL
into the HTML without escaping. A title is routinely derived from data — a route-title effect reading a CMS
record, a product name, a username — so a title containing a closing </title> followed by a script tag
became executable markup baked into every generated page, and it persisted there. Now escaped for the
position it lands in. head stays raw, because injecting markup is that option's documented purpose.

Code injection in the compiler's own output. The compiler builds JavaScript source by interpolating
template text into string literals, and quoted it with JSON.stringify. That is right for JSON and wrong
for code: JSON leaves </script> and U+2028/U+2029 raw. A template attribute holding </script> came
through verbatim, so the generated module terminated any script block it was inlined into. Three emit sites
bypassed the quoting helper entirely — including the one carrying the whole hoisted template.

🐛 The silent ones

The defects worth naming are the ones that produced no error at all.

A reactive update aimed at a running effect was dropped. A running computation is marked dirty for its
whole execution, and the invalidation check returned early on an already-dirty node — so an update arriving
mid-run vanished. The effect finished holding a value that was already stale, went clean, and left the
queue. Nothing threw, nothing looped; it was simply one update behind, permanently, and every later run
landed one behind again.

@await showed stale data. The rendered branch was rebuilt only when the await state changed, and
resource.mutate() — the documented optimistic-update path — writes data while leaving loading false. So
the branch kept showing the previous value. Refetches survived only because they bounce through pending
first, which is a different transition entirely. Two documented APIs, quietly incompatible.

@for … track could key every row identically. The key expression was resolved against the parent
scope instead of the row, so when a component binding shared the loop variable's name the parameter stopped
shadowing and every row got the same key. Keyed reconciliation then reused nodes for the wrong rows, state
bled between them, and removals collapsed.

A store died with the component that happened to use it first. store() ran its factory under whichever
owner was ambient at the first call, so every effect created inside it was disposed when that one component
unmounted — while every other consumer went on holding the same half-dead instance. Order-dependent, and
invisible: the signals kept working, so the store still looked alive.

Two ways the formatter changed your program. The Prettier plugin printed disabled="" as bare
disabled — a different prop type on a component tag — and dropped whitespace between inline elements, so
<span><b>a</b><b>b</b></span> started rendering "a b" instead of "ab". A formatter may reflow anything it
likes except the output.

🧭 Router, at the edges where the browser pushes back

A malformed percent-escape in a URL threw inside route resolution and blanked the page instead of falling
back to *. A guard redirect pushed a history entry instead of replacing it, so Back returned to the
guarded page, the guard fired again, and the user was trapped. A guard-vetoed multi-entry pop rolled back by
exactly one entry regardless of how far the jump went, leaving the URL and the rendered page disagreeing.

🛠 The daily loop

weave check honours your tsconfig.json. It used a hardcoded option set with no paths and no
baseUrl, so any project using path aliases got "Cannot find module" on every aliased import — a wall of
errors from the framework's own quality tool, against a project that is correct.

A failed rebuild no longer reloads the browser into nothing. weave dev cleared its in-memory outputs,
refilled them from a failed build's empty list, and reloaded anyway — so a syntax error produced a white
page with the real error only in the terminal. The last good bundle is now kept and the error is painted
over the still-working page.

Source maps. Neither weave dev nor weave build emitted any. Both do now.

⚙️ Release engineering

Publishing was gated only on a [publish] marker — no test ran before packages went to npm with a
provenance attestation. The suite now runs inside the publish job. And a partial publish can be resumed:
the failure message used to promise that npm skips already-published packages, which it does not.

Full detail

Every entry, with the reasoning: CHANGELOG.md.

v1.8.0

Choose a tag to compare

@github-actions github-actions released this 18 Jul 22:04

A release about the parts of Weave that teach — the skills an AI agent reads, the MCP server it
calls, the JSDoc your editor shows on hover. None of it changes how your app runs. All of it changes
whether the code written against Weave is correct.

The theme, plainly: documentation that is wrong is worse than documentation that is missing.
Missing docs make someone look things up. Wrong docs get copied.

✨ MCP server speaks the current protocol, and negotiates

@weave-framework/mcp declared MCP revision 2024-11-05 — about a year behind current. It now
speaks through 2025-11-25 and, more importantly, negotiates: it answers with the version the
client asked for when it knows that one, instead of always announcing its own. Bumping the number
alone would have been a regression — the spec tells a client to disconnect when handed a version it
does not support, so an older client would have been shown the door over a session that worked fine.

Also: a tool called with a missing argument now says which one. Every tool declared required in its
schema and nothing enforced it, so weave_compile_template answered "Empty template fragment" — a
message that sends you to inspect your markup when the fault was in the call.

🐛 The skills no longer teach a resource leak

The component skill's lifecycle example was:

onMount(() => {
  const id = setInterval(tick, 1000);
  onCleanup(() => clearInterval(id)); // registers nothing
});

onCleanup registers on the currently running computation, and an onMount callback fires later
on a microtask, where there is none. It silently did nothing. Every component written from that
example kept its interval running after unmount, with no error anywhere. It uses onDispose now, and
both hooks are documented with the distinction spelled out.

📚 The skills now cover the whole public API

They are what an AI assistant reads before writing Weave code, so a gap there is not a missing
paragraph — it is an assistant inventing an API in its place. Measured: 84 public exports were never
mentioned
, including onMount, provide/inject, the entire devtools surface, every transition,
Interceptor and Optimistic. All documented now, written from the source, weighted toward what
goes wrong rather than what exists.

A new CI gate keeps it that way: every public export must appear in its skill, every ts/html
example must parse, and the templates skill must show every block, directive and special attribute
the parser accepts. A code fence now carries a promise — ts/html is real, checked code you can
paste; shorthand notation lives in txt.

🐛 Two documentation defects worth naming

  • /learn/forms described an implementation that no longer exists. It called validateAsync()
    "a bounded poll (~30 ms ticks, capped at ~2 s)". It watches validating() with an effect: no
    polling, no timeout. A reader was budgeting for a two-second cap that was never there.
  • The fieldArray JSDoc example did not compile. Its seeds did not match the shape its factory
    returned (TS2322). That snippet is what your editor shows on hover and what the published API
    reference prints — a broken example handed over at the exact moment you are about to copy it.

v1.7.0

Choose a tag to compare

@github-actions github-actions released this 18 Jul 16:19

Correctness fixes in the framework and a substantial repair of the WebStorm editing experience,
plus a pass over the documentation site's own presentation.

🐛 Fixes

  • WebStorm no longer paints correct code red. The language server bundled inside the WebStorm
    plugin had gone stale: it predated auto-expose (a setup() may omit its return), so it typed
    the template context as void and flagged every binding of every such component. On a real
    application that was 1642 false errors across 39 of 41 files — while weave check on the same
    files reported none. Install weave-webstorm-0.23.0.zip. A new CI gate now fails the build if the
    server inside the shipped .zip ever differs from the one in the repository again.

  • Template highlighting is no longer one lonely colored word. A call inside a binding —
    {{ rangeValue() }}, {{ adapter() }} — rendered as plain black text, because the color key it
    inherited from has no foreground in any scheme WebStorm ships. A bare identifier next to it was
    colored, which made the whole thing look broken rather than merely plain. Calls and directive
    prefixes now carry explicit colors, and remain retunable under
    Settings → Editor → Color Scheme → Weave.

  • The editor now actually enforces a child component's props. Because the server never resolved
    an imported component's synthesized default export, typeof Child fell back to any and every
    <Child prop={{ … }}> check quietly passed — passing a number where a handler belongs, or a prop
    the child does not declare, produced no error. The same gap stripped inline handler parameters of
    their contextual type, so onChange={{ (v) => … }} reported a spurious implicit-any. Both are
    fixed, and a prop error is now pinned to the prop name in the template.

  • A nested @for no longer reads the wrong row. Every loop row function took a parameter
    literally named _row, so an inner loop shadowed its parent. An outer loop variable referenced
    inside a nested loop compiled to the inner item — silently, with no error. Where the inner item
    happened to carry a same-named property the value looked plausible; where it did not, the binding
    rendered empty. Each loop now gets its own row identifier. The magic names ($index, $first,
    $last, …) still rebind to the innermost loop, which is the correct shadowing.

  • A bare #fragment scrolls instead of navigating away. navigate('#section') took the empty
    string before the # as the destination path, which resolves to / — so an in-page anchor threw
    the app back to the root route and lost the page you were reading. A bare fragment now keeps the
    current path and query and just scrolls.

  • <Sidenav> fills its container. The shell set no height, so a drawer stopped at its last nav
    item instead of running the full height of the box it was given. Auto-height parents are
    unaffected (it resolves to a no-op there).

  • <Tree> and <List> use real icons. The tree disclosure marker was a CSS glyph and both
    components drew their reorder handle as a character; they are lucide <Icon>s now
    (chevron-right, grip-vertical). grip-vertical joins the built-in icon set. The tree's
    toggle-glyph token moves 12px → 14px, since a chevron icon needs slightly more than the glyph
    did to read at the same weight.

⚠️ Deprecated (still emitted, still safe to set)

Five design tokens became inert during this cycle — the rules that read them went away when a ×
character became a lucide icon and a marked button's underline became a tonal fill. They are kept
and emitted
, because the design-token contract is frozen public API: a token is deprecated first and
removed only in a major. If you set one of these, nothing breaks — but nothing happens either, so move
to the replacement when convenient.

Deprecated Instead
--weave-button-mark-width no replacement — marked is a tonal fill, not an underline
--weave-chips-remove-font-size --weave-chips-remove-icon
--weave-input-clear-size sized by the icon rules
--weave-datepicker-cell-size (typography) --weave-datepicker-cell-font
--weave-date-range-picker-cell-size (typography) --weave-date-range-picker-cell-font

(The sizing group's cell-size on both pickers is untouched and still does what it always did.)

📚 Documentation site

  • Each API reference package page now opens with a jump index of its exports, grouped by kind
    with a per-entry badge — @weave-framework/runtime alone is 56 symbols, previously all expanded
    end to end with no way to see what the package contained.
  • API anchors are unique. A function and a same-named type collided (resource and Resource
    both became #resource), which is a duplicate DOM id and sent half those links to the wrong
    symbol. Existing deep links still resolve.
  • Component demos read as finished, anchored surfaces rather than floating on the backdrop, and the
    Progress Bar — previously zero-width in the demo stage, with a track the colour of the page — is
    visible.

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 11:19

The big one: Weave renders to HTML at build time, and the browser resumes it instead of rebuilding it.
Everything here is opt-in and additive — a normal weave build is byte-for-byte what it was, and no existing
code changes.

✨ Features

  • weave build --ssg — static generation. Every route renders to real HTML at build time: painted on
    arrival, crawlable, and served as plain files (no server at request time). Routes are derived automatically,
    each page gets its own <title>, and each is its own chunk — a reader downloads the page they opened, not
    your whole site. On our own documentation site that is 1555.7 KB → 169.7 KB of transferred payload for a
    single page, measured in the browser.

  • ssg: { resume: true } — resumability. With --ssg alone the browser client-renders over the
    prerendered HTML; add this and it adopts it instead. The build snapshots the reactive graph — every
    signal's value, per component instance — into an inline <script type="application/weave">; the client
    rebuilds the signals and attaches the existing DOM to them. setup() is never called on the client.
    The nodes the server wrote are the nodes you keep, down to the same text node. Re-running means paying for
    the render twice — once to produce the HTML, once to discover what it already says. Resuming pays once.

  • Data prerenders with the page. A resource() that fetches during a build is awaited before the HTML is
    written
    , and its result travels in the snapshot. No spinner on first paint, and no second request for data
    the build already has.

  • lazy() prerenders too. A lazily-imported component still renders its real HTML into the page, and still
    stays out of everyone else's bundle. Lazy means "not in your bundle", not "not in your HTML" — so there is no
    trade between a complete first paint and a small download.

  • Resume covers what real components actually do: nested components, <slot>, element refs (re-bound
    from the adopted DOM rather than serialized), use: actions, @if/@for/@switch/@key/@render/
    @snippet, routed views, component-level on: handlers, props, module-scope bindings, effect() and
    onMount() in setup().

  • It tells you what it cannot resume, at build time. A value that cannot cross the wire (a live socket, a
    class instance) makes that component client-render — with a warning that names the binding, the file and the
    cause, never a silent degradation. weave build --ssg also warns for a handler that won't inline or a
    computed that can't be rebuilt.

  • <Timepicker>, <Select>, <Datepicker>, <DateRangePicker> use lucide icons instead of hand-drawn
    glyphs, matching the rest of the library.

🐛 Fixes

  • --ssg + resume was broken for every multi-root app — including ours. The client entry handed the
    adopt walk firstElementChild, which is right for a single-root component and wrong for a multi-root one,
    whose roots are the mount target's children. It threw on its first step, silently, and the page stayed
    inert server HTML. Nothing on our documentation site had ever resumed. The compiler now publishes the
    contract (adopt.container) rather than the caller guessing it, and a root that cannot adopt client-renders
    outright instead of arming handlers over DOM nobody adopted.

  • A bare effect() in setup() is re-created on resume. It binds no name, so nothing rebuilt it: a
    per-route document.title effect stayed frozen at the server's value forever.

  • An onMount() in setup() resumes. An earlier build refused to adopt any component that had one; that
    refusal was wrong (the hook is re-created, exactly as an effect is) and it was also the more expensive
    answer, since client-rendering re-runs setup() and fires the hook anyway.

  • onMount is inert during a build by construction — there is no browser at build time, and it no longer
    depends on the render happening to be synchronous to stay that way.

  • Regex literals, comments, type annotations, casts, destructuring, generics and shadowed declarations are
    no longer misread as variable references by the compiler's setup analysis. Each of these quietly narrowed
    what could resume.

🔬 Under the hood

  • CI. This repository had none: the test suite, typecheck, lint and size budgets ran only when someone
    remembered. Three jobs now run on every push, including verify:resume — a real app, built by the real CLI,
    resumed and clicked in a real browser.
  • Size budgets are enforced (verify:size). The SPA core is 21.2 KB gzipped; resume, adopt and serialize
    sit on their own budget lines and cost a SPA-only app zero bytes.
  • Zero third-party runtime dependencies, unchanged.

v1.5.28

Choose a tag to compare

@github-actions github-actions released this 14 Jul 21:19

🐛 Fixes

  • <DateRangePicker> — the second click now always registers (FW-17 follow-up). Picking the
    first date worked, but the second click often did nothing: while you moved the pointer toward the
    end date, each hover rebuilt the whole day grid, so the day cell under the cursor was replaced
    mid-click — the mousedown landed on the old node and the mouseup on the new one, and the
    browser never fired a click. The hover preview now re-decorates the existing day cells in
    place
    (a new shared refreshDays() on the calendar core) instead of rebuilding them, so cells
    keep their identity and real clicks always land. Also stopped the value-sync effect from
    re-rendering on hover (it was tracking the pending/hover signals through the day-cell decorator —
    now runs untracked). <Datepicker> is unaffected. Pinned by a regression test that drives a real
    mousedown → hover → mouseup and asserts the cell is never detached.