diff --git a/.formatter.exs b/.formatter.exs index d2cda26..5e3c114 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,17 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + plugins: [Volt.Formatter], + inputs: [ + "{mix,.formatter}.exs", + "{config,lib,test}/**/*.{ex,exs}", + "priv/ts/{client,compilers,test}/**/*.ts" + ], + volt: [ + trailing_comma: :none, + tab_width: 2, + semi: false, + single_quote: true, + print_width: 100, + arrow_parens: :always + ] ] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b66b59e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +priv/ts/npm.lock linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..786f701 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + elixir: + uses: elixir-vibe/actions/.github/workflows/elixir-ci.yml@v1 + with: + latest-elixir: "1.20" + latest-otp: "29" + min-elixir: "1.18" + min-otp: "27" + deps-command: | + sudo apt-get update + sudo apt-get install --yes inotify-tools + mix deps.get + mix volt.priv.vendor priv/ts + npm ci + npx playwright install --with-deps chromium + latest-command: MIX_ENV=dev mix lint && MIX_ENV=test mix test + min-command: mix compile --warnings-as-errors && mix test diff --git a/.gitignore b/.gitignore index c6598cf..21b1a10 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ volt-*.tar # Git worktrees for parallel agent work .worktrees/ +/priv/ts/node_modules/ diff --git a/.reach.exs b/.reach.exs index 979759b..96382fd 100644 --- a/.reach.exs +++ b/.reach.exs @@ -10,17 +10,19 @@ compat_json = [":json.*"] unsafe_runtime_encoding = ["Jason.encode!"] unstable_hashing = [":erlang.phash2"] builder_runtime = ["Volt.DevServer.*", "Volt.Watcher.*", "Volt.HMR.*"] -asset_runtime = ["Volt.Assets.*", "Volt.Builder.*", "Volt.DevServer.*", "Volt.JS.*"] +asset_runtime = ["Volt.Assets.*", "Volt.Builder.*", "Volt.DevServer.*", "Volt.JS.*", "Volt.NPM"] ast_side_effects = app_config ++ file_mutation ++ ["System.cmd", "QuickBEAM.*"] ++ builder_runtime adapter = [ "Mix.Tasks.Volt.*", "Volt", "Volt.Formatter", + "Volt.NPM", "Volt.Test.*" ] orchestrator = [ + "Volt.Build", "Volt.Builder", "Volt.Builder.Collector", "Volt.Builder.Compiler", @@ -32,25 +34,36 @@ orchestrator = [ "Volt.JS.Vendor", "Volt.Pipeline", "Volt.Tailwind", - "Volt.Watcher" + "Volt.Tailwind.Build", + "Volt.Watcher", + "Volt.Dev.Session.Watcher", + "Volt.Watcher.Ignore", + "Volt.Watcher.Path" ] model = [ "Volt.Builder.BuildContext", + "Volt.Builder.Asset", "Volt.Builder.Bundle", "Volt.Builder.Collector.State", "Volt.Builder.Context", + "Volt.Builder.Compiled", "Volt.Builder.Dependencies", "Volt.Builder.ManifestEntry", "Volt.Builder.OutputContext", "Volt.Builder.OutputFile", "Volt.Builder.Result", + "Volt.Build.Result", + "Volt.Build.Layout", + "Volt.Config.Tailwind", "Volt.ChunkGraph.Chunk", "Volt.Config", "Volt.Config.*", + "Volt.Dev.Session.Tables", "Volt.DevServer.CacheEntry", "Volt.DevServer.Config", "Volt.HMR.Message", + "Volt.HMR.Channel", "Volt.JS.ImportExtractor.Result", "Volt.JS.PrebundleEntry.Export", "Volt.JS.PrebundleEntry.Import", @@ -58,12 +71,23 @@ model = [ "Volt.JS.TSConfig", "Volt.MIME", "Volt.Paths", - "Volt.Pipeline.Result.Hashes" + "Volt.Pipeline.Result.Hashes", + "Volt.Tailwind.CompileError", + "Volt.Tailwind.ResolveError", + "Volt.Tailwind.Context", + "Volt.Tailwind.Metadata", + "Volt.Tailwind.State", + "Volt.Tailwind.Compilation" ] logic = [ + "Volt.Builder.Artifact", + "Volt.Builder.CSS.Prepared", + "Volt.Builder.Declarations", + "Volt.Builder.Plan", "Volt.Assets", "Volt.Assets.Query", + "Volt.Builder.Naming", "Volt.Builder.Resolver", "Volt.ChunkGraph", "Volt.CSS.AST", @@ -73,12 +97,16 @@ logic = [ "Volt.CSS.Modules", "Volt.Env", "Volt.Format", + "Volt.HMR.Document", "Volt.HTMLEntry", "Volt.JS.AST", "Volt.JS.Check", + "Volt.JS.CommonJS", + "Volt.JS.Lint.Config", + "Volt.JS.Lint.TSConfigOverlay", "Volt.JS.Extensions", "Volt.JS.Format", - "Volt.JS.Helpers", + "Volt.JS.Discovery", "Volt.JS.ImportExtractor", "Volt.JS.Package", "Volt.JS.Patch", @@ -111,12 +139,24 @@ infrastructure = [ "Volt.Application", "Volt.Builder.CSS", "Volt.Builder.Writer", + "Volt.Builder.Publication", "Volt.Cache", "Volt.Dev", + "Volt.Dev.Cleanup", + "Volt.Dev.Prebundled", + "Volt.Dev.Error", + "Volt.Priv.Vendor", + "Volt.DevServer.ClientTag", + "Volt.Dev.Assets", + "Volt.Dev.Session.Call", + "Volt.Dev.Session.State", + "Volt.Dev.Session.Supervisor", "Volt.Dev.ConsoleForwarder", "Volt.Dev.ConsoleForwarder.Payload", "Volt.ETS", "Volt.HMR.Boundary", + "Volt.HMR.Documents", + "Volt.HMR.Errors", "Volt.HMR.GlobGraph", "Volt.HMR.ImportGraph", "Volt.HMR.ModuleGraph", @@ -131,7 +171,12 @@ infrastructure = [ "Volt.JS.Runtime.Entry", "Volt.JS.Runtime.Error", "Volt.JS.Runtime.Installer", - "Volt.Tailwind.Loader" + "Volt.JS.Runtime.PackageSet", + "Volt.Tailwind.Artifact", + "Volt.Tailwind.Loader", + "Volt.Tailwind.Runtime", + "Volt.Tailwind.Supervisor", + "Volt.Tailwind.Worker" ] plugin = [ diff --git a/AGENTS.md b/AGENTS.md index a17e330..ea133ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,7 @@ # Project Guidelines +- Match primary Elixir module namespaces to their source paths. Nested data modules, protocol implementations, conditional definitions, and established acronym spellings are deliberate exceptions. +- Mirror source paths and namespaces in unit tests. Keep cross-module scenarios under the owning subsystem or integration directory; do not invent a production module merely to match a scenario test. +- Keep Mix tasks in nested `lib/mix/tasks/` paths as `.ex`, tests as `_test.exs`, and shared support as `.ex` under `test/support/`, compiled only in the test environment. `lib/volt/test/` contains shipped framework APIs, not project test support. + - Prefer parser/AST-backed solutions over hand-rolled regular expressions for source code, markup, and structured formats. Use existing parsers such as OXC for JavaScript/TypeScript and Floki for HTML-like markup when available. diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f928a..bfa34bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,288 @@ # Changelog -## Unreleased +## 0.23.0 - 2026-10-08 + +### Added + +- `mix volt.build --declarations` and `config :volt, declarations: true` write a library's type declarations with its bundle: one `.d.ts` per entry, beside it and without a hash, holding what the entry exports and the types those exports reach. Modules are emitted through OXC's isolated declarations (oxc 0.19), so exported declarations need explicit types and the build fails naming the module that lacks them; the declarations are then bundled as `rollup-plugin-dts` does, with types-only modules included, imports of other packages hoisted, and same-named declarations of different modules told apart. `.vue` modules are declared by the Vue plugin ([#71](https://github.com/elixir-volt/volt/issues/71)). + +### Changed + +- Require oxc 0.19 and quickbeam 0.11.3. + +## 0.22.1 - 2026-10-07 + +### Fixed + +- Start Volt's application before the browser a test run shares, which lives under its supervisor. A project that depends on Volt with `runtime: false` had to start it in `test_helper.exs`, or browser tests failed with `Volt.Supervisor` not running. + +## 0.22.0 - 2026-10-07 + +### Added + +- `mix volt.js.check --type-aware` types imports of `.vue` files by the component's own declaration. The Vue plugin generates it from each checked `.vue` file with `Vize.generate_dts/2` and Volt sends it to tsgolint as `.vue.d.ts`, which TypeScript resolves an import of the `.vue` file to. Props, emits and slots of imported components are then checked in `h(Button, {...})` and `InstanceType`, and no `declare module "*.vue"` shim is needed for checked files. Plugins declare their own files through the new `c:Volt.Plugin.declaration/3` callback ([#63](https://github.com/elixir-volt/volt/issues/63)). + +### Changed + +- Browser tests share one browser per run, in `Volt.Test.Browser`, and each test gets a browser context of its own. Every test started a Playwright driver and launched a browser for itself, because the driver was linked to the test process and went away with it, which cost more than running the test. A test file is now bundled once for all of its tests and bundled again only when one of its source files changes, and the ExUnit modules generated for browser tests are `async: true` ([#65](https://github.com/elixir-volt/volt/issues/65)). + +### Fixed + +- Give vendor modules a new URL when a package changes. The dev server serves `/@vendor/.js?v=` as immutable, and the hash covered the lockfiles and plugins but not the packages, so a package that changed in place, such as a linked one, kept its URL and browsers kept their copy of it. The hash now also covers the generation of the pre-bundle, which changes whenever a package is bundled again, and a compiled application module is compiled again when the hash it was compiled with is no longer current: after a lockfile change, unchanged modules kept importing the old URL, which answered 504. Packages linked from outside `node_modules` are watched, and a change bundles them again and reloads the page. `npm.lock`, the lockfile of `mix npm.install`, now counts as a lockfile; it was not read, so an install did not change the URL either ([#67](https://github.com/elixir-volt/volt/issues/67)). +- Treat Node built-in subpaths such as `timers/promises`, `fs/promises` and `stream/web` as built-ins, and read the `browser` field of packages. Bundling a package that requires `timers/promises`, such as `@sinonjs/fake-timers`, failed with `{:not_found, "timers/promises"}` even though its `browser` field maps the specifier to `false`. Specifiers a `browser` field maps to `false` now resolve to an empty module, and those mapped to a path resolve to that path, as in esbuild and webpack. The builder, the dev server and the Tailwind resolver share the built-in check ([#64](https://github.com/elixir-volt/volt/issues/64)). + +## 0.21.0 - 2026-10-05 + +### Compatibility + +- Require vize 0.17.1, which fixes three Vapor compiler bugs. Two can affect components compiled with `vapor: true`: static attribute values were decoded twice in Vapor templates, and default slot content beside a named `