From c136d829c4c996637e6b517c01d1487ac735b596 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 5 Oct 2026 16:33:35 +0400 Subject: [PATCH 1/5] Leave non-JavaScript Vue script blocks out of script modules Volt.Plugin.Vue returned every + + + + + """ + + assert script_modules(source) == [{".ts", "const n: number = 1"}] + end + + test "names script blocks by their JavaScript dialect" do + for {lang, extension} <- [ + {"", ".js"}, + {~s( lang="js"), ".js"}, + {~s( lang="jsx"), ".jsx"}, + {~s( lang="ts"), ".ts"}, + {~s( lang="tsx"), ".tsx"} + ] do + source = "export default {}" + + assert script_modules(source) == [{extension, "export default {}"}] + end + end + end + test "script-bearing scoped components receive their CSS scope on the default export" do for script <- [ "", From 28cbed24324734f33a913923eb3c88c7fd1d0156 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 5 Oct 2026 16:33:35 +0400 Subject: [PATCH 2/5] Serve OXC runtime helpers from the dev server With a :target below ES2022, OXC lowers a class field into an import of a helper from @oxc-project/runtime. The dev server rewrote that import to /@vendor/, which resolved the package from node_modules and answered 404 when it was not installed. The importing module then failed to load, and every module importing it, with nothing in the page or the log. The production build already leaves these imports to OXC.bundle/2, which provides the helpers itself. Vendor bundling now does the same through an entry that only names the helper. The check for such specifiers moves to Volt.JS.Specifier, shared by both. Fixes #58 --- CHANGELOG.md | 1 + lib/volt/builder/resolver.ex | 7 +---- lib/volt/js/specifier.ex | 13 ++++++++ lib/volt/js/vendor.ex | 13 +++++++- .../volt/dev_server/import_rewriting_test.exs | 30 +++++++++++++++++++ 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1700635..0781eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Leave Vue ` + + """) + + Application.put_env(:volt, :sources, ["lib/**/*.vue"]) + Application.put_env(:volt, :lint, tsgolint: @tsgolint, rules: %{}) + + output = + capture_io(:stderr, fn -> + catch_exit(Mix.Tasks.Volt.Js.Check.run(["--type-aware", "--type-check"])) + end) + + # The script is type-checked, and with the project's options: the path + # alias and the `*.vue` declaration resolve. + assert output =~ "TS2322" + refute output =~ "TS2307" + refute output =~ "Cannot find module" + end + test "type-aware overrides batch by effective rules and use original SFC paths" do files = Enum.map(["app.ts", "Component.vue", "Widget.svelte"], &Path.join(@tmp_dir, &1)) [app, vue, svelte] = files diff --git a/test/volt/js/lint/tsconfig_overlay_test.exs b/test/volt/js/lint/tsconfig_overlay_test.exs new file mode 100644 index 0000000..bfeb21e --- /dev/null +++ b/test/volt/js/lint/tsconfig_overlay_test.exs @@ -0,0 +1,72 @@ +defmodule Volt.JS.Lint.TSConfigOverlayTest do + use ExUnit.Case, async: true + + alias Volt.JS.Lint.TSConfigOverlay + + @moduletag :tmp_dir + + defp write!(root, path, content) do + path = Path.join(root, path) + File.mkdir_p!(Path.dirname(path)) + File.write!(path, content) + path + end + + test "adds virtual modules to the project of the nearest tsconfig", %{tmp_dir: root} do + original = """ + { + // comments and trailing commas are allowed in a tsconfig + "compilerOptions": { "paths": { "@/*": ["./assets/js/*"] }, }, + "include": ["assets/js/**/*.ts"], + } + """ + + tsconfig = write!(root, "tsconfig.json", original) + first = Path.join(root, "lib/app_web/Tally.vue.script0.ts") + second = Path.join(root, "assets/js/Card.vue.script0.ts") + + overrides = TSConfigOverlay.overrides([first, second]) + + # The original is carried unchanged beside the tsconfig that replaces it. + assert overrides[Path.join(root, "tsconfig.volt-base.json")] == original + + assert Jason.decode!(overrides[tsconfig]) == %{ + "extends" => "./tsconfig.volt-base.json", + "files" => [second, first] + } + end + + test "keeps the files the original names, which `files` would replace", %{tmp_dir: root} do + tsconfig = write!(root, "tsconfig.json", ~s({ files: ["src/main.ts", "./env.d.ts"] })) + virtual = Path.join(root, "src/App.vue.script0.ts") + + assert %{"files" => ["src/main.ts", "./env.d.ts", ^virtual]} = + TSConfigOverlay.overrides([virtual])[tsconfig] |> Jason.decode!() + end + + test "spells out the default include a tsconfig without one relies on", %{tmp_dir: root} do + tsconfig = write!(root, "tsconfig.json", ~s({ "compilerOptions": { "strict": true } })) + virtual = Path.join(root, "App.vue.script0.ts") + + assert %{"include" => ["**/*"], "files" => [^virtual]} = + TSConfigOverlay.overrides([virtual])[tsconfig] |> Jason.decode!() + end + + test "uses the nearest tsconfig for each virtual module", %{tmp_dir: root} do + outer = write!(root, "tsconfig.json", ~s({ "include": ["**/*"] })) + inner = write!(root, "assets/tsconfig.json", ~s({ "include": ["**/*"] })) + in_lib = Path.join(root, "lib/Page.vue.script0.ts") + in_assets = Path.join(root, "assets/js/App.vue.script0.ts") + + overrides = TSConfigOverlay.overrides([in_lib, in_assets]) + + assert %{"files" => [^in_lib]} = Jason.decode!(overrides[outer]) + assert %{"files" => [^in_assets]} = Jason.decode!(overrides[inner]) + end + + test "leaves virtual modules alone without a readable tsconfig above them", %{tmp_dir: root} do + write!(root, "broken/tsconfig.json", "not a config") + + assert TSConfigOverlay.overrides([Path.join(root, "broken/App.vue.script0.ts")]) == %{} + end +end