Skip to content

v0.20.0

Choose a tag to compare

@dannote dannote released this 03 Oct 12:57
· 33 commits to master since this release
001f84c

Breaking changes

  • Formatter options moved from config :volt, :format to the :volt key of .formatter.exs, where mix format plugins keep their options. config :volt, :format now only holds the build output format (:iife, :esm, or :cjs), so both can be set. A keyword list under config :volt, :format raises an ArgumentError with the options to move:

    # .formatter.exs
    [
      plugins: [Volt.Formatter],
      volt: [semi: false, single_quote: true]
    ]
    

    The formatting-only :root, :sources, and :ignore overrides for mix volt.js.format and mix volt.js.check move to the same key. .oxfmtrc.json and .prettierrc.json are still read when there is no :volt key. mix igniter.install volt writes the options to .formatter.exs.

  • External modules stay as imports in :esm and :cjs output, as in Rollup, Rolldown, esbuild and Bun. With format: :esm, external: ["phoenix"], import { Socket } from "phoenix" used to become const { Socket } = Phoenix;; it is now left for the browser or host to resolve, for example through an import map. :cjs output uses require("phoenix"). IIFE output, the default, still reads externals from globals. The global names in external: %{"phoenix" => "Phoenix"} apply to IIFE output only. ESM builds that relied on page globals need an import map or format: :iife.

  • Volt.JS.Format.load_config/1 takes the .formatter.exs options instead of reading the application environment.

Compatibility

  • Require vize 0.17. Volt's use of vize is unchanged: 0.17 replaces Vize.vapor_split/1 with Vize.split_template/2, which Volt doesn't call. Projects that depend on vize themselves need 0.17 alongside this release.

Added

  • Changes in reload_dirs reload only the pages whose HTML changed. The watcher broadcasts a document update for them instead of a full one. Pages carry an entity tag of their rendered HTML; on a change the dev client revalidates with If-None-Match and reloads unless the server answers 304 Not Modified. Editing one post no longer reloads every open tab, and a save that does not change a page's output reloads nothing. This covers every successful HTML response that passes through Volt.DevServer, including pages that already load the dev client, so site generators built on Volt need no change. Volt.HMR.document_update/2 sends the same request from other packages, and Volt.HMR.Document describes the contract for servers that send HTML some other way.
  • config :volt, :server, morph: true patches a changed page in place instead of reloading it, so scroll position, focus and JavaScript state survive edits to text and classes. The client reloads as before when scripts or stylesheets differ. morph: [preserve: selector] names elements owned by client code, such as mounted components, which are left alone. The client dispatches volt:document-updated after a patch.
  • Stylesheet updates no longer flash unstyled content. The dev client adds a <link> for the updated stylesheet and removes the previous one once the new one has loaded, instead of changing href in place.
  • The dev client reloads the page once when several updates ask for a reload at the same time, such as a template that is also a Tailwind source.
  • The dev server converts local CommonJS and UMD files, such as Phoenix's vendored topbar.js, to ES modules, so import topbar from "../vendor/topbar" works in development as it does in production builds. .cjs and .cts files, which the dev server did not serve, are converted the same way.
  • Relative watch_ignored patterns also resolve from the project directory, so _build/** matches a watched directory inside _build.
  • Volt's client types declare import.meta.env, so TypeScript projects no longer need their own ImportMeta declaration for MODE, DEV, PROD and exposed variables. mix igniter.install volt adds env.d.ts to configurations that list declaration files explicitly.

Fixed

  • Collect every test generated by test.each and describe.each. Source lines were matched to tests by position, and tests without a detected line were dropped: a five-case table followed by two tests collected as two tests. Each case now maps to the line of its table, and tests registered in ways the source does not show, such as in a loop, are kept without a line.
  • Stop the dev server reloading pages in a loop when a watched file is rewritten with identical content, as Phoenix LiveView does for colocated hooks on every code reload. The watcher now compares file contents before rebuilding.
  • Load a single instance of each pre-bundled dependency in development. Pre-bundles imported their siblings and shared chunks without the ?v= hash that application modules use, so browsers loaded packages such as Vue twice and component libraries built on them failed to render.
  • Report why a development session is unavailable. A second watcher with different options, such as a Mix.Tasks.Volt.Dev entry in the endpoint's :watchers next to plug Volt.DevServer, answered every request with a bare 503; the response and the log now name the conflict and how to resolve it.
  • Log Pre-bundled N vendor package(s) only when packages are bundled. Phoenix initializes plugs on every request in development, so it was logged per request.
  • Read external globals once in code-split IIFE output. Each chunk also declared them at the top level of the script, where two chunks importing the same name would clash.
  • Stop watchers before the Tailwind processes they release their contexts to. Volt's supervisor stopped the Tailwind registry first, so every watcher with Tailwind enabled crashed with unknown registry: Volt.Tailwind.Registry on shutdown.
  • Skip tsconfig.json path mappings that only point at declaration files. A types-only mapping such as "topbar": ["./types/topbar.d.ts"] became a bundler alias and bundled the .d.ts file in place of the package.