Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixed

- Install built-in Svelte, Solid, and Tailwind runtimes from package-owned npm_ex lockfiles, and include each lock digest in the named runtime signature.

## 0.17.10 - 2026-07-17

### Added
Expand Down
14 changes: 13 additions & 1 deletion lib/volt/js/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,24 @@ defmodule Volt.JS.Runtime do

defp runtime_signature(opts) do
opts
|> Keyword.take([:packages, :install_dir, :entry, :bundle])
|> Keyword.take([:packages, :install_dir, :entry, :bundle, :lockfile])
|> digest_lockfile()
|> Enum.sort()
|> :erlang.term_to_binary()
|> :erlang.md5()
end

defp digest_lockfile(opts) do
case Keyword.fetch(opts, :lockfile) do
{:ok, path} when is_binary(path) ->
digest = path |> File.read!() |> then(&:crypto.hash(:sha256, &1))
Keyword.put(opts, :lockfile, {:sha256, digest})

_ ->
opts
end
end

defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
end
43 changes: 43 additions & 0 deletions lib/volt/js/runtime/package_set.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule Volt.JS.Runtime.PackageSet do
@moduledoc false

alias Volt.JS.Runtime.Installer

@enforce_keys [:app, :name, :packages]
defstruct [:app, :name, :packages]

@type t :: %__MODULE__{
app: atom(),
name: atom(),
packages: %{required(String.t()) => String.t()}
}

@spec bundled(atom(), atom(), map()) :: t()
def bundled(app, name, packages)
when is_atom(app) and is_atom(name) and is_map(packages) do
%__MODULE__{app: app, name: name, packages: packages}
end

@spec install!(t(), keyword()) :: %{install_dir: String.t(), node_modules: String.t()}
def install!(%__MODULE__{} = set, opts \\ []) do
Installer.install!(set.packages, lock_opts(set, opts))
end

@spec runtime_opts(t(), keyword()) :: keyword()
def runtime_opts(%__MODULE__{} = set, opts \\ []) do
set
|> lock_opts(opts)
|> Keyword.put(:packages, set.packages)
end

@spec lockfile_path(t()) :: String.t()
def lockfile_path(%__MODULE__{app: app, name: name}) do
Volt.Priv.path({app, "npm"}, "#{name}.lock")
end

defp lock_opts(set, opts) do
opts
|> Keyword.delete(:packages)
|> Keyword.put(:lockfile, lockfile_path(set))
end
end
13 changes: 9 additions & 4 deletions lib/volt/plugin/solid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ defmodule Volt.Plugin.Solid do

@behaviour Volt.Plugin

alias Volt.JS.Runtime.PackageSet

@runtime_packages %{
"@babel/standalone" => "^7.0.0",
"babel-preset-solid" => "^1.9.0"
"@babel/standalone" => "7.29.8",
"babel-preset-solid" => "1.9.12"
}
@runtime_package_set PackageSet.bundled(:volt, :solid, @runtime_packages)

@runtime_name __MODULE__.Runtime
@jsx_exts ~w(.jsx .tsx)
Expand Down Expand Up @@ -61,12 +64,13 @@ defmodule Volt.Plugin.Solid do
def prebundle_entry(_specifier), do: nil

def runtime_packages, do: @runtime_packages
def runtime_package_set, do: @runtime_package_set

defp do_compile(source, filename, opts, plugin_opts) do
runtime =
Volt.JS.Runtime.ensure!(
PackageSet.runtime_opts(
@runtime_package_set,
name: @runtime_name,
packages: @runtime_packages,
apis: [:browser, :node],
entry: {:volt_asset, "compilers/solid.ts"},
bundle: true,
Expand All @@ -75,6 +79,7 @@ defmodule Volt.Plugin.Solid do
],
max_stack_size: 32 * 1024 * 1024
)
|> Volt.JS.Runtime.ensure!()

compile_options =
filename
Expand Down
13 changes: 9 additions & 4 deletions lib/volt/plugin/svelte.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ defmodule Volt.Plugin.Svelte do

@behaviour Volt.Plugin

@runtime_packages %{"svelte" => "^5.0.0"}
alias Volt.JS.Runtime.PackageSet

@runtime_packages %{"svelte" => "5.56.8"}
@runtime_package_set PackageSet.bundled(:volt, :svelte, @runtime_packages)
@runtime_name __MODULE__.Runtime
@resolve_extensions Volt.JS.Extensions.node_resolvable()

Expand Down Expand Up @@ -37,7 +40,7 @@ defmodule Volt.Plugin.Svelte do
def resolve(_, _), do: nil

defp resolve_svelte(specifier, _importer) do
install = Volt.JS.Runtime.Installer.install!(@runtime_packages)
install = PackageSet.install!(@runtime_package_set)

