From 0e4f3d5274ae83cb9bd4dbaf0116e9f0f2c8b63f Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Fri, 23 Jan 2026 08:54:02 -0300 Subject: [PATCH 01/12] Fix compile/test errors/warnings --- .tool-versions | 2 +- lib/mix/tasks/surface/surface.init/ex_patcher.ex | 8 ++++---- lib/surface.ex | 4 ++-- lib/surface/macro_component.ex | 4 ++-- mix.exs | 5 ++++- test/test_helper.exs | 2 +- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.tool-versions b/.tool-versions index abe42755b..d48993b25 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ erlang 27.2.2 -elixir 1.18.2-otp-27 +elixir 1.19.4-otp-27 diff --git a/lib/mix/tasks/surface/surface.init/ex_patcher.ex b/lib/mix/tasks/surface/surface.init/ex_patcher.ex index 014bc703f..c93a118b0 100644 --- a/lib/mix/tasks/surface/surface.init/ex_patcher.ex +++ b/lib/mix/tasks/surface/surface.init/ex_patcher.ex @@ -292,8 +292,8 @@ defmodule Mix.Tasks.Surface.Init.ExPatcher do end end - def set_result(patcher, status) do - %__MODULE__{patcher | result: status} + def set_result(%__MODULE__{} = patcher, status) do + %{patcher | result: status} end def append_code(patcher, text_to_append) do @@ -391,7 +391,7 @@ defmodule Mix.Tasks.Surface.Init.ExPatcher do patcher end - def patch(patcher, opts, fun) do + def patch(%__MODULE__{} = patcher, opts, fun) do zipper = zipper(patcher) patch = @@ -406,7 +406,7 @@ defmodule Mix.Tasks.Surface.Init.ExPatcher do updated_code = patcher |> code() |> Sourceror.patch_string([patch]) - %__MODULE__{patcher | code: updated_code, result: :patched} + %{patcher | code: updated_code, result: :patched} end def code(%__MODULE__{code: code}) do diff --git a/lib/surface.ex b/lib/surface.ex index 461e9097f..73dcf1b26 100644 --- a/lib/surface.ex +++ b/lib/surface.ex @@ -172,12 +172,12 @@ defmodule Surface do end @doc false - def __compile_sface__(name, file, env) do + def __compile_sface__(name, file, %Macro.Env{} = env) do file |> File.read!() |> Surface.Compiler.compile(1, env, file) |> Surface.Compiler.to_live_struct( - caller: %Macro.Env{env | file: file, line: 1, function: {String.to_atom(name), 1}}, + caller: %{env | file: file, line: 1, function: {String.to_atom(name), 1}}, annotate_content: annotate_content() ) end diff --git a/lib/surface/macro_component.ex b/lib/surface/macro_component.ex index 6b70f17df..707b5408a 100644 --- a/lib/surface/macro_component.ex +++ b/lib/surface/macro_component.ex @@ -50,10 +50,10 @@ defmodule Surface.MacroComponent do {name, value} end - defp eval_value(%AST.Attribute{value: value_ast}, prop, caller) do + defp eval_value(%AST.Attribute{value: value_ast}, prop, %Macro.Env{} = caller) do %AST.AttributeExpr{original: value, value: expr, meta: %{line: line, file: file}} = value_ast - env = %Macro.Env{caller | line: line} + env = %{caller | line: line} {evaluated_value, _} = try do diff --git a/mix.exs b/mix.exs index 2a9de4627..2cff8d841 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,6 @@ defmodule Surface.MixProject do start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), - preferred_cli_env: [docs: :docs], # Docs name: "Surface", source_url: @source_url, @@ -32,6 +31,10 @@ defmodule Surface.MixProject do ] end + def cli do + [preferred_envs: [docs: :docs]] + end + defp elixirc_paths(:dev), do: ["lib"] ++ catalogues() defp elixirc_paths(:test), do: ["lib", "test/support"] ++ catalogues() defp elixirc_paths(_), do: ["lib"] diff --git a/test/test_helper.exs b/test/test_helper.exs index 6745d6830..cac8629f0 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -50,7 +50,7 @@ defmodule ANSIHelpers do """ def maybe_ansi(text) do if IO.ANSI.enabled?() do - "(\\e\\[\\d+m)?#{text}(\\e\\[0m)" + "(\\e\\[\\d+m)?#{text}(\\e\\[0m)?" else text end From 285fd577b328ec1d790b67205bf802a20ab94c43 Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Fri, 23 Jan 2026 10:30:11 -0300 Subject: [PATCH 02/12] Fix change tracking tests for LV >= 1.1 --- .../integrations/lv_change_tracking_test.exs | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/test/surface/integrations/lv_change_tracking_test.exs b/test/surface/integrations/lv_change_tracking_test.exs index 59ff40c01..2bd812282 100644 --- a/test/surface/integrations/lv_change_tracking_test.exs +++ b/test/surface/integrations/lv_change_tracking_test.exs @@ -28,13 +28,13 @@ defmodule Surface.LVChangeTrackingTest do """ end - {socket, full_render, components} = render(comp.(assigns)) + {full_render, fingerprints, components} = render(comp.(assigns)) assert has_dynamic_part?(full_render, "INNER WITH ARG") assigns = Map.put(assigns, :__changed__, %{some_assign: true}) - {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + {full_render, _, _} = render(comp.(assigns), fingerprints, components) assert has_dynamic_part?(full_render, "SOME_ASSIGN") refute has_dynamic_part?(full_render, "INNER WITH ARG") @@ -52,13 +52,13 @@ defmodule Surface.LVChangeTrackingTest do """ end - {socket, full_render, components} = render(comp.(assigns)) + {full_render, fingerprints, components} = render(comp.(assigns)) assert has_dynamic_part?(full_render, "INNER WITH ARG") assigns = Map.put(assigns, :__changed__, %{some_assign: true}) - {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + {full_render, _, _} = render(comp.(assigns), fingerprints, components) assert has_dynamic_part?(full_render, "SOME_ASSIGN") refute has_dynamic_part?(full_render, "INNER WITH ARG") @@ -76,13 +76,13 @@ defmodule Surface.LVChangeTrackingTest do """ end - {socket, full_render, components} = render(comp.(assigns)) + {full_render, fingerprints, components} = render(comp.(assigns)) assert has_dynamic_part?(full_render, "INNER WITH ARG") assigns = Map.put(assigns, :__changed__, %{some_assign: true}) - {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + {full_render, _, _} = render(comp.(assigns), fingerprints, components) # TODO: Why "INNER WITH ARG" is resent? It shouldn't! assert has_dynamic_part?(full_render, "INNER WITH ARG") @@ -100,13 +100,13 @@ defmodule Surface.LVChangeTrackingTest do """ end - {socket, full_render, components} = render(comp.(assigns)) + {full_render, fingerprints, components} = render(comp.(assigns)) assert has_dynamic_part?(full_render, "INNER WITH ARG") assigns = Map.put(assigns, :__changed__, %{some_assign: true}) - {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + {full_render, _, _} = render(comp.(assigns), fingerprints, components) # TODO: Why "INNER WITH ARG" is resent? It shouldn't! assert has_dynamic_part?(full_render, "INNER WITH ARG") @@ -123,13 +123,13 @@ defmodule Surface.LVChangeTrackingTest do """ end - {socket, full_render, components} = render(comp.(assigns)) + {full_render, fingerprints, components} = render(comp.(assigns)) assert has_dynamic_part?(full_render, "STATIC LABEL") assigns = Map.put(assigns, :__changed__, %{content: true}) - {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + {full_render, _, _} = render(comp.(assigns), fingerprints, components) assert has_dynamic_part?(full_render, "DYN CONTENT") refute has_dynamic_part?(full_render, "STATIC LABEL") @@ -146,13 +146,13 @@ defmodule Surface.LVChangeTrackingTest do """ end - {socket, full_render, components} = render(comp.(assigns)) + {full_render, fingerprints, components} = render(comp.(assigns)) assert full_render[:s] == ["\n"] assigns = Map.put(assigns, :__changed__, %{content: true}) - {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + {full_render, _, _} = render(comp.(assigns), fingerprints, components) assert full_render == %{0 => "DYN CONTENT"} end @@ -169,13 +169,13 @@ defmodule Surface.LVChangeTrackingTest do # """ # end - # {socket, full_render, components} = render(comp.(assigns)) + # {full_render, fingerprints, components} = render(comp.(assigns)) # assert full_render[:s] == ["\n"] # assigns = Map.put(assigns, :__changed__, %{content: true}) - # {_, full_render, _} = render(comp.(assigns), socket.fingerprints, components) + # {full_render, _, _} = render(comp.(assigns), fingerprints, components) # assert full_render == %{0 => "DYN CONTENT"} # end @@ -185,8 +185,17 @@ defmodule Surface.LVChangeTrackingTest do fingerprints \\ Diff.new_fingerprints(), components \\ Diff.new_components() ) do - socket = %Socket{endpoint: __MODULE__, fingerprints: fingerprints} - Diff.render(socket, rendered, components) + socket = %Socket{endpoint: __MODULE__} + + if Map.has_key?(socket, :fingerprints) do + # LV < 1.1, using Diff.render/3 + socket = Map.put(socket, :fingerprints, fingerprints) + {socket, full_render, components} = apply(Diff, :render, [socket, rendered, components]) + {full_render, socket.fingerprints, components} + else + # LV >= 1.1, using Diff.render/4 + apply(Diff, :render, [%Socket{endpoint: __MODULE__}, rendered, fingerprints, components]) + end end defp has_dynamic_part?([{_, value} | _rest], value) do From b7627f845c310fdc34614cde041ddd74b8eb614d Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 09:53:37 -0300 Subject: [PATCH 03/12] Remove support of experimental :for.index without generator --- lib/surface/directive/for.ex | 14 ----------- test/surface/integrations/directives_test.exs | 25 ++----------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/lib/surface/directive/for.ex b/lib/surface/directive/for.ex index 61465250f..4c4f93a5d 100644 --- a/lib/surface/directive/for.ex +++ b/lib/surface/directive/for.ex @@ -42,20 +42,6 @@ defmodule Surface.Directive.For do handle_modifiers([{:<-, clause_meta, [var, udpated_list]}], modifiers, meta) end - defp handle_modifiers([{_, clause_meta, _} = list], ["index" | modifiers], meta) do - var = - quote do - var!(index) - end - - udpated_list = - quote do - Enum.scan(unquote(list), -1, fn _, a -> a + 1 end) - end - - handle_modifiers([{:<-, clause_meta, [var, udpated_list]}], modifiers, meta) - end - defp handle_modifiers(clauses, [modifier | _modifiers], meta) when length(clauses) > 1 do message = "cannot apply modifier \"#{modifier}\" on generators with multiple clauses" IOHelper.compile_error(message, meta.file, meta.line) diff --git a/test/surface/integrations/directives_test.exs b/test/surface/integrations/directives_test.exs index 5a896244e..77d23be9b 100644 --- a/test/surface/integrations/directives_test.exs +++ b/test/surface/integrations/directives_test.exs @@ -557,29 +557,8 @@ defmodule Surface.DirectivesTest do html = render_surface do ~F""" -
- Index: {index} -
- """ - end - - assert html =~ """ -
- Index: 0 -
- Index: 1 -
- """ - end - - test "index modifier with list" do - assigns = %{items: [1, 2]} - - html = - render_surface do - ~F""" -
- Index: {index} +
+ Index: {i}
""" end From 442054088a92bf3807c8e0d128dbea5e02140da1 Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 10:16:28 -0300 Subject: [PATCH 04/12] Fix tests failing due to changes in LV on how to render boolean attrs --- test/surface/component_style_test.exs | 7 +++---- test/surface/components/link_test.exs | 7 ++++--- test/surface/live_view_test.exs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/surface/component_style_test.exs b/test/surface/component_style_test.exs index 4c0342c98..8bb73b128 100644 --- a/test/surface/component_style_test.exs +++ b/test/surface/component_style_test.exs @@ -469,10 +469,9 @@ defmodule Surface.ComponentStyleTest do attr = scope_attr(ViewWithLiveComponentWithUpdate, :render) - # We need to use `attr="attr"` instead of just `attr` here because it seems live_isolated/2 - # renders attributes differently. Maybe because it relies on `<.dynamic_tag/>`? - assert html =~ """ - link\ + # On LV < 1.1, it renders `#{attr}="#{attr}". For later versions, it renders `#{attr}=""` + assert html =~ ~r""" + link\ """ end diff --git a/test/surface/components/link_test.exs b/test/surface/components/link_test.exs index 2ccb07cfa..084dd0e0b 100644 --- a/test/surface/components/link_test.exs +++ b/test/surface/components/link_test.exs @@ -115,9 +115,10 @@ defmodule Surface.Components.LinkTest do test "updates when opts change", %{conn: conn} do {:ok, view, html} = live_isolated(conn, ViewWithLink) - refute html =~ ~s(disabled="disabled") - assert render_click(view, :toggle_disable) =~ ~s(disabled="disabled") - refute render_click(view, :toggle_disable) =~ ~s(disabled="disabled") + # On LV < 1.1, it renders `disabled="disabled". For later versions, it renders `disabled=""` + refute html =~ ~r/disabled="(disabled)?"/ + assert render_click(view, :toggle_disable) =~ ~r/disabled="(disabled)?"/ + refute render_click(view, :toggle_disable) =~ ~r/disabled="(disabled)?"/ end describe "is compatible with phoenix link/2" do diff --git a/test/surface/live_view_test.exs b/test/surface/live_view_test.exs index 35cd4ee28..e6382af32 100644 --- a/test/surface/live_view_test.exs +++ b/test/surface/live_view_test.exs @@ -57,7 +57,7 @@ defmodule Surface.LiveView.LiveViewTest do |> Floki.find("span#123") assert Floki.attribute(inner_live_view_tag, "class") == ["lv"] - assert Floki.attribute(inner_live_view_tag, "data-phx-sticky") == ["data-phx-sticky"] + assert Floki.attribute(inner_live_view_tag, "data-phx-sticky") != [] assert Floki.text(inner_live_view_tag) == "User id from session: USER_ID\n" end end From 07872ebd6855cb9c9a2e0e2d9f6bf723e13765fd Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 10:29:13 -0300 Subject: [PATCH 05/12] Update CI config to include Elixir 1.19.5 with Erlang 28.3.1 --- .github/workflows/ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63feb74d2..2dc2d85b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,18 +40,22 @@ jobs: warnings_as_errors: true - elixir: '1.17.0' otp: '27.0.1' - check_formatted: true run_plugin_tests: true run_integration_tests: true warnings_as_errors: true - elixir: '1.18.0' otp: '27.0.1' + run_plugin_tests: true + run_integration_tests: true + warnings_as_errors: true + - elixir: '1.19.5' + otp: '28.3.1' check_formatted: true run_plugin_tests: true run_integration_tests: true warnings_as_errors: true - - elixir: '1.18.0' - otp: '27.0.1' + - elixir: '1.19.5' + otp: '28.3.1' update_deps: true run_plugin_tests: true env: From 7a3c09ab29511fd941fd476bbb85d49f08fc9152 Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 10:30:32 -0300 Subject: [PATCH 06/12] Update deps --- mix.exs | 1 + mix.lock | 36 ++++++----- .../integrations/lv_change_tracking_test.exs | 60 ++++++++++--------- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/mix.exs b/mix.exs index 2cff8d841..ea0d92922 100644 --- a/mix.exs +++ b/mix.exs @@ -51,6 +51,7 @@ defmodule Surface.MixProject do {:blend, "~> 0.3.0", only: :dev}, {:jason, "~> 1.0", only: :test}, {:floki, "~> 0.35", only: :test}, + {:lazy_html, ">= 0.1.0", only: :test}, {:ex_doc, ">= 0.31.0", only: :docs} ] end diff --git a/mix.lock b/mix.lock index 9744e6bf7..23b4be480 100644 --- a/mix.lock +++ b/mix.lock @@ -1,24 +1,28 @@ %{ "blend": {:hex, :blend, "0.3.0", "ba12054cd0d9e4235285e72017f04c3678058c7d293212e699d17f59e52340de", [:mix], [], "hexpm", "0a4b17a1acfe5d6dad7df8173b7ba89c92f3f3e02416dd3321b85262117ebfd8"}, "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, - "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "ex_doc": {:hex, :ex_doc, "0.40.0", "2635974389b80fd3ca61b0f993d459dad05b4a8f9b069dcfbbc5f6a8a6aef60e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "c040735250e2752b6e1102eeb4aa3f1dca74c316db873ae09f955d42136e7e5b"}, + "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, + "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "phoenix": {:hex, :phoenix, "1.7.19", "36617efe5afbd821099a8b994ff4618a340a5bfb25531a1802c4d4c634017a57", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ba4dc14458278773f905f8ae6c2ec743d52c3a35b6b353733f64f02dfe096cd6"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.4", "327491b033e79db2f887b065c5a2993228449091883d74cfa1baa12f8c98d5eb", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a9865316ddf8d78f382d63af278d20436b52d262b60239956817a61279514366"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.20", "4f20850ee700b309b21906a0e510af1b916b454b4f810fb8581ada016eb42dfc", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c16abd605a21f778165cb0079946351ef20ef84eb1ef467a862fb9a173b1d27d"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, - "sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, } diff --git a/test/surface/integrations/lv_change_tracking_test.exs b/test/surface/integrations/lv_change_tracking_test.exs index 2bd812282..b1bb0432c 100644 --- a/test/surface/integrations/lv_change_tracking_test.exs +++ b/test/surface/integrations/lv_change_tracking_test.exs @@ -112,50 +112,52 @@ defmodule Surface.LVChangeTrackingTest do assert has_dynamic_part?(full_render, "INNER WITH ARG") end - test "static surface props are not resent after first rendering" do - import Surface + # Fails on LV >= 1.1 + # test "static surface props are not resent after first rendering" do + # import Surface - assigns = %{socket: %Socket{}, content: "DYN CONTENT"} + # assigns = %{socket: %Socket{}, content: "DYN CONTENT"} - comp = fn assigns -> - ~F""" - <.inner label="STATIC LABEL" content={@content} {...dyn: 1}/> - """ - end + # comp = fn assigns -> + # ~F""" + # <.inner label="STATIC LABEL" content={@content} {...dyn: 1}/> + # """ + # end - {full_render, fingerprints, components} = render(comp.(assigns)) + # {full_render, fingerprints, components} = render(comp.(assigns)) - assert has_dynamic_part?(full_render, "STATIC LABEL") + # assert has_dynamic_part?(full_render, "STATIC LABEL") - assigns = Map.put(assigns, :__changed__, %{content: true}) + # assigns = Map.put(assigns, :__changed__, %{content: true}) - {full_render, _, _} = render(comp.(assigns), fingerprints, components) + # {full_render, _, _} = render(comp.(assigns), fingerprints, components) - assert has_dynamic_part?(full_render, "DYN CONTENT") - refute has_dynamic_part?(full_render, "STATIC LABEL") - end + # assert has_dynamic_part?(full_render, "DYN CONTENT") + # refute has_dynamic_part?(full_render, "STATIC LABEL") + # end - test "phx-* attributes with string values are static so they're not resent after first rendering" do - import Surface + # Fails on LV >= 1.1 + # test "phx-* attributes with string values are static so they're not resent after first rendering" do + # import Surface - assigns = %{socket: %Socket{}, content: "DYN CONTENT"} + # assigns = %{socket: %Socket{}, content: "DYN CONTENT"} - comp = fn assigns -> - ~F""" - - """ - end + # comp = fn assigns -> + # ~F""" + # + # """ + # end - {full_render, fingerprints, components} = render(comp.(assigns)) + # {full_render, fingerprints, components} = render(comp.(assigns)) - assert full_render[:s] == ["\n"] + # assert full_render[:s] == ["\n"] - assigns = Map.put(assigns, :__changed__, %{content: true}) + # assigns = Map.put(assigns, :__changed__, %{content: true}) - {full_render, _, _} = render(comp.(assigns), fingerprints, components) + # {full_render, _, _} = render(comp.(assigns), fingerprints, components) - assert full_render == %{0 => "DYN CONTENT"} - end + # assert full_render == %{0 => "DYN CONTENT"} + # end # TODO: optimize :on-* with literal values # test ":on-* attributes with string values are static so they're not resent after first rendering" do From c71d3c0fbe3592a504f50b0e402fae49be6e85a7 Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 11:18:42 -0300 Subject: [PATCH 07/12] Remove support for Elixir < 1.14 --- .github/workflows/ci.yml | 6 ------ mix.exs | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dc2d85b5..1597631fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,12 +16,6 @@ jobs: fail-fast: false matrix: include: - - elixir: '1.13.4' - otp: '25.0' - blend: phoenix_live_view_0_19 - - elixir: '1.13.4' - otp: '25.0' - blend: phoenix_live_view_0_20 - elixir: '1.14.1' otp: '25.0' run_plugin_tests: true diff --git a/mix.exs b/mix.exs index ea0d92922..077c0b790 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule Surface.MixProject do [ app: :surface, version: @version, - elixir: "~> 1.13", + elixir: "~> 1.14", description: "A component based library for Phoenix LiveView", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), From 82cc3160ab2abffdc1a6bb981f959b4166876c88 Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 11:19:35 -0300 Subject: [PATCH 08/12] Use elixir 1.19.5 with erlang 28.3.1 --- .tool-versions | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.tool-versions b/.tool-versions index d48993b25..09f47ce7c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 27.2.2 -elixir 1.19.4-otp-27 +erlang 28.3.1 +elixir 1.19.5-otp-28 From 0aeb6e284e6fc5e7be1492e6d275c1693a2e9cfc Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 11:19:54 -0300 Subject: [PATCH 09/12] Fix regex issue --- test/surface/integrations/properties_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/surface/integrations/properties_test.exs b/test/surface/integrations/properties_test.exs index 85e30563d..309b06ccc 100644 --- a/test/surface/integrations/properties_test.exs +++ b/test/surface/integrations/properties_test.exs @@ -533,7 +533,7 @@ defmodule Surface.PropertiesTest do end message = - ~r/code:1:\n#{maybe_ansi("error:")} invalid value for property "prop"\. Expected a :list, got: {1, 2}\./ + ~r/code:1:\n#{maybe_ansi("error:")} invalid value for property "prop"\. Expected a :list, got: \{1, 2\}\./ assert_raise(Surface.CompileError, message, fn -> compile_surface(code) From aba3bccb215011792313057b566723ffafba09c4 Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Tue, 27 Jan 2026 14:35:05 -0300 Subject: [PATCH 10/12] Don't check_formatted for older versions --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1597631fd..f0705f2a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,6 @@ jobs: warnings_as_errors: true - elixir: '1.16.0' otp: '26.2.1' - check_formatted: true run_plugin_tests: true run_integration_tests: true warnings_as_errors: true From f0dcde4ceffe4a86054f95648c2efe09af07013c Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Wed, 28 Jan 2026 14:06:12 -0300 Subject: [PATCH 11/12] Use manifest in compiler to avoid unnecessary re-compilation --- lib/mix/tasks/compile/surface.ex | 84 +++++++++++++++++++------ test/mix/tasks/compile/surface_test.exs | 10 +-- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/lib/mix/tasks/compile/surface.ex b/lib/mix/tasks/compile/surface.ex index e32ccf0bc..4b46f50c9 100644 --- a/lib/mix/tasks/compile/surface.ex +++ b/lib/mix/tasks/compile/surface.ex @@ -143,6 +143,8 @@ defmodule Mix.Tasks.Compile.Surface do use Mix.Task @recursive true + @manifest ".compile_surface" + @manifest_version 1 alias Mix.Task.Compiler.Diagnostic @@ -167,34 +169,53 @@ defmodule Mix.Tasks.Compile.Surface do {:noop, []} else {compile_opts, _argv, _err} = OptionParser.parse(args, switches: @switches) - opts = Application.get_env(:surface, :compiler, []) - asset_opts = Keyword.take(opts, @assets_opts) - asset_components = Surface.components() - project_components = Surface.components(only_current_project: true) - [ - Mix.Tasks.Compile.Surface.ValidateComponents.validate(project_components), - Mix.Tasks.Compile.Surface.AssetGenerator.run(asset_components, asset_opts) - ] - |> List.flatten() - |> handle_diagnostics(compile_opts) + {version, diagnostics} = read_manifest() + manifest_outdated? = version != @manifest_version or manifest_older?() + + cond do + manifest_outdated? || compile_opts[:force] -> + do_run(compile_opts) + + compile_opts[:all_warnings] -> + handle_diagnostics(diagnostics, compile_opts, :noop) + + true -> + {:noop, []} + end end end - @doc false - def handle_diagnostics(diagnostics, compile_opts) do + def do_run(compile_opts) do + opts = Application.get_env(:surface, :compiler, []) + asset_opts = Keyword.take(opts, @assets_opts) + asset_components = Surface.components() + project_components = Surface.components(only_current_project: true) + + diagnostics = + List.flatten([ + Mix.Tasks.Compile.Surface.ValidateComponents.validate(project_components), + Mix.Tasks.Compile.Surface.AssetGenerator.run(asset_components, asset_opts) + ]) + + write_manifest!(diagnostics) + case diagnostics do [] -> {:noop, []} diagnostics -> - if !compile_opts[:return_errors], do: print_diagnostics(diagnostics) - status = status(compile_opts[:warnings_as_errors], diagnostics) - - {status, diagnostics} + handle_diagnostics(diagnostics, compile_opts, :ok) end end + @doc false + def handle_diagnostics(diagnostics, compile_opts, status) do + if !compile_opts[:return_errors], do: print_diagnostics(diagnostics) + status = status(compile_opts[:warnings_as_errors], diagnostics, status) + {status, diagnostics} + end + defp print_diagnostics(diagnostics) do for %Diagnostic{message: message, severity: severity, file: file, position: position} <- diagnostics do print_diagnostic(message, severity, file, position) @@ -223,11 +244,38 @@ defmodule Mix.Tasks.Compile.Surface do IO.puts(:stderr, [error, message, ?\n, stacktrace]) end - defp status(warnings_as_errors, diagnostics) do + defp status(warnings_as_errors, diagnostics, default) do cond do Enum.any?(diagnostics, &(&1.severity == :error)) -> :error warnings_as_errors && Enum.any?(diagnostics, &(&1.severity == :warning)) -> :error - true -> :ok + true -> default + end + end + + @doc false + def manifests, do: [manifest()] + + defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest) + + defp read_manifest do + case File.read(manifest()) do + {:ok, contents} -> :erlang.binary_to_term(contents) + _ -> {:unknown, nil} end end + + defp write_manifest!(diagnostics) do + File.write!(manifest(), :erlang.term_to_binary({@manifest_version, diagnostics})) + end + + defp manifest_older? do + other_manifests = Mix.Tasks.Compile.Elixir.manifests() + manifest_mtime = mtime(manifest()) + Enum.any?(other_manifests, fn m -> mtime(m) > manifest_mtime end) + end + + defp mtime(file) do + %File.Stat{mtime: mtime} = File.stat!(file) + mtime + end end diff --git a/test/mix/tasks/compile/surface_test.exs b/test/mix/tasks/compile/surface_test.exs index 4be4a9a75..4aea99a43 100644 --- a/test/mix/tasks/compile/surface_test.exs +++ b/test/mix/tasks/compile/surface_test.exs @@ -54,7 +54,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do test "generate index.js with empty object if there's no hooks available" do refute File.exists?(@hooks_output_dir) - assert capture_io(:standard_error, fn -> run(["--return-errors"]) end) == "" + assert capture_io(:standard_error, fn -> run(["--return-errors", "--force"]) end) == "" assert File.read!(@hooks_index_file) == """ /* @@ -76,7 +76,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do output = capture_io(:standard_error, fn -> - assert {:ok, [^diagnostic]} = handle_diagnostics([diagnostic], []) + assert {:ok, [^diagnostic]} = handle_diagnostics([diagnostic], [], :ok) end) assert output =~ "warning:" @@ -95,7 +95,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do output = capture_io(:standard_error, fn -> assert {:error, [^diagnostic]} = - handle_diagnostics([diagnostic], return_errors: true, warnings_as_errors: true) + handle_diagnostics([diagnostic], [return_errors: true, warnings_as_errors: true], :ok) end) refute output =~ "test warning" @@ -112,7 +112,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do output = capture_io(:standard_error, fn -> - assert {:error, [^diagnostic]} = handle_diagnostics([diagnostic], []) + assert {:error, [^diagnostic]} = handle_diagnostics([diagnostic], [], :ok) end) assert output == (IO.ANSI.format([:red, "error: "]) |> IO.iodata_to_binary()) <> "test error\n file.ex:1\n" @@ -129,7 +129,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do output = capture_io(:standard_error, fn -> - assert {:error, [^diagnostic]} = handle_diagnostics([diagnostic], return_errors: true) + assert {:error, [^diagnostic]} = handle_diagnostics([diagnostic], [return_errors: true], :ok) end) assert output == "" From 0c74dc05de9d0c2ab01ea05b538df85bc3f519ba Mon Sep 17 00:00:00 2001 From: Marlus Saraiva Date: Wed, 28 Jan 2026 14:46:23 -0300 Subject: [PATCH 12/12] Remove useless code after removing support for < v1.14 --- lib/mix/tasks/compile/surface.ex | 12 ++------- lib/surface/io_helper.ex | 33 +++++-------------------- test/mix/tasks/compile/surface_test.exs | 9 ++++--- 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/lib/mix/tasks/compile/surface.ex b/lib/mix/tasks/compile/surface.ex index 4b46f50c9..ecfb630ce 100644 --- a/lib/mix/tasks/compile/surface.ex +++ b/lib/mix/tasks/compile/surface.ex @@ -222,16 +222,8 @@ defmodule Mix.Tasks.Compile.Surface do end end - if Version.match?(System.version(), ">= 1.14.0") do - defp print_diagnostic(message, :warning, file, {line, col}) do - IO.warn(message, file: file, line: line, column: col) - end - end - - # TODO: Remove this clause in Surface v0.13 and set required elixir to >= v1.14 - defp print_diagnostic(message, :warning, file, line) do - rel_file = file |> Path.relative_to_cwd() |> to_charlist() - IO.warn(message, [{nil, :__FILE__, 1, [file: rel_file, line: line]}]) + defp print_diagnostic(message, :warning, file, {line, col}) do + IO.warn(message, file: file, line: line, column: col) end defp print_diagnostic(message, :error, file, line) do diff --git a/lib/surface/io_helper.ex b/lib/surface/io_helper.ex index db9c9620f..198aed736 100644 --- a/lib/surface/io_helper.ex +++ b/lib/surface/io_helper.ex @@ -13,15 +13,8 @@ defmodule Surface.IOHelper do IO.warn(message, stacktrace) end - if Version.match?(System.version(), ">= 1.14.0") do - def warn(message, _caller, file, {line, column}) do - IO.warn(message, file: file, line: line, column: column) - end - else - # TODO: Remove this clause in Surface v0.13 and set required elixir to >= v1.14 - def warn(message, caller, file, {line, _column}) do - warn(message, caller, file, line) - end + def warn(message, _caller, file, {line, column}) do + IO.warn(message, file: file, line: line, column: column) end def warn(message, caller, file, line) do @@ -30,30 +23,16 @@ defmodule Surface.IOHelper do IO.warn(message, stacktrace) end - if Version.match?(System.version(), ">= 1.14.0") do - def compile_error(message, file, {line, column}) do - reraise(%Surface.CompileError{file: file, line: line, column: column, description: message}, []) - end - else - # TODO: Remove this clause in Surface v0.13 and set required elixir to >= v1.14 - def compile_error(message, file, {line, _column}) do - reraise(%Surface.CompileError{line: line, file: file, description: message}, []) - end + def compile_error(message, file, {line, column}) do + reraise(%Surface.CompileError{file: file, line: line, column: column, description: message}, []) end def compile_error(message, file, line) do reraise(%Surface.CompileError{line: line, file: file, description: message}, []) end - if Version.match?(System.version(), ">= 1.14.0") do - def compile_error(message, hint, file, {line, column}) do - reraise(%Surface.CompileError{file: file, line: line, column: column, description: message, hint: hint}, []) - end - else - # TODO: Remove this clause in Surface v0.13 and set required elixir to >= v1.14 - def compile_error(message, hint, file, {line, _column}) do - reraise(%Surface.CompileError{file: file, line: line, description: message, hint: hint}, []) - end + def compile_error(message, hint, file, {line, column}) do + reraise(%Surface.CompileError{file: file, line: line, column: column, description: message, hint: hint}, []) end def compile_error(message, hint, file, line) do diff --git a/test/mix/tasks/compile/surface_test.exs b/test/mix/tasks/compile/surface_test.exs index 4aea99a43..7b881d90e 100644 --- a/test/mix/tasks/compile/surface_test.exs +++ b/test/mix/tasks/compile/surface_test.exs @@ -4,6 +4,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do import Mix.Tasks.Compile.Surface import ExUnit.CaptureIO + import ANSIHelpers alias Mix.Task.Compiler.Diagnostic @@ -69,7 +70,7 @@ defmodule Mix.Tasks.Compile.SurfaceTest do diagnostic = %Diagnostic{ message: "test warning", file: "file.ex", - position: 1, + position: {1, 1}, severity: :warning, compiler_name: "Surface" } @@ -79,15 +80,15 @@ defmodule Mix.Tasks.Compile.SurfaceTest do assert {:ok, [^diagnostic]} = handle_diagnostics([diagnostic], [], :ok) end) - assert output =~ "warning:" - assert output =~ "test warning\n file.ex:1: (file)\n\n" + assert output =~ ~r"#{maybe_ansi("warning:")} test warning" + assert output =~ "file.ex:1: (file)\n\n" end test "don't print and return `{:error, diagnostics}` on warning with `return_errors` and `warnings_as_errors`" do diagnostic = %Diagnostic{ message: "test warning", file: "file.ex", - position: 1, + position: {1, 1}, severity: :warning, compiler_name: "Surface" }