Hi I was evaluating volt and wanted to see how it would fair with an existing project, what would break and what would require workarounds.
I had claude take a stab at it and found a few small issues with workarounds. And just to confirm they were issues, created a reproduction repo. I'll include all the issues here but can split them into separate issues if you'd prefer.
Repro repo: https://github.com/andr-ec/volt-repro-bugs (pinned to {:volt, "== 0.14.12"}, Elixir 1.20)
Each bug is an independent Volt profile, reproduced two ways: with mix volt.build <profile> (production) and against a live Volt.DevServer over HTTP (dev_server_check.exs). Source-line references are against 0.14.12.
1. CSS bundler can't resolve a bare (package) @import
@import "csslib/theme.css"; where csslib is a package (with an exports map for ./theme.css). Vite/LightningCSS resolve this into node_modules.
- Expected: built CSS contains the imported rule.
- Prod (
mix volt.build css_import): Build failed: ["CSS bundle error: Error { kind: ResolverError(Os { code: 2, kind: NotFound ... }), loc: ... styles.css, line: 2 ...]
- Dev (
GET /assets/src/css_import/styles.css): 500 compilation error.
- Cause:
Volt.Builder.Writer (lib/volt/builder/writer.ex:100) calls Vize.CSS.compile(css, minify: ...) with only a minify flag — no resolver, no conditions — and lib/volt/css/dependencies.ex only accepts relative @import. A bare specifier is never resolved against resolve_dirs.
- Workaround: pre-bundle package CSS with esbuild (
--conditions=style) and <link> the flat output.
2. Resolver ignores a subpath directory's package.json "main"/"module"
Importing dirlib/sub, where dirlib/sub/ is a directory containing only a package.json ("main": "../dist/sub.js", "module": "...", no index.js) and the parent has no exports map. Node, esbuild, and Vite all follow that nested package.json.
- Expected: the module resolves to
dist/sub.js.
- Prod (
mix volt.build dir_pkg): build succeeds silently but emits (function(e){console.log(e.SUB)})(dirlib_sub); — the unresolved specifier is externalized into an undefined global dirlib_sub, so it throws at runtime and the real export never lands. The silent-externalize-on-unresolved behaviour is the dangerous part.
- Dev (
GET /assets/src/dir_pkg/app.js): import rewritten to /@vendor/dirlib__slash__sub.js, which returns 404 vendor module not found: dirlib/sub.
- Cause:
NPM.Resolution.PackageResolver.resolve_without_exports/5 (deps/npm/lib/npm/resolution/package_resolver.ex:313, subpath clause :323) only probes <subpath>.<ext> and <subpath>/index.<ext>; it never reads a subpath directory's package.json main/module.
- Real-world trigger:
react-remove-scroll-bar/constants (a Radix transitive dep). Workaround: an explicit alias.
3. ?raw is ignored for .md
import md from "./note.md?raw". Vite honours ?raw for any extension.
- Expected:
md is the file contents as a string.
- Prod (
mix volt.build md_raw): Build failed: [%{message: "Invalid Character ..."}] — the .md is compiled as JavaScript.
- Dev (
GET /assets/src/md_raw/note.md?raw): 500, no module served.
- Cause:
Volt.MIME lists .md in @non_asset_exts (lib/volt/mime.ex:16), so asset?/1 returns false (:49) and the ?raw branch — gated on asset? — never fires.
- Workaround: rename raw-imported
.md files to .txt.
4. Vue SFC <style> is dropped in development (production is fine)
An SFC with a <style> block. Run mix run --no-start repro_vue_dev_style.exs, or fetch it from the dev server.
- Expected: the component is styled in dev.
- Dev (
GET /assets/src/vue_style/Widget.vue): 200, but the served module is the script + render function only — it has no self-import of the style (no ?vue&type=style, no .css). Vize.compile_sfc puts the CSS in result.css, but nothing references it, so the browser never requests it and the component renders unstyled.
- Cause:
Volt.Plugin.Vue.compile/3 returns css: result.css separately (lib/volt/plugin/vue.ex:36) and embedded_modules/3 returns only the scripts (:62-64); nothing appends a style self-import, and Volt.DevServer.code_for_request/4's normal-module clause serves result.code as-is. Vite's Vue plugin injects that self-import for exactly this reason.
- Production is unaffected:
mix volt.build vue_style collects result.css into the entry CSS and records it under the manifest entry's css[]. (Minor: Volt.Preload.tags/2 only emits modulepreload for .js, so a consumer must read manifest[entry]["css"] itself to <link> those styles.)
- Workaround: move SFC styles into a global stylesheet.
Happy to split any of these into their own issues.
Hi I was evaluating volt and wanted to see how it would fair with an existing project, what would break and what would require workarounds.
I had claude take a stab at it and found a few small issues with workarounds. And just to confirm they were issues, created a reproduction repo. I'll include all the issues here but can split them into separate issues if you'd prefer.
Repro repo: https://github.com/andr-ec/volt-repro-bugs (pinned to
{:volt, "== 0.14.12"}, Elixir 1.20)Each bug is an independent Volt profile, reproduced two ways: with
mix volt.build <profile>(production) and against a liveVolt.DevServerover HTTP (dev_server_check.exs). Source-line references are against 0.14.12.1. CSS bundler can't resolve a bare (package)
@import@import "csslib/theme.css";wherecsslibis a package (with anexportsmap for./theme.css). Vite/LightningCSS resolve this intonode_modules.mix volt.build css_import):Build failed: ["CSS bundle error: Error { kind: ResolverError(Os { code: 2, kind: NotFound ... }), loc: ... styles.css, line: 2 ...]GET /assets/src/css_import/styles.css):500compilation error.Volt.Builder.Writer(lib/volt/builder/writer.ex:100) callsVize.CSS.compile(css, minify: ...)with only a minify flag — no resolver, no conditions — andlib/volt/css/dependencies.exonly accepts relative@import. A bare specifier is never resolved againstresolve_dirs.--conditions=style) and<link>the flat output.2. Resolver ignores a subpath directory's
package.json"main"/"module"Importing
dirlib/sub, wheredirlib/sub/is a directory containing only apackage.json("main": "../dist/sub.js","module": "...", noindex.js) and the parent has noexportsmap. Node, esbuild, and Vite all follow that nestedpackage.json.dist/sub.js.mix volt.build dir_pkg): build succeeds silently but emits(function(e){console.log(e.SUB)})(dirlib_sub);— the unresolved specifier is externalized into an undefined globaldirlib_sub, so it throws at runtime and the real export never lands. The silent-externalize-on-unresolved behaviour is the dangerous part.GET /assets/src/dir_pkg/app.js): import rewritten to/@vendor/dirlib__slash__sub.js, which returns404 vendor module not found: dirlib/sub.NPM.Resolution.PackageResolver.resolve_without_exports/5(deps/npm/lib/npm/resolution/package_resolver.ex:313, subpath clause:323) only probes<subpath>.<ext>and<subpath>/index.<ext>; it never reads a subpath directory'spackage.jsonmain/module.react-remove-scroll-bar/constants(a Radix transitive dep). Workaround: an explicitalias.3.
?rawis ignored for.mdimport md from "./note.md?raw". Vite honours?rawfor any extension.mdis the file contents as a string.mix volt.build md_raw):Build failed: [%{message: "Invalid Character ..."}]— the.mdis compiled as JavaScript.GET /assets/src/md_raw/note.md?raw):500, no module served.Volt.MIMElists.mdin@non_asset_exts(lib/volt/mime.ex:16), soasset?/1returns false (:49) and the?rawbranch — gated onasset?— never fires..mdfiles to.txt.4. Vue SFC
<style>is dropped in development (production is fine)An SFC with a
<style>block. Runmix run --no-start repro_vue_dev_style.exs, or fetch it from the dev server.GET /assets/src/vue_style/Widget.vue):200, but the served module is the script + render function only — it has no self-import of the style (no?vue&type=style, no.css).Vize.compile_sfcputs the CSS inresult.css, but nothing references it, so the browser never requests it and the component renders unstyled.Volt.Plugin.Vue.compile/3returnscss: result.cssseparately (lib/volt/plugin/vue.ex:36) andembedded_modules/3returns only the scripts (:62-64); nothing appends a style self-import, andVolt.DevServer.code_for_request/4's normal-module clause servesresult.codeas-is. Vite's Vue plugin injects that self-import for exactly this reason.mix volt.build vue_stylecollectsresult.cssinto the entry CSS and records it under the manifest entry'scss[]. (Minor:Volt.Preload.tags/2only emitsmodulepreloadfor.js, so a consumer must readmanifest[entry]["css"]itself to<link>those styles.)Happy to split any of these into their own issues.