case NPM.Resolution.PackageResolver.resolve(specifier, install.install_dir,
extensions: @resolve_extensions,
Expand Down Expand Up @@ -77,17 +80,19 @@ defmodule Volt.Plugin.Svelte do
end

def runtime_packages, do: @runtime_packages
def runtime_package_set, do: @runtime_package_set

defp do_compile(path, source, opts, plugin_opts) do
runtime =
Volt.JS.Runtime.ensure!(
PackageSet.runtime_opts(
@runtime_package_set,
name: @runtime_name,
packages: @runtime_packages,
apis: [:browser, :node],
entry: {:volt_asset, "compilers/svelte.ts"},
bundle: true,
max_stack_size: 16 * 1024 * 1024
)
|> Volt.JS.Runtime.ensure!()

compile_options =
path
Expand Down
5 changes: 3 additions & 2 deletions lib/volt/tailwind.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ defmodule Volt.Tailwind do

defp ensure_runtime(%{runtime: nil} = state) do
runtime =
Volt.JS.Runtime.ensure!(
packages: Volt.Tailwind.Loader.runtime_packages(),
Volt.JS.Runtime.PackageSet.runtime_opts(
Volt.Tailwind.Loader.runtime_package_set(),
apis: [:browser, :node],
handlers: fn runtime -> Volt.Tailwind.Loader.handlers(runtime.node_modules) end,
define: fn runtime ->
Expand All @@ -88,6 +88,7 @@ defmodule Volt.Tailwind do
end,
entry: {:volt_asset, "compilers/tailwind.ts"}
)
|> Volt.JS.Runtime.ensure!()

%{state | runtime: runtime, scanner: build_scanner(state.sources)}
end
Expand Down
8 changes: 5 additions & 3 deletions lib/volt/tailwind/loader.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
defmodule Volt.Tailwind.Loader do
@moduledoc "Handles Tailwind module loading, prebundling CJS graphs via OXC, and stylesheet resolution."

alias Volt.JS.Runtime.PackageSet
alias Volt.JS.Transforms.Specifiers
alias Volt.Tailwind.Resolver

@tailwind_install_spec "^4.3.0"
@tailwind_runtime_deps %{
"tailwindcss" => @tailwind_install_spec,
"@tailwindcss/typography" => "*"
"tailwindcss" => "4.3.3",
"@tailwindcss/typography" => "0.5.20"
}
@runtime_package_set PackageSet.bundled(:volt, :tailwind, @tailwind_runtime_deps)

def runtime_packages, do: @tailwind_runtime_deps
def runtime_package_set, do: @runtime_package_set

def handlers(runtime_node_modules) do
%{
Expand Down
126 changes: 126 additions & 0 deletions priv/npm/solid.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"lockfileVersion": 1,
"packages": {
"@babel/helper-module-imports": {
"dependencies": {
"@babel/types": "^7.18.6"
},
"has_install_script": false,
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"version": "7.18.6"
},
"@babel/helper-plugin-utils": {
"dependencies": {},
"has_install_script": false,
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
"version": "7.29.7"
},
"@babel/helper-string-parser": {
"dependencies": {},
"has_install_script": false,
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"version": "7.29.7"
},
"@babel/helper-validator-identifier": {
"dependencies": {},
"has_install_script": false,
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"version": "7.29.7"
},
"@babel/plugin-syntax-jsx": {
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
"has_install_script": false,
"integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
"version": "7.29.7"
},
"@babel/standalone": {
"dependencies": {},
"has_install_script": false,
"integrity": "sha512-XgbPNz+u6JzB7cKGnPDoS1U24J5td8yp3HFWbT/6f4/ASZ0PqaQGIvts1o5v5AI+f6i3jdVB/L/o2CYLCv101A==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.29.8.tgz",
"version": "7.29.8"
},
"@babel/types": {
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"has_install_script": false,
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
"version": "7.29.8"
},
"babel-plugin-jsx-dom-expressions": {
"dependencies": {
"@babel/helper-module-imports": "7.18.6",
"@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/types": "^7.20.7",
"html-entities": "2.3.3",
"parse5": "^7.1.2"
},
"has_install_script": false,
"integrity": "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.7.tgz",
"version": "0.40.7"
},
"babel-preset-solid": {
"dependencies": {
"babel-plugin-jsx-dom-expressions": "^0.40.6"
},
"has_install_script": false,
"integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz",
"version": "1.9.12"
},
"entities": {
"dependencies": {},
"has_install_script": false,
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"version": "6.0.1"
},
"html-entities": {
"dependencies": {},
"has_install_script": false,
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
"version": "2.3.3"
},
"parse5": {
"dependencies": {
"entities": "^6.0.0"
},
"has_install_script": false,
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"optional_dependencies": {},
"tarball": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"version": "7.3.0"
}
},
"policy": {
"allow_registry_redirects": false,
"allowed_registries": [
"https://registry.npmjs.org"
],
"block_exotic_subdeps": true,
"exotic_deps": []
}
}
Loading