Releases: vertz-dev/vertz
v0.2.80
What's Changed
- feat(fetch+ui+openapi): StreamDescriptor — auto cache keys for streaming endpoints [#2855] by @viniciusdacal in #2862
- fix(ci): stop premature "latest" flips on release workflow [#2860] by @viniciusdacal in #2864
- docs(plans): open-agents clone scoping note + fix shipped status on agents-loop plan by @viniciusdacal in #2869
- docs(design): design doc for agent visual handoff [#2865] by @matheuspoleza in #2866
- fix(db): jsonb parity on SQLite/D1 + type-gated path filters [#2850] by @viniciusdacal in #2870
- chore: version packages by @github-actions[bot] in #2863
- feat(vtz): phase 1 headless screenshot — POC + design for #2865 by @matheuspoleza in #2871
- feat(vtz): screenshot::artifacts module — Task 1 of Phase 1 [#2865] by @matheuspoleza in #2872
- feat(vtz): chromiumoxide dep + binary-size decision gate — Task 2 [#2865] by @matheuspoleza in #2873
- fix(ui-server): use createRequire for ESM-safe module loading [#2875] by @viniciusdacal in #2879
- feat(vtz): screenshot::fetcher local-probe path — Task 3a [#2865] by @matheuspoleza in #2874
- fix(landing): land #2876 + #2877 on main — missed during stacked merge by @viniciusdacal in #2882
- fix(compiler): recognize regex literals in strip_comments [#2878] by @viniciusdacal in #2883
- fix(ui,theme-shadcn,ui-server,cli): default token scales + two SSR bugs by @viniciusdacal in #2887
- fix(landing): replace array-wrapped css() blocks with plain objects by @viniciusdacal in #2892
- fix(db): run d.jsonb validator on writes across both dialects [#2867] by @viniciusdacal in #2891
- docs(plans): design doc for typed JSONB operators [#2868] by @viniciusdacal in #2884
- chore: version packages by @github-actions[bot] in #2888
- docs(mint-docs): guide for reading and writing files with vtz by @viniciusdacal in #2900
- fix(vertz): auto-load import.meta.hot types from client-runtime subpaths [#2893] by @viniciusdacal in #2895
- feat(db): typed JSONB operators — path() + jsonContains / jsonContainedBy / hasKey [#2868] by @viniciusdacal in #2896
- docs(mint-docs): guide for spawning processes with node:child_process by @viniciusdacal in #2901
- fix(ui,theme-shadcn,component-docs): radius scale vars, __conditional thunk unwrap, sidebar scroll preservation by @viniciusdacal in #2904
- fix(native-compiler): preserve attr exprs in arrow-fn JSX props [#2897] by @viniciusdacal in #2905
- fix(db): route SQLite writes without RETURNING through execute() [#2890] by @viniciusdacal in #2906
- fix(vtz): dedup redundant package versions [#2894] by @viniciusdacal in #2909
- feat(vtz): screenshot::fetcher download path — Task 3b [#2865] by @matheuspoleza in #2910
- fix(db): stringify primitive values written to d.jsonb() on SQLite [#2889] by @viniciusdacal in #2907
- feat(vtz): screenshot::pool + chromium spawner — Task 4 [#2865] by @matheuspoleza in #2913
- feat(vtz): MCP tool vertz_browser_screenshot + artifact route — Task 5 [#2865] by @matheuspoleza in #2915
- chore(ci): silence release workflow warnings by @viniciusdacal in #2911
- fix(vtz): iterate install dedup until fixpoint so orphan optional binaries collapse [#2912] by @viniciusdacal in #2914
- feat(create-vertz-app): dev-server-tools rule docs vertz_browser_screenshot — Task 6 [#2865] by @matheuspoleza in #2916
- feat(vertz): Vite-parity HMR API — invalidate/decline/on/off [#2812] by @viniciusdacal in #2917
- docs(mint-docs): vertz_browser_screenshot guide — Task 7 [#2865] by @matheuspoleza in #2918
- ci(vtz): screenshot E2E on Linux + macOS — Task 8 [#2865] by @matheuspoleza in #2919
- feat(db): d.bytea() column type for binary storage [#2843] by @viniciusdacal in #2921
- feat(db): hasAllKeys / hasAnyKey JSONB operators [#2886] by @viniciusdacal in #2922
- fix(vtz): preserve package bin files when regenerating .bin shims [#2908] by @viniciusdacal in #2924
- fix(vtz): native sqlite binds Uint8Array + reads BLOB as Uint8Array [#2920] by @viniciusdacal in #2923
- fix(server): coerce form-encoded bodies on the server [#2808] by @viniciusdacal in #2928
- fix(db): type-gate array operators (arrayContains/arrayContainedBy/arrayOverlaps) to Postgres [#2885] by @viniciusdacal in #2927
- feat(ui): add @vertz/ui/client subpath for UI-only HMR types [#2813] by @viniciusdacal in #2926
- fix(schema): user-friendly invalid-type messages for number/bigint/date [#2809] by @viniciusdacal in #2925
- fix(vtz): don't rewrite
effect→domEffectinside strings/comments [#2801] by @viniciusdacal in #2930 - fix(ui-primitives): resolve no-narrowing-let warning in calendar-composed [#2802] by @viniciusdacal in #2929
- fix(vtz): populate module graph in test watch mode [#2765] by @viniciusdacal in #2932
- fix(pm): transitive dist-tag deps with multiple versions in graph [#2796] by @viniciusdacal in #2931
- chore: changeset for #2865 Phase 1 (headless screenshot MCP tool) by @matheuspoleza in #2935
- fix(changeset): use @vertz/runtime (not vtz) so release workflow parses by @matheuspoleza in #2936
- fix(compiler): diagnose non-bare-identifier route component factories [#2787] by @viniciusdacal in #2933
- fix(compiler): field-selection gaps — reduce/flatMap, loops/try/switch, scope shadowing [#2782] by @viniciusdacal in #2934
- chore: version packages by @github-actions[bot] in #2902
- fix(changeset): use @vertz/runtime (not vtz) in #2934's field-selection changeset by @matheuspoleza in #2939
- chore: version packages by @github-actions[bot] in #2937
- docs: retrospective for #2865 Phase 1 (headless screenshot) by @matheuspoleza in #2941
- fix(vtz): invalidate dependents' cache when a file fails to compile [#2766] by @viniciusdacal in #2940
- fix(compiler): AOT threads hydration id onto first element child of fragment roots [#2784] by @viniciusdacal in #2938
- fix(client): type import.meta.main for run-if-main idioms (#2811) by @viniciusdacal in #2955
- docs(agents): design doc for agent-store ↔ entity bridge [#2847] by @viniciusdacal in #2959
- docs(plans): GEO/SEO strategy design doc + 5 phase files by @matheuspoleza in #2963
- feat(landing): blog — dogfood Vertz stack at /blog [#2947] by @matheuspoleza in #2960
- chore(2947): post-merge housekeeping — retro + archive plan by @matheuspoleza in #2973
- feat(landing): 10 thematic blog posts + per-post covers by @matheuspoleza in #2977
- feat(agents): bridge AgentStore to Vertz entities (RLS-aware sessions/messages) [#2847] by @viniciusdacal in #2966
- fix(compiler): disambiguate context stable ids on same-name collisions [#2786] by @viniciusdacal in #2965
- fix(ui): prevent concurrent form submissions on double-click [#2982] by @viniciusdacal in #2983
- test(ui,ui-primitives): regression coverage for List.Item context on reactive re-runs [#2956] by @viniciusdacal in #2978
- fix(query): refetch on remount after mutation while unmounted [#2986] by @viniciusdacal in #2988
- fix(compiler): make early-return guards reactive in component bodies by @viniciusdacal in #2991
- feat(ui,ui-server)!: remove Suspense — loading UX is early-return + query.loading [#2985] by @viniciusdacal in #2990
- fix(ui): form() initial option re-evaluates reactively [#2984] by @viniciusdacal in #2989
- fix(server): type rules.entitlement() with regi...
vertz@0.2.80
Patch Changes
-
#2965
06605b0Thanks @viniciusdacal! - fix(compiler): disambiguate context stable ids when twocreateContextcalls share a variable name in the same fileCloses #2786.
injectContextStableIdsgenerated the id as{filePath}::{varName}. TwocreateContext()calls in the same file with the same variable name (e.g. an inlined/formatted pair on one line, or a code-generated module) produced the same id, so the runtime context registry silently returned the same object for both — breaking Provider/useContext pairing.Both the Rust transform (
native/vertz-compiler-core/src/context_stable_ids.rs) and the TypeScript sibling (packages/ui-server/src/build-plugin/context-stable-ids.ts) now track a per-name occurrence counter and suffix@Non repeats. The first occurrence of a name keeps the original{filePath}::{varName}id (so existing single-context files are unchanged); the second becomes{filePath}::{varName}@1, the third@2, and so on.A per-name counter is used rather than a source span because counters only shift when contexts are added or removed, whereas spans shift on any edit to earlier code — counters are more HMR-stable.
-
#2938
682eb30Thanks @viniciusdacal! - fix(compiler): AOT threads hydration id onto the first element child of a fragment rootCloses #2784.
Previously, when an interactive component (a component with a
letdeclaration) returned a JSX fragment, the AOT SSR transformer silently dropped thedata-v-idmarker.fragment_to_stringtook nohydration_idparameter, and the call site inexpr_to_stringpassed the id intoelement_to_stringbut not intofragment_to_string. Result: the server-rendered HTML had no root marker the client hydrator could locate, so event handlers and signal subscriptions never attached — an invisible failure for any component written asreturn <>...</>.The AOT path now matches the runtime SSR behavior (where
inject_hydration_attralready skipsdocument.createDocumentFragment()and targets the first__element(...)call): the hydration id is threaded intofragment_to_string, and gets attached to the first element-or-fragment child. Text and expression children are skipped; nested fragments recurse, carrying the id down until an element is found or the fragment is exhausted. -
#3019
3accbd6Thanks @viniciusdacal! - fix(compiler): use span-overlap (not point) check for mutation ranges in signal transformerCloses #2785.
is_in_mutation_rangetested whetherident.span.startwas a member of a recorded mutation range (pos >= start && pos < end). That contract relied onmutation_analyzeralways recording a span that begins exactly at the identifier's first character — so any future tightening (e.g., the operator-only span for+=, or a span recorded on an inner sub-expression) would silently misclassify the identifier as outside the range and double-handle it by appending.valueon top of the mutation rewrite.Replaced the point check with a span-overlap check (
ident.start < range.end && ident.end > range.start), renamed the predicate tooverlaps_mutation_range, and applied it at all three call sites: identifier reads, assignment-expression LHS, and update-expression targets. -
#2991
27ea038Thanks @viniciusdacal! - fix(compiler): make early-return guards reactive in component bodiesCloses #2987.
A component of the shape
if (cond) return <Loading/>; return <Ready/>;froze at the guard branch: the body ran once at mount and never re-ran whencondflipped. This broke the documented loading-UX pattern ofquery().loading+ early return.The compiler now detects the guard shape (N consecutive
if (cond) return <jsx>;at the top of a component body, followed by a single trailingreturn <jsx>;) and rewrites the body to wrap the main return in a chain of__conditional(() => cond, () => branch, () => fallback)calls, so the condition is re-evaluated reactively and the DOM swaps between branches without re-mounting the component.Guards with an
elsebranch, multi-statement blocks, or non-guard statements between guards are left alone — the per-return mount-frame wrapper still handles them. -
#2955
2c1616cThanks @viniciusdacal! - feat(client): typeimport.meta.maininvertz/clientand@vertz/ui/clientCloses #2811.
Follow-up to #2777. The vtz runtime (via deno_core) already sets
import.meta.mainon every module —truefor the entry module,falsefor imported modules — so the standard "run if main" idiom works without any polyfill:// src/api/server.ts const app = createServer({ /* ... */ }); export default app; if (import.meta.main) app.listen(env.PORT);
Previously the type was only available to projects that pulled in
bun-types. The client augmentation (packages/ui/client.d.ts) now declaresreadonly main: booleanalongsidehot, so any tsconfig that includes"types": ["vertz/client"](or"@vertz/ui/client") gets it automatically. Scaffolded apps already have this entry.bun-typesremoved fromsites/dev-orchestratorwhere it was only kept for this type. -
Updated dependencies [
06605b0,4855184,513fe1e,2840af4,5b3838b,f426ce5,85707b2,2c1616c,e84adde]:- @vertz/ui-server@0.2.80
- @vertz/ui@0.2.80
- @vertz/schema@0.2.80
- @vertz/fetch@0.2.80
- @vertz/server@0.2.80
- @vertz/cli@0.2.80
- @vertz/cloudflare@0.2.80
- @vertz/db@0.2.80
- @vertz/errors@0.2.80
- @vertz/testing@0.2.80
- @vertz/tui@0.2.80
- @vertz/ui-auth@0.2.80
- @vertz/ui-primitives@0.2.80
create-vertz@0.2.80
Patch Changes
- Updated dependencies []:
- @vertz/create-vertz-app@0.2.80
@vertz/ui@0.2.80
Patch Changes
-
#2996
4855184Thanks @viniciusdacal! - fix(ui,schema): coerce form values for JSON bodies, drop fields not in the schemaCloses #2980.
form()was sending checkbox values as raw strings in JSON bodies ("concluida":"true") when the SDK lackedmeta.bodySchema, causing the server to reject the request with422 Expected boolean, received string.Two changes:
form()no-schema fallback now usesformDataToObject(fd, { nested: true, coerce: true }), so plain checkbox/number inputs serialize asboolean/numberin the JSON body.coerceFormDataToSchemanow treats the schema as the contract: only fields declared in the schema's shape are included in the result. Unknown form keys are dropped instead of being forwarded to the server. This prevents accidental leakage (e.g. an attacker injectingtenantIdvia DevTools, or a stale<input>from another form) and removes a class of.strict()-rejection footguns.
Custom non-Vertz schema adapters keep their existing behavior — values pass through untouched so user-supplied parsers stay in control.
-
#2983
513fe1eThanks @viniciusdacal! - fix(ui): prevent concurrent form submissions (double-click creates duplicates)Closes #2982.
form()did not lock against re-entrant submissions. A double-clicked submit button fired twosubmitevents back-to-back; both enteredsubmitPipeline, passed validation, and called the SDK — creating duplicate records.submitPipelinenow checkssubmitting.peek()synchronously at entry and returns early if a submission is already in flight.submitting.value = trueis set before any work (so the second synchronous call sees the guard), and atry/finallyensures it is reset on every exit path. The pipeline returns a boolean so theonSubmit/submitwrappers skip their post-processing (form reset) when a call was rejected. -
#2994
2840af4Thanks @viniciusdacal! - fix(ui,fetch): map server 422 validation errors back to form field error signalsCloses #2981.
Server 422 responses were being swallowed. The server emits per-field errors under
body.error.details, but the fetch client was looking atbody.error.errors, soFetchValidationErrornever fired.form()then fell back to the generic_formhandler, and per-field UI feedback never appeared.Two changes make the round-trip work:
@vertz/fetchnow readserror.details(matchingpackages/server/src/entity/error-handler.ts) and normalizes paths: array paths (e.g.['items', 0, 'name']) become dot-notation strings, and empty paths (''/[]) become_form— matching the convention already used by client-side validation. An emptydetailsarray falls through to the regular HTTP error path so the top-level message still reaches the UI.FetchValidationErrorandisFetchValidationErrorare re-exported from@vertz/fetchfor consumer use.form()checks forFetchValidationErrorinsubmitPipelineand walks the already-normalized.errors, writing each message to the matching field'serrorsignal.onErrorreceives the same per-field record. Non-validation errors keep the existing_formfallback.
-
#2988
5b3838bThanks @viniciusdacal! - fix(query): refetch on remount when a mutation occurred while the query was unmounted [#2986]When a user navigated from a list page to a form, created an entity, and navigated back via
router.navigate(), the list kept showing the old cached data until a full page reload. Between unmount and remount, the list query had unsubscribed from theMutationEventBus, so theemit()fired by the form's create mutation had no live listener — yet the cached entry (and its query indices) remained, and was served on remount.MutationEventBusnow tracks a monotonic per-entity-type version that increments on everyemit().MemoryCache.set()accepts an optionalversionargument and records it alongside the value;CacheStore<T>gained an optionalgetVersion(key)accessor. Whenquery()gets a cache hit for an entity-backed query on mount, it compares the cached entry's version with the current bus version for the same entity type and treats the entry as stale when the bus version is newer, falling through to a fresh fetch.This is additive — custom
CacheStoreimplementations that don't implementgetVersioncontinue to work and simply keep the previous (cached) behavior. -
#3004
85707b2Thanks @viniciusdacal! - fix(ui,runtime): return correct HTTP status for SSR routesCloses #3001.
The dev server returned
404 GET /for the task-manager example even though SSR rendered the page successfully. Two related bugs collapsed three states into two:matchForSSR()only setctx.matchedRoutePatternswhen a route matched, leaving itundefinedfor an unmatched URL — indistinguishable from "no router was rendered". Now it explicitly records[]for "router rendered, no match".- The vtz JS↔Rust bridge in
persistent_isolate.rsserializedresult.matchedRoutePatterns ?? nullinstead of|| [], preserving theundefined-vs-empty distinction so the Rust handler can return200for routerless apps and404only when a router actually failed to match.
Status mapping is now uniform across
@vertz/ui-server's handlers and thevtz devserver: missing/null→ 200,[]→ 404,[…]→ 200. -
#2955
2c1616cThanks @viniciusdacal! - feat(client): typeimport.meta.maininvertz/clientand@vertz/ui/clientCloses #2811.
Follow-up to #2777. The vtz runtime (via deno_core) already sets
import.meta.mainon every module —truefor the entry module,falsefor imported modules — so the standard "run if main" idiom works without any polyfill:// src/api/server.ts const app = createServer({ /* ... */ }); export default app; if (import.meta.main) app.listen(env.PORT);
Previously the type was only available to projects that pulled in
bun-types. The client augmentation (packages/ui/client.d.ts) now declaresreadonly main: booleanalongsidehot, so any tsconfig that includes"types": ["vertz/client"](or"@vertz/ui/client") gets it automatically. Scaffolded apps already have this entry.bun-typesremoved fromsites/dev-orchestratorwhere it was only kept for this type. -
#2990
e84addeThanks @viniciusdacal! - feat(ui,ui-server)!: remove Suspense — use early-return guards for loading statesCloses #2985.
Suspenseis removed from@vertz/ui. Vertz's reactivity model handles loading states viaquery().loadingand the compiler-supported early-return guard pattern —if (q.loading) return <Loading/>; return <Real/>— which gives you a fully-typedq.datapast the guard and avoids the Promise-throwing machinery Suspense inherits from React.Breaking changes
@vertz/ui—SuspenseandSuspensePropsare no longer exported. Replace with an early-return guard (see the new "Early return when you need loaded data" section in the data fetching guide).@vertz/ui-server—createSlotPlaceholder,resetSlotCounter,createTemplateChunk, andRenderToStreamOptionsare removed. The internal__suspenseVNode tag (never produced by any shipped code) is gone too.renderToStream(tree, options?)is nowrenderToStream(tree)— it walks the tree synchronously and serializes into a single HTML chunk.
Cleanup
error-boundary-context.ts(the async-error handler stack used only by Suspense) is removed.ErrorBoundarykeeps its synchronous try/catch + retry behavior unchanged.
-
Updated dependencies [
4855184,2840af4]:- @vertz/schema@0.2.80
- @vertz/fetch@0.2.80
@vertz/ui-server@0.2.80
Patch Changes
-
#2965
06605b0Thanks @viniciusdacal! - fix(compiler): disambiguate context stable ids when twocreateContextcalls share a variable name in the same fileCloses #2786.
injectContextStableIdsgenerated the id as{filePath}::{varName}. TwocreateContext()calls in the same file with the same variable name (e.g. an inlined/formatted pair on one line, or a code-generated module) produced the same id, so the runtime context registry silently returned the same object for both — breaking Provider/useContext pairing.Both the Rust transform (
native/vertz-compiler-core/src/context_stable_ids.rs) and the TypeScript sibling (packages/ui-server/src/build-plugin/context-stable-ids.ts) now track a per-name occurrence counter and suffix@Non repeats. The first occurrence of a name keeps the original{filePath}::{varName}id (so existing single-context files are unchanged); the second becomes{filePath}::{varName}@1, the third@2, and so on.A per-name counter is used rather than a source span because counters only shift when contexts are added or removed, whereas spans shift on any edit to earlier code — counters are more HMR-stable.
-
#2990
e84addeThanks @viniciusdacal! - feat(ui,ui-server)!: remove Suspense — use early-return guards for loading statesCloses #2985.
Suspenseis removed from@vertz/ui. Vertz's reactivity model handles loading states viaquery().loadingand the compiler-supported early-return guard pattern —if (q.loading) return <Loading/>; return <Real/>— which gives you a fully-typedq.datapast the guard and avoids the Promise-throwing machinery Suspense inherits from React.Breaking changes
@vertz/ui—SuspenseandSuspensePropsare no longer exported. Replace with an early-return guard (see the new "Early return when you need loaded data" section in the data fetching guide).@vertz/ui-server—createSlotPlaceholder,resetSlotCounter,createTemplateChunk, andRenderToStreamOptionsare removed. The internal__suspenseVNode tag (never produced by any shipped code) is gone too.renderToStream(tree, options?)is nowrenderToStream(tree)— it walks the tree synchronously and serializes into a single HTML chunk.
Cleanup
error-boundary-context.ts(the async-error handler stack used only by Suspense) is removed.ErrorBoundarykeeps its synchronous try/catch + retry behavior unchanged.
-
Updated dependencies [
4855184,513fe1e,2840af4,5b3838b,85707b2,2c1616c,e84adde]:- @vertz/ui@0.2.80
- @vertz/core@0.2.80