diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f9f1a..f368c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/volt/js/runtime.ex b/lib/volt/js/runtime.ex index c9274b4..3a50936 100644 --- a/lib/volt/js/runtime.ex +++ b/lib/volt/js/runtime.ex @@ -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 diff --git a/lib/volt/js/runtime/package_set.ex b/lib/volt/js/runtime/package_set.ex new file mode 100644 index 0000000..ad690d9 --- /dev/null +++ b/lib/volt/js/runtime/package_set.ex @@ -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 diff --git a/lib/volt/plugin/solid.ex b/lib/volt/plugin/solid.ex index 509d06c..91162f8 100644 --- a/lib/volt/plugin/solid.ex +++ b/lib/volt/plugin/solid.ex @@ -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) @@ -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, @@ -75,6 +79,7 @@ defmodule Volt.Plugin.Solid do ], max_stack_size: 32 * 1024 * 1024 ) + |> Volt.JS.Runtime.ensure!() compile_options = filename diff --git a/lib/volt/plugin/svelte.ex b/lib/volt/plugin/svelte.ex index a5e8cbe..d3d5cb2 100644 --- a/lib/volt/plugin/svelte.ex +++ b/lib/volt/plugin/svelte.ex @@ -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() @@ -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, @@ -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 diff --git a/lib/volt/tailwind.ex b/lib/volt/tailwind.ex index 2339fbc..d162271 100644 --- a/lib/volt/tailwind.ex +++ b/lib/volt/tailwind.ex @@ -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 -> @@ -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 diff --git a/lib/volt/tailwind/loader.ex b/lib/volt/tailwind/loader.ex index 7b211a1..2a8a723 100644 --- a/lib/volt/tailwind/loader.ex +++ b/lib/volt/tailwind/loader.ex @@ -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 %{ diff --git a/priv/npm/solid.lock b/priv/npm/solid.lock new file mode 100644 index 0000000..97ce82f --- /dev/null +++ b/priv/npm/solid.lock @@ -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": [] + } +} diff --git a/priv/npm/svelte.lock b/priv/npm/svelte.lock new file mode 100644 index 0000000..78620ec --- /dev/null +++ b/priv/npm/svelte.lock @@ -0,0 +1,205 @@ +{ + "lockfileVersion": 1, + "packages": { + "@jridgewell/gen-mapping": { + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "has_install_script": false, + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "version": "0.3.13" + }, + "@jridgewell/remapping": { + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "has_install_script": false, + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "version": "2.3.5" + }, + "@jridgewell/resolve-uri": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "version": "3.1.2" + }, + "@jridgewell/sourcemap-codec": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "version": "1.5.5" + }, + "@jridgewell/trace-mapping": { + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + }, + "has_install_script": false, + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "version": "0.3.31" + }, + "@sveltejs/acorn-typescript": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-J1jNYG23QWd67UfrQSFHtjhV37r9mVi0gdc12A3MWPldOjRK35Xk+um+qACPVjgw3AleiqoyEAhok8Wm3q46NA==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.12.tgz", + "version": "1.0.12" + }, + "@types/estree": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "version": "1.0.9" + }, + "@types/trusted-types": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "version": "2.0.7" + }, + "acorn": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "version": "8.18.0" + }, + "aria-query": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "version": "5.3.1" + }, + "axobject-query": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "version": "4.1.0" + }, + "clsx": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "version": "2.1.1" + }, + "devalue": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz", + "version": "5.9.0" + }, + "esm-env": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "version": "1.2.2" + }, + "esrap": { + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "has_install_script": false, + "integrity": "sha512-40GyiEJevYKXzYTHtZkFqAgTjLOuFcaXMao8TPyOlnWTlkHDlvZ6mPMJaJyOqVwrVCgomEG1WhJd81w0X+IcCw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/esrap/-/esrap-2.3.2.tgz", + "version": "2.3.2" + }, + "is-reference": { + "dependencies": { + "@types/estree": "^1.0.6" + }, + "has_install_script": false, + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "version": "3.0.3" + }, + "locate-character": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "version": "3.0.0" + }, + "magic-string": { + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + }, + "has_install_script": false, + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "version": "0.30.21" + }, + "svelte": { + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "has_install_script": false, + "integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz", + "version": "5.56.8" + }, + "zimmerframe": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "version": "1.1.4" + } + }, + "policy": { + "allow_registry_redirects": false, + "allowed_registries": [ + "https://registry.npmjs.org" + ], + "block_exotic_subdeps": true, + "exotic_deps": [] + } +} diff --git a/priv/npm/tailwind.lock b/priv/npm/tailwind.lock new file mode 100644 index 0000000..0f67f46 --- /dev/null +++ b/priv/npm/tailwind.lock @@ -0,0 +1,58 @@ +{ + "lockfileVersion": 1, + "packages": { + "@tailwindcss/typography": { + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "has_install_script": false, + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "version": "0.5.20" + }, + "cssesc": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "version": "3.0.0" + }, + "postcss-selector-parser": { + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "has_install_script": false, + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "version": "6.0.10" + }, + "tailwindcss": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "version": "4.3.3" + }, + "util-deprecate": { + "dependencies": {}, + "has_install_script": false, + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional_dependencies": {}, + "tarball": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "version": "1.0.2" + } + }, + "policy": { + "allow_registry_redirects": false, + "allowed_registries": [ + "https://registry.npmjs.org" + ], + "block_exotic_subdeps": true, + "exotic_deps": [] + } +} diff --git a/scripts/update_runtime_locks.exs b/scripts/update_runtime_locks.exs new file mode 100644 index 0000000..bab34b3 --- /dev/null +++ b/scripts/update_runtime_locks.exs @@ -0,0 +1,62 @@ +Logger.configure(level: :warning) + +alias Volt.JS.Runtime.Installer + +sets = [ + Volt.Plugin.Svelte.runtime_package_set(), + Volt.Plugin.Solid.runtime_package_set(), + Volt.Tailwind.Loader.runtime_package_set() +] + +{opts, args} = OptionParser.parse!(System.argv(), strict: [only: :keep]) + +if args != [] do + raise ArgumentError, "unexpected arguments: #{Enum.join(args, " ")}" +end + +requested = opts |> Keyword.get_values(:only) |> MapSet.new() + +selected = + if MapSet.size(requested) == 0 do + sets + else + Enum.filter(sets, &MapSet.member?(requested, Atom.to_string(&1.name))) + end + +found = selected |> Enum.map(&Atom.to_string(&1.name)) |> MapSet.new() +missing = MapSet.difference(requested, found) + +if MapSet.size(missing) > 0 do + raise ArgumentError, + "unknown runtime package sets: #{missing |> Enum.sort() |> Enum.join(", ")}" +end + +tmp_dir = + Path.join( + System.tmp_dir!(), + "volt-runtime-locks-#{System.unique_integer([:positive])}" + ) + +File.mkdir_p!(tmp_dir) + +try do + Enum.each(selected, fn set -> + install = + Installer.install!(set.packages, + install_dir: Path.join(tmp_dir, Atom.to_string(set.name)), + force: true + ) + + generated = Path.join(install.install_dir, "npm.lock") + target = Path.expand("../priv/npm/#{set.name}.lock", __DIR__) + File.mkdir_p!(Path.dirname(target)) + File.cp!(generated, target) + + digest = + target |> File.read!() |> then(&:crypto.hash(:sha256, &1)) |> Base.encode16(case: :lower) + + Mix.shell().info("Updated #{Path.relative_to_cwd(target)} (sha256: #{digest})") + end) +after + File.rm_rf!(tmp_dir) +end diff --git a/test/volt/js/runtime/package_set_test.exs b/test/volt/js/runtime/package_set_test.exs new file mode 100644 index 0000000..39fe869 --- /dev/null +++ b/test/volt/js/runtime/package_set_test.exs @@ -0,0 +1,60 @@ +defmodule Volt.JS.Runtime.PackageSetTest do + use ExUnit.Case, async: true + + alias Volt.JS.Runtime.PackageSet + + @runtime_modules [Volt.Plugin.Svelte, Volt.Plugin.Solid, Volt.Tailwind.Loader] + + test "built-in package sets ship exact direct versions in valid npm_ex locks" do + Enum.each(@runtime_modules, fn module -> + set = module.runtime_package_set() + lockfile_path = PackageSet.lockfile_path(set) + + assert File.regular?(lockfile_path) + assert NPM.Lockfile.version(lockfile_path) == 1 + assert {:ok, lockfile} = NPM.Lockfile.read(lockfile_path) + assert {:ok, policy} = NPM.Lockfile.read_policy(lockfile_path) + assert NPM.Lockfile.policy_matches?(policy) + + Enum.each(set.packages, fn {package, exact_version} -> + assert %{version: ^exact_version} = Map.fetch!(lockfile, package) + end) + end) + end + + test "runtime opts cannot replace a package set's packages or lockfile" do + set = PackageSet.bundled(:volt, :fixture, %{"fixture" => "1.0.0"}) + + opts = + PackageSet.runtime_opts(set, + packages: %{"other" => "2.0.0"}, + lockfile: "/tmp/other.lock", + bundle: true + ) + + assert opts[:packages] == %{"fixture" => "1.0.0"} + assert opts[:lockfile] == PackageSet.lockfile_path(set) + assert opts[:bundle] + end + + @tag :integration + @tag :tmp_dir + test "built-in package sets install their packaged locks without re-resolving", %{ + tmp_dir: tmp_dir + } do + Enum.each(@runtime_modules, fn module -> + set = module.runtime_package_set() + source_lock = PackageSet.lockfile_path(set) + + install = + PackageSet.install!(set, + install_dir: Path.join(tmp_dir, Atom.to_string(set.name)), + force: true + ) + + installed_lock = Path.join(install.install_dir, "npm.lock") + + assert File.read!(installed_lock) == File.read!(source_lock) + end) + end +end diff --git a/test/volt/js/runtime_test.exs b/test/volt/js/runtime_test.exs index 7bf73d9..deac552 100644 --- a/test/volt/js/runtime_test.exs +++ b/test/volt/js/runtime_test.exs @@ -8,7 +8,12 @@ defmodule Volt.JS.RuntimeTest do tmp_dir = Path.join(System.tmp_dir!(), Atom.to_string(name)) on_exit(fn -> - if pid = GenServer.whereis(name), do: Runtime.stop(pid) + try do + if pid = GenServer.whereis(name), do: Runtime.stop(pid) + catch + :exit, _reason -> :ok + end + File.rm_rf!(tmp_dir) end) @@ -27,4 +32,34 @@ defmodule Volt.JS.RuntimeTest do Runtime.ensure!(name: name, packages: %{}, install_dir: second_dir) end end + + test "named runtimes include package-owned lock contents in their signature", %{ + name: name, + tmp_dir: tmp_dir + } do + install_dir = Path.join(tmp_dir, "runtime") + lockfile = Path.join(tmp_dir, "npm.lock") + File.mkdir_p!(tmp_dir) + :ok = NPM.Lockfile.write(%{}, lockfile) + + runtime = + Runtime.ensure!( + name: name, + packages: %{}, + lockfile: lockfile, + install_dir: install_dir + ) + + assert is_pid(runtime.pid) + File.write!(lockfile, File.read!(lockfile) <> "\n") + + assert_raise ArgumentError, ~r/already started with different options/, fn -> + Runtime.ensure!( + name: name, + packages: %{}, + lockfile: lockfile, + install_dir: install_dir + ) + end + end end