Repository navigation
Releases
1.6.0
A minor release from main: zerolith lint checks a project's templates and fails on any finding, and every new project's npm run check runs it, so a frozen read stops the check. A fifth tutorial follows a coding agent from a request to a fix, and the other four are rewritten in plain language.
Added
@zerolith/cliand thezerolithcrate:zerolith lintruns the template lint ofzerolith doctoralone and exits with 1 on any finding: a frozen read, a component bound without being called, or an image, button, or frame without a name.--jsonprints the findings as JSON.zerolith doctoris unchanged, so its warnings, such asskill_outdatedafter an upgrade, still exit with 0.- Docs: a fifth tutorial, Coding agent, asks an agent for a page in a new project, checks what it wrote, and has it fix a frozen read from the code
npm run checkprints.
Fixed
@zerolith/cliand thezerolithcrate: across a project,zerolith lintandzerolith doctorreport a capitalized name a module imports as a component bound uncalled only when the module it comes from exports a function or a class under that name, following re-exports andexport *, so an imported constant such as${APP_NAME}, or a view held in aconst, is no longer reported. The Vite plugin, which reads one module at a time, still warns on every imported capitalized name.@zerolith/cliand thezerolithcrate: a new project'slintscript runsoxlint && zerolith lint, sonpm run check, the gate its AGENTS.md names, fails on a template mistake that type-checks, where it passed unless a test happened to cover it. An existing project gets the same by adding&& zerolith lintto itslintscript.- Docs: the four tutorials are rewritten in plain language, with terms defined where they first appear and a Try it section in every step.
- Docs: in the packing list, an item added while Packed is selected switches the filter back to All, where it was added out of sight, and the release notes reader's search step uses the
/releases/:versionroute the other steps use. @zerolith/router,@zerolith/cli, and@zerolith/vite: the READMEs namefocus,shallow,failed, and a table given as a function;zerolith build --entry; and the lint for a component bound without being called.
Install
npm install zerolith@1.6.0
npx @zerolith/cli@1.6.0 new my-app
cargo install zerolith --version 1.6.0 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
1.5.0
A minor release from main, the last of the fixes the tutorials turned up: the router takes a focus option, which the starter sets, zerolith build --entry --classic writes a plain script, the template lint reports a component bound without being called, and zerolith preview works beside zerolith dev by leaving a running backend as it is.
Added
@zerolith/router:createRoutertakes afocusoption, a selector for the element that takes keyboard focus after each navigation to another page, so keyboard and screen reader users stay with the page when the link they pressed goes away. The starter sets it to"h1", and its stylesheet keeps a ring off an element withtabindex="-1".@zerolith/cliand thezerolithcrate:zerolith build --entry --classicwrites a plain script instead of an ES module, which a page loads with<script src>from any origin, with no CORS header.@zerolith/vite,@zerolith/cli, and thezerolithcrate: the template lint reportstemplate_component_uncalledfor a component bound as content without being called, as${AddItem}, which type-checks and fails only when the page renders.
Fixed
@zerolith/cliand thezerolithcrate:zerolith dev,serve, andpreviewleave a backend whose address already answers as it is, and say so, where a second copy failed on the taken port, sonpm run previewworks besidenpm run dev.@zerolith/cliand thezerolithcrate: the starter's stylesheet keeps a radio button or a checkbox on one line with its label on a narrow screen, where the text dropped underneath.- Docs: the release notes reader drops its
PageHeadingcomponent for the router'sfocusoption, the sign-up form drops its rule for labels and the step that stopped the dev server first, and the widget's embed step no longer stops it.
Install
npm install zerolith@1.5.0
npx @zerolith/cli@1.5.0 new my-app
cargo install zerolith --version 1.5.0 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
1.4.0
A minor release from main: a custom element's view receives what the tag held as children, zerolith build --unbundled gives the application's own modules addresses that carry their hash, and zerolith preview says what is wrong with a folder it cannot serve. A Content-Security-Policy that allows an unbundled page's import map by its hash has to take the new hash from each build, which the build prints.
Added
zerolith/element: a view receives what the tag held aschildren, a signal beside its props, and binds it where it belongs, as a slot does. It holds the same nodes through a move, and takes in what the parser adds to the tag after a script that ran early.<note-card heading="Drafts">Two notes</note-card>now needs only${children}in the view. It replaces reading the content from the host, which lost it when the element moved and could move the view's own anchor with it.
Fixed
@zerolith/cliand thezerolithcrate:zerolith build --unbundledresolves each of the application's own modules through the import map to an address carrying its hash, and names the page's module scripts, preloads, and stylesheets the same way, so a host that sets its own cache time cannot give a returning visitor an old copy of one module beside a new copy of another, which could fail with a missing export and leave the page blank until the copy expired. A module that did not change keeps its address and stays cached, and no module is rewritten. The import map's text now changes with most changes to the code, so a Content-Security-Policy that allows the map by its hash has to take the new hash from each build, whichzerolith build --unbundledandzerolith importmap --htmlprint; a nonce, or the bundled build, needs nothing new.@zerolith/cliand thezerolithcrate:zerolith previewon a folder that does not exist says to runzerolith build, where it printed only that the system could not find the file, and on a folder with noindex.html, as afterzerolith build --entry, says so in its banner.
Install
npm install zerolith@1.4.0
npx @zerolith/cli@1.4.0 new my-app
cargo install zerolith --version 1.4.0 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
1.3.0
A minor release from main: zerolith build --entry writes a module as one script with a name that stays the same, createRouter takes its table as a function and a failed view, navigate takes shallow, and the fixes the three new tutorials turned up, so those tutorials use plain code where they worked around a limit. zerolith importmap now names each vendored folder for its version, so an existing page needs one more run of it to move.
Added
@zerolith/router:createRoutertakes the route table as a function, read on first use, for a table whose components import the router back.@zerolith/cliand thezerolithcrate:zerolith build --entry <module>writes a module and everything it imports as one script with a name that stays the same, for a page someone else writes, such as a custom element other sites embed.--namechooses the file's name. It needs noindex.htmland writes no page.@zerolith/router:navigatetakes ashallowoption, which keeps the page shown, and where it is scrolled, when the URL's path is the page's own, whatever its query, so a page can write a search to the query string as the reader types without losing the field's focus. Back and forward between such entries keep the page too. A navigation that changed only the query rebuilt the page.- Docs: three more tutorials, each a small app built step by step from a fresh project, with every file a step shows run by the test suite. Build a sign-up form uses
@zerolith/formsand@zerolith/a11yagainst a small backend the reader runs; Build a release notes reader uses route parameters,resource,@zerolith/i18n, anddebounced; and Build a widget for any backend makes a custom element posting through@zerolith/clientto an OpenAPI contract, built into one script any page loads. @zerolith/router:createRoutertakes afailedoption, a view the outlet shows in place of a page whose view throws as a navigation shows it, given the error and the URL, where the outlet could only stay empty. The navigation still fails with the error.
Changed
- Docs site: /learn/ lists the tutorials as ruled rows with each one's name, line, and number of steps, and each tutorial has a folder, a sidebar, and a pager of its own. The packing list moved to /learn/packing-list/, and each of its old step addresses redirects to the new one.
- Docs: the tutorial's undo step passes
validatetostoredwith theisItemcheck from the step before, so a saved value that is not a list of items takes the starting items there too.
Fixed
@zerolith/cli, thezerolithcrate, and@zerolith/vite: a template in a branch the build rules out, such asimport.meta.env.DEV ? html...: "", leaves the production bundle, where its text stayed because the prepared call was kept. The prepare pass marks its calls pure.@zerolith/devtools: the launcher docks to the edge of the window and covers an 8-pixel sliver until the pointer or the keyboard reaches it, where it sat over the last row's button on a short page.- Docs site: every module carries its hash in its address, as the stylesheet does, so a reader crossing a deploy keeps the theme switch, the menu, and search, where the browser could refuse a module against its integrity hash for up to ten minutes.
@zerolith/cliand thezerolithcrate:zerolith importmapandzerolith build --unbundledput each package in a folder named for its version, asvendor/zerolith@1.3.0/, so a page upgraded to a new zerolith version no longer goes blank for a returning visitor whose browser kept the old files, which it refused against the new integrity hashes, through a reload as well, until the copies expired. Runzerolith importmap --htmlonce to move an existing page; the old folders are removed.@zerolith/cliand thezerolithcrate: the starter passes its route table tocreateRouteras a function, so a test or a module that importssrc/routes.tsfirst loads, where it failed withCannot access 'routes' before initialization.zerolith/element: an element replaces whatever it held when it connects, such as fallback text in the tag, where the view rendered beside it. The view can read it from the host as it runs, a view that throws leaves it in place, and an element that upgraded while the page was still parsing drops what the parser adds to it after that, instead of showing fallback or server markup under the view.zerolith: a view rendered inside a shadow root gets the scoped stylesheets, the ones the document has and the ones created later, where a shadow tree saw none of them, so an element placed in another component's shadow root lost its styles.@zerolith/cliand thezerolithcrate: the starter's lint passes a module imported for its effects, such asimport "./print.css"as the styles guide writes it or a module that defines an element, whereimport/no-unassigned-importfailed it.@zerolith/cliand thezerolithcrate:zerolith devandzerolith previewserve the app'sindex.htmlfor a page the browser loads whose last path segment holds a dot, such as/releases/2.3.0, where they answered 404. A request for a missing script, stylesheet, or data file still gets 404; the two are told apart by the request'sAcceptandSec-Fetch-Modeheaders.- Docs site: a page no longer loses its styles when a browser holds a copy of the stylesheet from another deploy. The stylesheet carried an integrity hash on an address that stayed the same between deploys, and GitHub Pages lets a browser keep a file for ten minutes, so a browser could refuse it and show the page with no styles at all. The stylesheet and the theme script now have an address with a short form of their hash and no integrity hash.
- Docs site: a page paints in the brand's fonts from its first frame, where it painted in a system font and reflowed when each font arrived, on any visit whose fonts had to come over the network or be checked again. The fonts are held in a stylesheet of their own, which a browser waits for before it paints, and whose address changes only with the fonts, so a deploy that changes only the site's styles leaves a returning reader's copy of the fonts in place.
Install
npm install zerolith@1.3.0
npx @zerolith/cli@1.3.0 new my-app
cargo install zerolith --version 1.3.0 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
1.2.0
A minor release from main: stored takes a validate option, and the fixes the packing-list tutorial turned up when it was built step by step from a fresh starter. A keyed list no longer half-applies an update with a bad key, a page whose view throws no longer stays behind a new URL, an undo no longer takes back another tab's change, and zerolith dev shows compile errors and skipped CSS in the browser's console.
Added
@zerolith/state:storedtakes avalidateoption, which receives each parsed value, on creation and from other tabs; a value it refuses, or throws on, takes the initial value and reportsstate_storage_unreadable, so a value of another shape, such as another app's under the same key, no longer breaks the page.
Changed
- Docs site: a push to
maindeploys the site, and whilemaincarries the stable line its stable docs are built frommainitself, so a change to the site goes live with its push; a line branch such as1.x, or the release's tag, stands in otherwise. - Docs site: the playground has three more examples, for what 1.1.0 added. Retry and timeout loads a city's record through a stand-in for a flaky service, which gives the first call of every request a 503 and the second no answer, so
timeout,retry,backoff, andretryIfplay out the same way each time whileattemptcounts the runs. Saved state is a notes app whose notes survive a reload withstored, with undo and redo fromhistory, adebouncedsearch, and aselectorfor the open note. Browser signals reads outonline,visible,windowSize,size, andinViewas they change. The preview's sandboxed frame has an opaque origin, where readinglocalStoragethrows, so the playground gives it alocalStorageand asessionStorageof its own, kept for the tab through each run and a reload until Reset, which refuse a write past 262,144 characters of keys and values with aQuotaExceededError, as a full storage does. - Docs: the tutorial has a step on undo and offline after saving with effects. The list moves to
storedfrom@zerolith/state,historygives it Undo and Redo buttons, andonline()from@zerolith/browserfills a status line while the browser is offline. The styles step's page carries both, and every file the new step shows runs as a test. - Docs site: the home page's tour adds Saved state, a draft kept in
localStoragewith undo throughstoredandhistory, drawn with each write it makes, and Browser, a section that measures itself withsize. Each result is drawn from the values its test asserts, as the other tabs' are, and the state guide shows the draft under Undo and redo. - Docs site: the home page's hero, its launch figure included, and its closing band sit on a toned basalt stage, where the hero was a chartreuse plane under a shade, and no panel keeps an offset shadow; the numbers are a ruled table with the check behind each, and the facts beside them a plain line; the closing section is a band with the create command and the two commands after it, where the playground's examples stretched it into a tall block; and every section shares one column.
- Docs site: every page ends with a site footer that links the docs, the project, its community files, and the legal pages: Privacy, Terms of use, and Contact, which are new, with the license and the security policy. A site test fails if a page loads anything from another origin or sets a cookie, as the privacy page says.
- Docs site: the benchmarks page's head sits on the same toned stage, its scores have a section of their own that gives zerolith's score in a sentence, where the score was set large on a card; every section of the page sits on the page ground under a rule, where the grounds alternated and the method was an inverted block. Tables, code blocks, file views, and the example picker drop their offset shadows on every page, page titles drop the chartreuse rule beside them, and the not-found page takes the page column. The benchmarks summary names a tie when another framework prints the same score as zerolith, where it ranked zerolith ahead on digits the page does not show.
- Docs site: the home page's headline is a plain line, "A web framework built on signals", set smaller, where it was a slogan; the footer and the home page's title use the same line.
- Docs site: the home page's headline runs on one line over a row with the lead and the create command, where the two sat beside it, and breaks between its two phrases on a phone; the stage holds the launch figure as well, which sits raised on it under a soft shadow while the stage settles into the page below it, so no edge or strip of the next section shows under the figure, and the stage is a step darker in the light theme so it reads against the page. Every other home section sits on the page ground under a rule, where the grounds alternated and the numbers were an inverted band. The ways to learn zerolith joined the closing band, whose terminal gives each of its three commands a copy button, where one button copied only the first; the command bar and the terminal follow the theme; the package grid labels its libraries and tools; and the launch figure says what to watch and scrolls its code to the lines that run. The speed heading gives the extra time as the bars print it, "About 10 percent slower than hand-written JavaScript", where "Within 10 percent" claimed less than the run measured, and the paragraph under it names a tie. A copy the browser blocks selects the text and names the keys that copy it. A wheel over the launch figure's code scrolls the page, where it stopped in the code and then jumped the page without easing; a click or Tab into the code lets the wheel scroll it.
- Docs site: the launch figure's code reads plainly at the hero's size, with lines short enough not to wrap, no nested conditionals, and named climb and burn rates, in two functions, the tick and the component, whose stages read as the words the readout shows; the click writes only the stage and the clock resets the rocket when a flight starts, and after a flight the button reads Launch, where it read Reset. The line above the counters says the page called LaunchControl() once to draw the panel. The code pane follows a run: the clock's effect and the button's handler as a flight starts or stops, the readout's countdown branch beside the tick's countdown half while counting down, and the tick's flight half in flight, gliding between them and pulsing exactly the lines each round ran, where it scrolled once and tinted the whole tick; it stays still while the reader is in it. Its four counters sit two by two under the rocket, so the code beside them shows about 22 lines, where it showed 13. Its flow diagram draws every path the code takes, adding the clock's write that ends a countdown or a flight, which was missing. The dashed throttle read marches while the clock reads it in flight, where it lit only when the throttle moved mid-flight although that never runs the clock. A change sends a small square along each edge it ran along, easing over 260 ms, one layer every 180 ms, while the lines stay gray, where a dash raced along each edge in 140 ms and flickered during a scroll; no two edges merge into one line. A click or a throttle move travels with the changes it caused, where it followed them a round later and the clock took the credit, a square reaches the readout only from what the readout was reading, and a key under the diagram says what a solid and a dashed line mean.
- Docs site: the home page's section headings are a step smaller, so the hero leads. The speed heading states zerolith's place among the frameworks in the run, for example "Tied for fastest of eight frameworks", where it gave the extra time against hand-written JavaScript, which moves into the sentence under it. The packages are a ruled list with what each gives and its own gzipped size, measured on every build without the core, where they were a grid of icon cells, and the core's size budget and the counts the build holds join that section, where they had one of their own.
Fixed
zerolith:whenrefuses a function as its condition at compile time, with a message that asks for the computed value, where only the run time refused it withwhen_condition_function.zerolith: a keyed list checks every key of an update before it changes a row, so an update with a repeated or missing key fails witheach_key_duplicateoreach_key_undefinedand leaves the list as it was, where the rows before the bad key had already changed: some updated, some added, and the ones it would remove still there.@zerolith/router: a page whose view throws during a navigation leaves the outlet empty at the new URL, under the new page's head, and the navigation fails with the view's error as before, where the page before stayed on screen under a URL it did not match. A view that throws while no navigation runs, as when the outlet is first mounted, still throws from the mount.@zerolith/state: ahistoryover astoredsignal forgets its checkpoints when another tab writes the key, where an undo went back to a value from before that tab's change and took the change back in every tab without a word.@zerolith/cliand thezerolithcrate: the starter's footer names the inspector's shortcut only while developing, throughimport.meta.env.DEV, where a production build said to press Alt+Shift+Z for an inspector it does not include, and the starter's.gitignoreleaves out.vitest/, where Vitest keeps the screenshots of failed browser tests.@zerolith/cliand thezerolithcrate:zerolith devprints a module that fails to compile in the browser's console as well as the terminal, with its file, where the browser showed only that the module importing it lacked an export, since a module that imports a name from a failed one fails before the failed one runs. A page that opens while the error stands is to...
1.1.0
A minor release from main: the first @zerolith/state and @zerolith/browser, retry and timeout for resource, the diagnostic exports a package warns with, and the docs site's redesign and narrow-screen fixes. It holds every fix of 1.0.1.
Added
@zerolith/state, signals with a memory over the core alone:stored(key, initial)is a writable signal kept inlocalStorageorsessionStorageas JSON and set again when another tab writes the key;history(signal)gives undo and redo from explicit checkpoints, withcanUndoandcanRedoas signals;debounced(signal, ms)andthrottled(signal, ms)follow a signal after a quiet spell or at most so often; andselector(signal)gives each key a signal that is true for the selected one, so a selection among a thousand rows updates two. Each timer and listener is disposed with the enclosing owner. Four warnings join the catalog:state_storage_unreadable,state_storage_unavailable,state_value_not_json, andstate_key_duplicate.@zerolith/browser, the page as signals:online(),visible(), andwindowSize()are one signal each for the page, following the browser's own events;size(target)andinView(target, options?)keep an element's content box and whether it is in the viewport through observers that stop with the enclosing owner, and take the element or the signal areffills. Each is still on the server: online, visible, and no size. A threshold outside 0 to 1 or a root margin the browser refuses fails within_view_options_invalid, which joins the catalog.zerolith:resourcetakestimeout,retry,backoff, andretryIf, and givesattempt. A run of the loader past the timeout fails withresource_timed_out, which joins the catalog, and its abort signal fires with that error. A run that fails runs again as many times asretrysays,Infinityto keep trying, whileretryIfsays the failure is worth it; the first wait isbackoff, 250 milliseconds by default, and each wait after it doubles until it reaches 30 seconds. A waiting retry is dropped when the params change, onreload(), and when the owner is disposed. The resource stays pending through the retries, andattemptcounts the runs.zerolith:diagnostic(code, severity, args?)andreport(record)are exported, the way the runtime builds and reports its own warnings, so a package can warn with a catalog code andonDiagnosticlisteners hear it;hasOwner()tells whetheronDisposehas an owner to register with, so a package ties a cleanup to the enclosing owner when there is one and lives for the page otherwise.
Changed
- Docs: the site shows two releases when a pre-release is newer than the stable one: the stable release at the root, built from its line branch when one exists, and the pre-release under
/next/, built from its tag, with a switch in the header between them at the same page where it exists and at the other release's home otherwise; a diagnostic code only the pre-release has gets a page at the root that sends the reader to/next/, so the URL a pre-release prints never comes back not found. Every release deploys the site again, as before 1.0.0. - The core's size budget grows by 512 bytes after gzip, to 11,264 for the production bundle and 14,848 for the modules a buildless page loads, for retry and timeout in
resource. - Docs site: the home page leads with launch control running beside its code on a chartreuse plane. Press Launch and steer the throttle, and a diagram of its signal graph draws each change from the event through the signals, computed values, and the clock effect it ran to the DOM it reached, while a readout counts the text nodes and attributes each change touched, the elements created, which stay at none, and the one call of the component. A tour shows signals, templates, routing, forms, and server rendering, each tab a guide's example that runs as a test beside what it produces, each result drawn from the values its test asserts so the docs build fails when the test no longer holds them; then the newest benchmark scores as ranked bars,
zerolith doctorreportingtemplate_frozen_readwith the message the command line prints, the packages, four measured numbers with the check behind each, and the create command again beside the playground's other examples. The sections alternate full-width grounds and widths, showcase panels sit on hard offset slabs, and the revision history moves to the changelog and the release notes. Buttons, cards, and arrow links answer the pointer with short transitions, which stop under reduced motion. - Docs site: the benchmarks page sums the newest run up in one sentence over the ranked bars, zerolith's score set large beside its place among the frameworks and the release before, all computed from the run, and the bars now hold the run's date, machine, and method beside them; every operation, memory, size, and first paint, and every run follow on alternating grounds, and the method closes the page at reading width. Pages other than home open with their title on the page ground, a chartreuse wire running in from the left edge to a square at the title.
- Docs site: the playground opens with its title, the example picker, and the actions in one row, over the editor's key hints; the picker is a listbox of its own that shows each example's summary and works by pointer and keyboard (the arrow keys, Home, End, Enter, Space, and Escape), over the native select that a page without JavaScript keeps, and the stray frame beside it is gone. The table of contents beside a guide marks the section being read as the page scrolls. Code blocks in guides carry a tab naming their language with the copy button in it, and code blocks, file views, and tables sit on offset slabs, table headers on the surface, and callouts take their status color for their frame. Guide, tutorial, reference, and diagnostic pages set their text at 17 px with more space between lines and paragraphs and smaller inline code. The command line guide's importmap section is split into what the command writes, what it follows, its preloads, integrity, and source maps, and the checks it makes before writing, and long paragraphs in the routing, signals, and command line guides are split where they turn.
- Docs site: at 960 px and narrower the menu button opens a sheet in place of the page: the sections as large rows, the current one on the chartreuse plane and underlined, and on a reading page the search and its page list under them. Escape or the button closes it and brings the page back where it was; following a link from it, or going back or forward, closes it first, so the page left keeps its place in history and the page reached scrolls to its target. It closes too when the window widens past 960 px. The home page's flow diagram runs top to bottom there, with the same edges and sparks, where it scrolled inside its panel, and the figure's code wraps and scrolls in a window that ends on a whole line. From 961 px the wide drawing fits its panel, where up to about 990 px it scrolled inside it.
- Docs site: the benchmarks page leaves out the release before that each run measures beside the newest release: its score moved from one session to the next, and a run from a line branch could measure an older release than the one before. The runs and the benchmarks README still carry it.
Fixed
- Docs site: the playground's example list stays inside the page at every width, where below about 1,000 px it reached past the right edge and the page scrolled sideways: it takes the header row's width, up to 640 px, under the picker where that fits. The example field fits a 320 px screen.
- Docs site: the home page's benchmark sentence names the release the run measured, where it named the site's own release, which is newer from a release until the benchmarks workflow measures it, and counts the frameworks from the run.
- Docs site: following a link to the page already shown no longer wires the header's buttons and the tabs a second time, which made the theme toggle switch twice on each press, so it seemed to do nothing, and the arrow keys move two tabs at once.
- Docs site: the switch between the stable and pre-release docs loads the other release's site in full, where the page router rendered the other release's page with this release's stylesheet and scripts.
zerolith:resource'sreload()runs the loader untracked, so an effect that calls it no longer follows the signals the loader reads, and does nothing whileparamsthrows, where it loaded the last params that did not.
Install
npm install zerolith@1.1.0
npx @zerolith/cli@1.1.0 new my-app
cargo install zerolith --version 1.1.0 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
1.0.1
A patch release of the 1.x line with the fixes to @zerolith/client and @zerolith/forms found after 1.0.0. The one most apps will notice: @zerolith/client sends query strings on every browser the support policy admits, where 1.0.0 dropped them on Safari before 17, Chrome before 113, and Firefox before 112.
Changed
- Docs: the site shows two releases when a pre-release is newer than the stable one: the stable release at the root, built from its line branch when one exists, and the pre-release under
/next/, built from its tag, with a switch in the header between them at the same page where it exists and at the other release's home otherwise; a diagnostic code only the pre-release has gets a page at the root that sends the reader to/next/, so the URL a pre-release prints never comes back not found. Every release deploys the site again, as before 1.0.0.
Fixed
@zerolith/client: a query string is sent on browsers whoseURLSearchParamshas nosize, Chrome before 113, Firefox before 112, and Safari before 17, all admitted by the support policy, where every query string was dropped.@zerolith/client: aFormDatabody keeps its multipart boundary when the client's or the call's headers set acontent-type, which the client now leaves out for it.@zerolith/client: aReadableStreambody on a platform that would send it as the text[object ReadableStream], such as Firefox and Chrome before 105, throws aTypeErrorthat asks for aBlobor anArrayBuffer, where it sent that text. A platform that keeps the stream in the request goes on as before: Deno sends it, and Safari'sfetchrefuses it with an error of its own.@zerolith/client: a 2xx JSON body that is empty or holds only the whitespace JSON allows, spaces, tabs, and line breaks, as a HEAD response's or a server's empty result does, resolvesundefined, as the guide says, where it rejected with aSyntaxError.@zerolith/client: a HEAD response is read without its body on every transport, as RFC 9110, section 9.3.2, gives it none andfetchreads it: JSON or text resolvesundefined, any other type an emptyBlobof its type, and a failed one rejects with the problem its status line gives, where a body thatportTransportor another transport brought back was read and resolved, or read as the problem.@zerolith/client:generateClienttypes a HEAD operation's output asundefinedwhere its document gives the response a JSON or text body, which a HEAD response never carries and the client resolves asundefined, where it typed the body's schema orstring; a binary type staysBlob, the empty one the client resolves.@zerolith/client: a problem member of the wrong type, such as a stringstatusor a numerictitle, is left out, as RFC 9457, section 3.1, requires, where it replaced the response's status or became the error's message.fieldsis read as lists of messages, as@zerolith/formsreads it, and left out when it is not an object, such as a string,null, or an array, where any value reached theFieldErrorsthat promise lists of strings.@zerolith/client: an operation or a path parameter whose name onlyObject.prototypeprovides, such asconstructorortoString, or whose value is a function, such as a class'sconstructoror a method, counts as missing, socall("constructor")throws the unknown-operationTypeErrorwhere it crashed and a{toString}or{constructor}placeholder throws where it was filled with the text of a function; anullparameter counts as missing too, where it went as the textnull. Any other name is read as before, a getter on a class, an inherited route table, and a proxy'sgettrap included.@zerolith/client: a bodyJSON.stringifyturns into nothing, a function, a symbol, or a value whosetoJSONreturnsundefined, throws aTypeError, where it went as no body under a JSON content type.@zerolith/client: a whole query that is iterable, such as aURLSearchParams,FormData,Map,Set,Headers, array, or typed array, or that is aDateor a non-empty string, throws aTypeErrornaming its kind, where its values were dropped or sent as numbered keys; any other value is read by its own keys, as before.@zerolith/client: anundefinedornullitem of a query array is left out, as the guide says of query values, where it went as the textundefinedornull.@zerolith/client: a query's request aborts with the reason of the signal that aborted it, the caller's or the resource's own, where it aborted with a newAbortError, and thesignaloption's documentation says the two are joined rather than replaced.@zerolith/client:portTransportremoves its abort listener when the answer arrives, so a request's signal no longer keeps one listener and the request it closes over after the call, and rejects a call whose answer cannot become aResponse, such as one with a status outside 200 to 599, with the error the platform throws, where the call stayed pending for good.@zerolith/forms: a problem is read as@zerolith/clientnow reads it: a member of the wrong type, such as a numerictitleor a stringstatus, is left out, as RFC 9457, section 3.1, requires,statuscomes from the response when the body has none, and afieldsthat is not an object is left out, where a string's characters or an array's indexes became fields andnullended the submission inerrorwith aTypeError.@zerolith/client: a problem sent asApplication/Problem+JSON, or in any other casing, is read, since media types are case-insensitive (RFC 9110, section 8.3.1), where the error carried the problem the status line builds.@zerolith/forms: a 2xx JSON body is read whatever the casing of its media type, whereApplication/JSONcame back as text, and one that is empty or holds only spaces, tabs, and line breaks givesresultundefined, as@zerolith/clientnow reads it, where the submission ended inerrorwith aSyntaxError.@zerolith/client:servePortanswers with its 500 problem when the handler resolves to something other than aResponseor answers with a body that cannot be read, such as a stream that errors, where no answer went and the call waited for good.@zerolith/client: everyportTransportdraws its request ids from one counter, so several can share a port, where two transports numbered their calls from 1, so one answer settled both calls under the same number and the other answer was dropped.@zerolith/client:servePortsends no body in its answer to a HEAD request, a failure's 500 included, and cancels the handler's body unread, as RFC 9110, section 9.3.2, requires of a server, where the handler's body crossed the port and one that could not be read turned the answer into a 500.
Install
npm install zerolith@1.0.1
npx @zerolith/cli@1.0.1 new my-app
cargo install zerolith --version 1.0.1 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
1.0.0
The first stable release. The API of zerolith, zerolith/ssr, zerolith/element, zerolith/jsx-runtime, @zerolith/router, and @zerolith/router/server is frozen through RFC 0003: the reports in api/ are its text, and the support policy says how long the line gets fixes. The other packages and the command line are frozen at 1.0 as well: every package carries one version under semantic versioning, so a breaking change in any of them comes only in a major, after the deprecation notice the versioning policy describes. Packages publish under latest, so npm install zerolith and npx @zerolith/cli new my-app need no tag. The changes since 0.1.0-alpha.10 follow.
Added
@zerolith/cli: a proxy rule inzerolith.jsontakesrun, a command such asnpm run apithat starts its backend:dev,serve, andpreviewrun it in the project folder through the shell when they start, show its output in the same terminal, and stop it with every process it started when they stop, so one command runs an app and its backend; the same command under several prefixes starts once, and--no-backendsleaves the backends to you, as when one runs under a debugger. A backend that ends on its own is reported under the banner, and a termination signal or a hangup stops the server as Ctrl+C does, backends included. The fullstack example and the projectzerolith new --example fullstackwrites namenpm run apithis way, sonpm run devruns both, and in this repositorynpm run fullstackdoes.@zerolith/a11y:inertOutside(element)makes everything outside an element inert, the siblings of the element and of each ancestor up to the body, through a shadow tree's host, and returns what undoes it, so a drawer or a menu that slides over the page keeps the page behind it from focus, clicks, and assistive technology while an effect holds it open. An element inert on its own is left alone, one two holds share stays inert until the last lets go, and an element given before its template is in the page is held once it is; a native<dialog>opened withshowModal()needs none of it.
Changed
-
@zerolith/a11y: a focus trap acts only while its element is rendered, and its initial focus waits for that; the trap of a closed native<dialog>or of ahiddendrawer had pulled focus into the hidden element, stamped atabindexon it, and stood in the way of an older trap. -
Docs: the benchmark scores measure every framework against hand-written JavaScript, which is 1.00 by definition, so a score says how many times its time a framework takes and a cell below 1.00 is faster than it; they had measured against the fastest framework at each operation, as the official results page does, which put hand-written JavaScript at 1.01 whenever another framework was faster at one operation. The ranking does not change, since every score moves by the same factor.
Fixed
@zerolith/cli:importmap --htmlandbuild --unbundledremove the vendored files an earlier run left only after the pages are written, so a page write that fails leaves every file the page still names, where the files went first; a symbolic link to a directory left in a vendored package folder is removed as a link, where removing it as a file failed on Windows; and an entry of a page's own import map written as a relative URL, such as../vendor/zerolith/dist/old.js, is read against the page, so it is dropped like a root-relative one when the run no longer vendors its file, where it had stayed and failed to load, and so is an integrity hash the page held for a file the run vendors, which had come before the fresh hash in the map and made the browser refuse the module.
Install
npm install zerolith@1.0.0
npx @zerolith/cli@1.0.0 new my-app
cargo install zerolith --version 1.0.0 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.
0.1.0-alpha.10
Breaking changes
@zerolith/router:href()throwsroute_name_unknownfor a name the route table does not hold, where it returned the base path, so a JavaScript project cannot ship a link to the home page in place of a misspelled one; a TypeScript project saw the mistake at type-check time already. The error names the route given.@zerolith/router: the document head is applied as a whole on every commit.createRoutertakeshead, the fields every page starts from, as an object or as a function of the URL and the matched route's name, undefined for the fallback, so the not-found page can have a title of its own; a route's fields go over them one by one, and a field neither gives goes back to what the page loaded with, the title and description written inindex.html. A route that left out a field had kept the previous page's value, and the fallback had kept the previous title.@zerolith/router: a link or anavigate()to the URL of the page shown keeps the page, as back and forward between its entries already did: no loader runs and no view renders, the history entry follows the link, and withscrollon the page moves to the hash or the top. Rendering the page again had lost its state, such as an open<details>or the focus. A navigation whose query differs is another page, and one to the URL shown while another navigation loads cancels that one.navigate(url, { reload: true })loads and renders the page shown again on purpose, as after a form's redirect back to it; a form'snavigateoption that passes the router'snavigatethrough should passreloadwith it.zerolith: a diagnostic'surlends with a slash,https://zerolith.dev/e/<code>/, the address of the code's page, where it ended without one and relied on a redirect; the command line's messages do the same.Diagnosticno longer declareslocationandsuggestion, andDiagnosticLocation,DiagnosticSuggestion, andApplicabilityare no longer exported, since no emitter filled them; they return as additions with the first emitter that does.effect_cycleleaves the catalog, since no path raised it.zerolith: a write inside a computed function fails withsignal_written_inside_computedwhatever runs the write,untrackedor an effect started inside the function included; it had checked only the tracking state, whichuntrackedclears.zerolith: the JSX types check the namespaces against the element, as RFC 0001 says they do, so TypeScript code that passed a value a namespace does not take fails to compile where it compiled before; the runtime is unchanged.prop:takes the type of the element's own property, or a signal of it,on:takes a handler of the DOM event withcurrentTargettyped as the element, soevent.currentTarget.valuereads on an input and fails on a div,bool:takes a boolean or a signal of one, andreftakes a function of the element or a writable signal. Every namespace had takenunknown. A plain attribute still takes any value, and an element TypeScript does not know, such as a custom element, takes any property, handler, and flag.Bindable,Ref,EventHandler,PropertyAttributes,EventAttributes,BooleanAttributes,KnownElements, andAnyElementAttributesare exported fromzerolith/jsx-runtimefor a component that forwards attributes.zerolith: what a view function returns is typed asRenderable, aView, text, a number, a bigint, a boolean,null,undefined, a node, a signal of one of these, or an iterable of them, whereRowOf,when's branches,ElementOptions.view, and the router'sRouteViewandfallbackhad returnedunknown;render,hydrate, andrenderToStringtake aRenderableor a function returning one, where they tookunknown. A component passed where a view is expected now fails to compile;html's values andwhen's condition stayunknown, as their positions allow.zerolith: arefbound tonullorundefinedhands the element to nothing, as anullhandler adds no listener, wherenullhad failed withref_not_assignableandundefinedhad been ignored; any other value still fails. A binding inside a<template>element fails at template creation withbinding_in_template_element, in the runtime, the Vite plugin, and the command line, where the server rendered it and the client never bound it.@zerolith/router:Matchtakes the route table as a type parameter,Match<typeof routes>, androuter.currentis typed with it: a union over the table's names, each with the parameters its path names and the data its loader resolves to, so a check ofnamenarrowsparamsanddata, wheredatahad beenunknownfor every route. BareMatchkeeps its shape, andDataOfandRouteParamsare exported with it. A route path the matcher cannot read as written, a*restnot at the end, a second*rest, a parameter without a name, or a modifier such as?after one, failscreateRouterwithroute_path_invalid, where it was matched as something else in silence.
Changed
@zerolith/router:readysettles on the first navigation that commits, the initial one or a newer one that replaced it, and rejects with the first failure before that, where it had fulfilled when the initial navigation returned, canceled or not;dispose()during a pending navigation clearspending; a back or forward whose navigation fails moves the history back by the distance it moved, so the address bar shows the page still shown;matchescompares paths without a trailing slash and without a query or hash, so/settings/and/settings?tab=1are current at/settings, and the base root is current at the base without its slash; and the embedded route state is consumed by the initial navigation whatever it resolves to, where a fallback or a failed lazy import had left it for a later navigation, and a record withoutdatais not one.zerolith: aDocumentFragmentbound as content keeps its nodes, so the position can show another value later, where the emptied fragment had been kept and replacing it threw;hydraterepairs a node bound as content with astructuremismatch, where it had failed withunrenderable_valuefor a valuerenderaccepts;renderToStringreads markers where the scanner placed them, so static text or an attribute value that readszl$0renders as it is, where the renderer had rewritten it; and the server hands a row function its index signal only when the function takes two parameters, as the browser does.- Docs: the API reports print a
consttype parameter modifier and a symbol-keyed property as the source declares them, sodefine<const Props …>,createRouter<const T …>,route<const Path …>, andView's[VIEW]brand read as they ship. zerolith: an effect whose first run only made another effect throw is kept and given to its owner, where it was disposed as if its own function had thrown; an effect that loops is disposed whichever run loops, where a loop on a later run had left it running 101 rounds on every write; and when a function and an effect its writes triggered both throw, inbatch, in an effect's run, or in its cleanup, the two arrive as oneAggregateError, where the flush error had replaced the function's. An effect function that returns a promise is reported aseffect_async, a warning, since its reads after the firstawaitare not tracked and the promise is no cleanup.zerolith: aresourcewhoseparamsfunction throws fails the load as a loader failure does, withstatuserrorand the error inerror, where the error had reached whoever wrote the signal;reload()after the owner is disposed does nothing, where it started a request nothing aborted; and an emptyargsno longer leaves a bare?on a diagnostic's URL.@zerolith/cliand thezerolithcrate:zerolith importmapandzerolith build --unbundledcopy each package'sLICENSEnext to itsVERSIONfile, since a site that commits and deploysvendor/redistributes the packages, and writevendor/.gitattributes, which turns line ending conversion off so a checkout on Windows does not fail the modules' integrity hashes, andvendor/.gitignore, which keeps the packages'dist/folders in commits whatever the project's owndist/rule says. A page gets its import map before its first module script, where it had gone before</head>, after a module script already there, which the browser then ignored the map for; a page with CRLF line endings keeps them, where every run had marked it changed; and the preload links are indented like the map line, where they had started at the margin.@zerolith/cliand thezerolithcrate:zerolith devandserveserve a module under a folderzerolith importmapwrote as it is, so the integrity hash the page's import map gives it holds; they had rewritten its imports as they rewrite the project's own, which failed the hash on every load.@zerolith/cliand thezerolithcrate:zerolith importmap --htmlfollows the page's module scripts, inline ones included, through their relative and root-relative imports, and maps and vendors the entries they import, statically or throughimport()of a literal or of aconstholding one, with the modules those load; the entries the page does not reach, such aszerolith/ssrand@zerolith/router/serveron a page rendered in the browser, are left out of the map, and a package it does not reach is left on disk as it is, for a page the run was not given.--htmlcan be given once per page, which vendors what any of them reaches and writes each page's own tags. The preload links name the modules the page's entries load statically and the vendored modules they import, where they had named every module every entry of every package loads and none of the page's own. A page whose modules load another through animport()the command cannot follow, or that import nothing fro...
0.1.0-alpha.9
Rows hold less memory: with 1,000 rows of the js-framework-benchmark entry, the page keeps about 11% less than with 0.1.0-alpha.8, both measured the same way on one machine, since a signal bound into a template no longer holds an effect function and its closure, a binding that is its element's only child adds no comment marker, and a row holds fewer objects and arrays. The core's production bundle is 10,315 B after gzip, 418 B more than 0.1.0-alpha.8's, and its budget grows to 10,752 B from 10,240 B to make room for this work. Each release is now measured on js-framework-benchmark once it is published, beside the release before in the same session, and https://zerolith.dev/benchmarks/ shows the results, this release's memory among them.
Changed
zerolith: a signal written into a template's content, as${count}, or as the whole value of an attribute or a property is bound by a reaction that holds the signal and the part it writes to, where it had been an effect whose function closed over both, so each such binding is one object in place of an effect, a closure, and the closure's scope. A row of the js-framework-benchmark entry holds two closures fewer, so 1,000 rows keep 126 KB less, asperformance.measureUserAgentSpecificMemory()reports them after garbage collection, the medians of seven loads on one machine. A binding still tracks what writing its value reads, owns what that creates, and ends with its owner.npm run bench:comparemeasures appending 1,000 rows 6% faster than 0.1.0-alpha.8 and every other operation within 1.5% of it.zerolith: an owner keeps the effects, bindings, scopes, andonDisposecallbacks it owns in a list that runs through them, where it kept an array, which V8 sizes for 17 entries on the first push, so a keyed row's owner, which holds a few, takes 79 B less, 79 KB for 1,000 rows. Disposal still runs newest first and reaches every child when one throws, and a child disposed with its owner lets go of the older ones, so a child still referenced elsewhere keeps none of them alive.npm run bench:comparemeasures every operation within 3.2% of the core before it.zerolith: a binding that is its element's only child, as${label}in<a>${label}</a>, adds no comment marker to the page. It shows text, or nothing, in the one text node its element holds, and adds its marker after that node only when it first shows a template, a list, a node, or raw markup. A row of the js-framework-benchmark entry now holds the nodes the vanilla entry's row holds, two comments fewer, so 1,000 rows keep 178 KB less, andnpm run bench:comparemeasures creating and appending rows about 3% faster. Server-rendered markup keeps its markers, which hydration adopts as before, and rows the client adds afterward carry none.zerolith: a template instance with one top-level node, as a keyed row usually is, holds that node with no array around it, and so does the roweachkeeps for it, so 1,000 rows keep 26 KB less.zerolith: a binding on an element is a part of the class its kind needs, so the part of an attribute, a boolean attribute, or a property holds no listener or ref fields; 1,000 keyed rows with a bound class keep 16 KB less.zerolith: a row ofeachis also the owner of what its view creates, where it held a separate owner, so 1,000 rows keep 9 KB less.- Each release is measured on js-framework-benchmark once it is published. The benchmarks workflow measures it next to hand-written JavaScript and the frameworks
benchmarks/frameworks.jsonlists, along with the release before, built from that release's own entry files, in the same session on the same machine, and commits the run tobenchmarks/results/, from which the benchmarks README and the docs site render their results. Memory, size, and first paint, which the benchmark samples once a pass, are the medians of five passes. The entry inbenchmarks/js-framework-benchmark/is pinned to each release as its run lands. - Docs site: a benchmarks page, https://zerolith.dev/benchmarks/, linked from the header and the home page, shows the newest release's run: each framework's score, ranked, with a bar for its extra time over the fastest framework at each operation and zerolith's release before ranked beside it, its bar outlined, when the run measured it; then each operation's median time, memory, size, and first paint, the scores of every run kept so far, and how the runs are measured. Its Markdown twin,
/benchmarks.md, carries the results as the benchmarks README renders them. - Docs site: the playground's async data example fetches each city's record, a JSON file the site serves beside the playground, where it had waited on a timer to stand in for a server, and aborts a request that a newer choice replaces; Atlantis, which has no record, fails with the server's own 404. The card shows the city's local time, ticking in the city's time zone, the time zone, the population with the year it was counted, the area, the density, the coordinates, the currency, the official languages, and the calling code, formatted for the reader's locale with
Intl, and names its sources: Wikipedia and the official statistics it cites. - Docs: the benchmarks README leads with the newest release's results, which
npm run bench:reportwrites from the runs inbenchmarks/results/, followed by every run's scores, where it had added each run below the earlier ones; the roadmap marks the per-row memory work done so far and names what remains.
Fixed
- Docs site: a page exactly as tall as the window, as the playground is, no longer scrolls by 17 px. The screen reader announcer at the end of every page is visually hidden, but kept a paragraph's margins; it now has the -1 px margin of the visually hidden pattern. The playground also fills the window to its last 2 px, which it had taken off for the header's rule a second time.
Install
npm install zerolith@0.1.0-alpha.9
npx @zerolith/cli@0.1.0-alpha.9 new my-app
cargo install zerolith --version 0.1.0-alpha.9 --lockedEvery npm package is published with provenance, and the CycloneDX SBOM of the release is attached.