From 1f8e8362478dc3208bd2544f5c55e2a72c24f126 Mon Sep 17 00:00:00 2001 From: David Green <134172184+green-david@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:09:11 -0500 Subject: [PATCH 1/2] Add ColocatedCSS with Scoped CSS Support --- config/e2e.exs | 2 + lib/mix/tasks/compile/phoenix_live_view.ex | 5 +- lib/phoenix_live_view/colocated_css.ex | 397 ++++++++++++++++++ package-lock.json | 24 +- package.json | 2 +- test/e2e/support/colocated_live.ex | 159 +++++++ test/e2e/test_helper.exs | 7 +- test/e2e/tests/colocated.spec.js | 82 ++++ test/phoenix_live_view/colocated_css_test.exs | 284 +++++++++++++ 9 files changed, 946 insertions(+), 16 deletions(-) create mode 100644 lib/phoenix_live_view/colocated_css.ex create mode 100644 test/phoenix_live_view/colocated_css_test.exs diff --git a/config/e2e.exs b/config/e2e.exs index 33e9a3fe20..349e351e7e 100644 --- a/config/e2e.exs +++ b/config/e2e.exs @@ -1,3 +1,5 @@ import Config config :logger, :level, :error + +config :phoenix_live_view, :root_tag_attribute, "phx-r" diff --git a/lib/mix/tasks/compile/phoenix_live_view.ex b/lib/mix/tasks/compile/phoenix_live_view.ex index 957698d013..a2a8683b19 100644 --- a/lib/mix/tasks/compile/phoenix_live_view.ex +++ b/lib/mix/tasks/compile/phoenix_live_view.ex @@ -2,8 +2,8 @@ defmodule Mix.Tasks.Compile.PhoenixLiveView do @moduledoc """ A LiveView compiler for HEEx macro components. - Right now, only `Phoenix.LiveView.ColocatedHook` and `Phoenix.LiveView.ColocatedJS` - are handled. + Right now, only `Phoenix.LiveView.ColocatedHook`, `Phoenix.LiveView.ColocatedJS`, + and `Phoenix.LiveView.ColocatedCSS` are handled. You must add it to your `mix.exs` as: @@ -30,5 +30,6 @@ defmodule Mix.Tasks.Compile.PhoenixLiveView do defp compile do Phoenix.LiveView.ColocatedJS.compile() + Phoenix.LiveView.ColocatedCSS.compile() end end diff --git a/lib/phoenix_live_view/colocated_css.ex b/lib/phoenix_live_view/colocated_css.ex new file mode 100644 index 0000000000..1933604fd5 --- /dev/null +++ b/lib/phoenix_live_view/colocated_css.ex @@ -0,0 +1,397 @@ +defmodule Phoenix.LiveView.ColocatedCSS do + @moduledoc ~S''' + A special HEEx `:type` that extracts any CSS styles from a colocated ` + ``` + + ## Scoped CSS + + By default, Colocated CSS styles are scoped at compile time to the template in which they are defined. + This provides style encapsulation preventing CSS rules within a component from unintentionally applying + to elements in other nested components. Scoping is performed via the use of the `@scope` CSS at-rule. + For more information, see [the docs on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope). + + To prevent Colocated CSS styles from being scoped to the current template you can provide the `global` + attribute, for example: + + ```heex + + ``` + + **Note:** When using Scoped Colocated CSS with implicit `inner_block` slots or named slots, the content + provided will be scoped to the parent template which is providing the content, not the component which + defines the slot. For example, in the following snippet the elements within [`intersperse/1`](`Phoenix.Component.intersperse/1`)'s + `inner_block` and `separator` slots will both be styled by the `.sample-class` rule, not any rules defined within the + [`intersperse/1`](`Phoenix.Component.intersperse/1`) component itself: + + ```heex + +
Item {item}
++ {if @should_flex?, do: "Should", else: "Shouldn't"} Flex {x} +
+ """ + end end diff --git a/test/e2e/test_helper.exs b/test/e2e/test_helper.exs index 5b60354ca2..538485a441 100644 --- a/test/e2e/test_helper.exs +++ b/test/e2e/test_helper.exs @@ -277,6 +277,10 @@ defmodule Phoenix.LiveViewTest.E2E.Endpoint do from: Path.join(Mix.Project.build_path(), "phoenix-colocated/phoenix_live_view"), at: "/assets/colocated" + plug Plug.Static, + from: Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view"), + at: "/assets/colocated_css" + plug Plug.Static, from: System.tmp_dir!(), at: "/tmp" plug :health_check @@ -316,8 +320,9 @@ end IO.puts("Starting e2e server on port #{Phoenix.LiveViewTest.E2E.Endpoint.config(:http)[:port]}") -# we need to manually compile the colocated hooks / js +# we need to manually compile the colocated hooks / js and css Phoenix.LiveView.ColocatedJS.compile() +Phoenix.LiveView.ColocatedCSS.compile() if not IEx.started?() do # when running the test server manually, we halt after diff --git a/test/e2e/tests/colocated.spec.js b/test/e2e/tests/colocated.spec.js index b0d6e1c672..2adfdd2daf 100644 --- a/test/e2e/tests/colocated.spec.js +++ b/test/e2e/tests/colocated.spec.js @@ -41,3 +41,85 @@ test("custom macro component works (syntax highlighting)", async ({ page }) => { page.locator("pre").nth(1).getByText("@temperature"), ).toHaveClass("na"); }); + +test("global colocated css works", async ({ page }) => { + await page.goto("/colocated"); + await syncLV(page); + + await expect(page.locator('[data-test="global"]')).toHaveCSS( + "background-color", + "rgb(255, 0, 0)", + ); +}); + +test("scoped colocated css works", async ({ page }) => { + await page.goto("/colocated"); + await syncLV(page); + + await expect(page.locator('[data-test="scoped"]')).toHaveCSS( + "background-color", + "rgba(0, 0, 0, 0)", + ); + + const blueLocator = page.locator('[data-test-scoped="blue"]'); + + await expect(blueLocator).toHaveCount(6); + + for (const shouldBeBlue in blueLocator.all()) { + await expect(shouldBeBlue).toHaveCSS("background-color", "rgb(0, 0, 255)"); + } + + const noneLocator = page.locator('[data-test-scoped="none"]'); + + await expect(noneLocator).toHaveCount(5); + + for (const shouldBeTransparent in noneLocator.all()) { + await expect(shouldBeTransparent).toHaveCSS( + "background-color", + "rgba(0, 0, 0, 0)", + ); + } + + await expect(page.locator('[data-test-scoped="yellow"]')).toHaveCSS( + "background-color", + "rgb(255, 255, 0)", + ); + + await expect(page.locator('[data-test-scoped="green"]')).toHaveCSS( + "background-color", + "rgb(0, 255, 0)", + ); +}); + +test("scoped colocated css lower bound inclusive/exclusive works", async ({ + page, +}) => { + await page.goto("/colocated"); + await syncLV(page); + + const lowerBoundContainerLocator = page.locator( + "[data-test-lower-bound-container]", + ); + + await expect(lowerBoundContainerLocator).toHaveCount(2); + + for (const shouldBeFlex in lowerBoundContainerLocator.all()) { + await expect(shouldBeFlex).toHaveCSS("display", "flex"); + } + + const inclusiveFlexItemsLocator = page.locator('[data-test-inclusive="yes"]'); + + await expect(inclusiveFlexItemsLocator).toHaveCount(3); + + for (const shouldFlex in inclusiveFlexItemsLocator.all()) { + await expect(shouldFlex).toHaveCSS("flex", "1"); + } + + const exclusiveFlexItemsLocator = page.locator('[data-test-inclusive="yes"]'); + + await expect(exclusiveFlexItemsLocator).toHaveCount(3); + + for (const shouldntFlex in exclusiveFlexItemsLocator.all()) { + await expect(shouldntFlex).not().toHaveCSS("flex", "1"); + } +}); diff --git a/test/phoenix_live_view/colocated_css_test.exs b/test/phoenix_live_view/colocated_css_test.exs new file mode 100644 index 0000000000..edb9fb8fd3 --- /dev/null +++ b/test/phoenix_live_view/colocated_css_test.exs @@ -0,0 +1,284 @@ +defmodule Phoenix.LiveView.ColocatedCSSTest do + # we set async: false because we call the colocated CSS compiler + # and it reads / writes to a shared folder, and also because + # we manipulate the Application env for :root_tag_attribute + use ExUnit.Case, async: false + + alias Phoenix.LiveView.TagEngine.Tokenizer.ParseError + + setup do + Application.put_env(:phoenix_live_view, :root_tag_attribute, "phx-r") + on_exit(fn -> Application.delete_env(:phoenix_live_view, :root_tag_attribute) end) + end + + describe "global styles" do + test "are extracted and available under manifest import" do + defmodule TestGlobalComponent do + use Phoenix.Component + + def fun(assigns) do + ~H""" + + """ + end + end + + assert module_folders = + File.ls!( + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view") + ) + + assert folder = + Enum.find(module_folders, fn folder -> + folder =~ ~r/#{inspect(__MODULE__)}\.TestGlobalComponent$/ + end) + + assert [style] = + Path.wildcard( + Path.join( + Mix.Project.build_path(), + "phoenix-colocated-css/phoenix_live_view/#{folder}/*.css" + ) + ) + + assert File.read!(style) == "\n .sample-class { background-color: #FFFFFF; }\n" + + # now write the manifest manually as we are in a test + Phoenix.LiveView.ColocatedCSS.compile() + + assert manifest = + File.read!( + Path.join( + Mix.Project.build_path(), + "phoenix-colocated-css/phoenix_live_view/colocated.css" + ) + ) + + path = + Path.relative_to( + style, + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view/") + ) + + # style is in manifest + assert manifest =~ ~s[@import "./#{path}";\n] + after + :code.delete(__MODULE__.TestGlobalComponent) + :code.purge(__MODULE__.TestGlobalComponent) + end + + test "raises for invalid global attribute value" do + message = ~r/expected nil or true for the `global` attribute of colocated css, got: "bad"/ + + assert_raise ParseError, + message, + fn -> + defmodule TestBadGlobalAttrComponent do + use Phoenix.Component + + def fun(assigns) do + ~H""" + + """ + end + end + end + after + :code.delete(__MODULE__.TestBadGlobalAttrComponent) + :code.purge(__MODULE__.TestBadGlobalAttrComponent) + end + + test "raises if scoped css specific options are provided" do + message = + ~r/colocated css must be scoped to use the `lower-bound` attribute, but `global` attribute was provided/ + + assert_raise ParseError, + message, + fn -> + defmodule TestScopedAttrWhileGlobalComponent do + use Phoenix.Component + + def fun(assigns) do + ~H""" + + """ + end + end + end + after + :code.delete(__MODULE__.TestScopedAttrWhileGlobalComponent) + :code.purge(__MODULE__.TestScopedAttrWhileGlobalComponent) + end + end + + describe "scoped styles" do + test "with exclusive (default) lower-bound is extracted and available under manifest import" do + defmodule TestScopedExclusiveComponent do + use Phoenix.Component + + def fun(assigns) do + ~H""" + + """ + end + end + + assert module_folders = + File.ls!( + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view") + ) + + assert folder = + Enum.find(module_folders, fn folder -> + folder =~ ~r/#{inspect(__MODULE__)}\.TestScopedExclusiveComponent$/ + end) + + assert [style] = + Path.wildcard( + Path.join( + Mix.Project.build_path(), + "phoenix-colocated-css/phoenix_live_view/#{folder}/*.css" + ) + ) + + file_contents = File.read!(style) + + file_contents = + Regex.replace(~r/phx-css=".+?"/, file_contents, ~s|phx-css="SCOPE_HERE"|) + + # The scope is a generated value, so for testing reliability we just replace it with a known + # value to assert against. + assert file_contents == + ~s|@scope ([phx-css="SCOPE_HERE"]) to ([phx-r]) { \n .sample-class { background-color: #FFFFFF; }\n }| + + # now write the manifest manually as we are in a test + Phoenix.LiveView.ColocatedCSS.compile() + + assert manifest = + File.read!( + Path.join( + Mix.Project.build_path(), + "phoenix-colocated-css/phoenix_live_view/colocated.css" + ) + ) + + path = + Path.relative_to( + style, + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view/") + ) + + # style is in manifest + assert manifest =~ ~s[@import "./#{path}";\n] + after + :code.delete(__MODULE__.TestScopedExclusiveComponent) + :code.purge(__MODULE__.TestScopedExclusiveComponent) + end + + test "with inclusive lower-bound is extracted and available under manifest import" do + defmodule TestScopedInclusiveComponent do + use Phoenix.Component + + def fun(assigns) do + ~H""" + + """ + end + end + + assert module_folders = + File.ls!( + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view") + ) + + assert folder = + Enum.find(module_folders, fn folder -> + folder =~ ~r/#{inspect(__MODULE__)}\.TestScopedInclusiveComponent$/ + end) + + assert [style] = + Path.wildcard( + Path.join( + Mix.Project.build_path(), + "phoenix-colocated-css/phoenix_live_view/#{folder}/*.css" + ) + ) + + file_contents = File.read!(style) + + file_contents = + Regex.replace(~r/phx-css=".+?"/, file_contents, ~s|phx-css="SCOPE_HERE"|) + + # The scope is a generated value, so for testing reliability we just replace it with a known + # value to assert against. + assert file_contents == + ~s|@scope ([phx-css="SCOPE_HERE"]) to ([phx-r] > *) { \n .sample-class { background-color: #FFFFFF; }\n }| + + # now write the manifest manually as we are in a test + Phoenix.LiveView.ColocatedCSS.compile() + + assert manifest = + File.read!( + Path.join( + Mix.Project.build_path(), + "phoenix-colocated-css/phoenix_live_view/colocated.css" + ) + ) + + path = + Path.relative_to( + style, + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view/") + ) + + # style is in manifest + assert manifest =~ ~s[@import "./#{path}";\n] + after + :code.delete(__MODULE__.TestScopedInclusiveComponent) + :code.purge(__MODULE__.TestScopedInclusiveComponent) + end + + test "raises for invalid lower-bound attribute value" do + message = + ~r/expected "inclusive" or "exclusive" for the `lower-bound` attribute of colocated css, got: "unknown"/ + + assert_raise ParseError, + message, + fn -> + defmodule TestBadLowerBoundAttrComponent do + use Phoenix.Component + + def fun(assigns) do + ~H""" + + """ + end + end + end + after + :code.delete(__MODULE__.TestBadLowerBoundAttrComponent) + :code.purge(__MODULE__.TestBadLowerBoundAttrComponent) + end + end + + test "writes empty manifest when no colocated styles exist" do + manifest = + Path.join(Mix.Project.build_path(), "phoenix-colocated-css/phoenix_live_view/colocated.css") + + Phoenix.LiveView.ColocatedCSS.compile() + assert File.exists?(manifest) + assert File.read!(manifest) == "" + end +end From 7660ebcda7cc9925ef3c764ee1399911f95edf3d Mon Sep 17 00:00:00 2001 From: David Green <134172184+green-david@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:09:16 -0500 Subject: [PATCH 2/2] Address review comments --- .github/workflows/ci.yml | 2 +- lib/phoenix_live_view/colocated_css.ex | 19 +++++++++++-------- mix.exs | 2 +- mix.lock | 14 +++++++------- test/phoenix_live_view/colocated_css_test.exs | 8 ++++---- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83846a7727..83a0301de3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,7 +173,7 @@ jobs: runs-on: ubuntu-latest container: - image: mcr.microsoft.com/playwright:v1.56.1-jammy + image: mcr.microsoft.com/playwright:v1.58.0-jammy env: ImageOS: ubuntu22 HOME: /root diff --git a/lib/phoenix_live_view/colocated_css.ex b/lib/phoenix_live_view/colocated_css.ex index 1933604fd5..67e4b6da13 100644 --- a/lib/phoenix_live_view/colocated_css.ex +++ b/lib/phoenix_live_view/colocated_css.ex @@ -182,7 +182,7 @@ defmodule Phoenix.LiveView.ColocatedCSS do {scope, data} = extract(opts, text_content, meta) # we always drop colocated CSS from the rendered output - {:ok, "", data, [root_tag_attribute: {"phx-css", scope}]} + {:ok, "", data, [root_tag_attribute: {"phx-css-#{scope}", true}]} end def transform(_ast, _meta) do @@ -192,9 +192,8 @@ defmodule Phoenix.LiveView.ColocatedCSS do defp validate_phx_version! do phoenix_version = to_string(Application.spec(:phoenix, :vsn)) - if not Version.match?(phoenix_version, "~> 1.8.0-rc.4") do - # TODO: bump message to 1.8 once released to avoid confusion - raise ArgumentError, ~s|ColocatedCSS requires at least {:phoenix, "~> 1.8.0-rc.4"}| + if not Version.match?(phoenix_version, "~> 1.8.0") do + raise ArgumentError, ~s|ColocatedCSS requires at least {:phoenix, "~> 1.8.0"}| end end @@ -234,10 +233,10 @@ defmodule Phoenix.LiveView.ColocatedCSS do # _build/dev/phoenix-colocated-css/otp_app/MyApp.MyComponent/line_no.css target_path = Path.join(target_dir(), inspect(meta.env.module)) - scope = scope(meta) + scope = scope(text_content, meta) root_tag_attribute = root_tag_attribute() - upper_bound_selector = ~s|[phx-css="#{scope}"]| + upper_bound_selector = ~s|[phx-css-#{scope}]| lower_bound_selector = ~s|[#{root_tag_attribute}]| lower_bound_selector = @@ -266,11 +265,15 @@ defmodule Phoenix.LiveView.ColocatedCSS do {scope, filename} end - defp scope(meta) do - hash("#{meta.env.module}_#{meta.env.line}") + defp scope(text_content, meta) do + hash("#{meta.env.module}_#{meta.env.line}: #{text_content}") end defp hash(string) do + # It is important that we do not pad + # the Base32 encoded value as we use it in + # an HTML attribute name and = (the padding character) + # is not valid. string |> then(&:crypto.hash(:md5, &1)) |> Base.encode32(case: :lower, padding: false) diff --git a/mix.exs b/mix.exs index bc7e2260b8..0e2792f1ca 100644 --- a/mix.exs +++ b/mix.exs @@ -46,7 +46,7 @@ defmodule Phoenix.LiveView.MixProject do defp deps do [ {:igniter, "~> 0.6 and >= 0.6.16", optional: true}, - {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc"}, + {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0"}, {:plug, "~> 1.15"}, {:phoenix_template, "~> 1.0"}, {:phoenix_html, "~> 3.3 or ~> 4.0 or ~> 4.1"}, diff --git a/mix.lock b/mix.lock index cc416e1c20..f846c750d8 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"}, + "bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"}, "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, @@ -28,15 +28,15 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"}, - "phoenix": {:hex, :phoenix, "1.8.0-rc.4", "6c18c1e07938d3d8dbb957ed0d193fa591718a2997058f6883cfa7447f07612a", [: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", "754c8caf0d1332bc691f826d678b192b3f78cfeb01df2f623683e308b363dc41"}, + "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_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "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"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [: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", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "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"}, "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, @@ -45,7 +45,7 @@ "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, - "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "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/phoenix_live_view/colocated_css_test.exs b/test/phoenix_live_view/colocated_css_test.exs index edb9fb8fd3..912b7de13c 100644 --- a/test/phoenix_live_view/colocated_css_test.exs +++ b/test/phoenix_live_view/colocated_css_test.exs @@ -152,12 +152,12 @@ defmodule Phoenix.LiveView.ColocatedCSSTest do file_contents = File.read!(style) file_contents = - Regex.replace(~r/phx-css=".+?"/, file_contents, ~s|phx-css="SCOPE_HERE"|) + Regex.replace(~r/\[phx-css-.+?\]/, file_contents, ~s|[phx-css-SCOPE_HERE]|) # The scope is a generated value, so for testing reliability we just replace it with a known # value to assert against. assert file_contents == - ~s|@scope ([phx-css="SCOPE_HERE"]) to ([phx-r]) { \n .sample-class { background-color: #FFFFFF; }\n }| + ~s|@scope ([phx-css-SCOPE_HERE]) to ([phx-r]) { \n .sample-class { background-color: #FFFFFF; }\n }| # now write the manifest manually as we are in a test Phoenix.LiveView.ColocatedCSS.compile() @@ -217,12 +217,12 @@ defmodule Phoenix.LiveView.ColocatedCSSTest do file_contents = File.read!(style) file_contents = - Regex.replace(~r/phx-css=".+?"/, file_contents, ~s|phx-css="SCOPE_HERE"|) + Regex.replace(~r/\[phx-css-.+?\]/, file_contents, ~s|[phx-css-SCOPE_HERE]|) # The scope is a generated value, so for testing reliability we just replace it with a known # value to assert against. assert file_contents == - ~s|@scope ([phx-css="SCOPE_HERE"]) to ([phx-r] > *) { \n .sample-class { background-color: #FFFFFF; }\n }| + ~s|@scope ([phx-css-SCOPE_HERE]) to ([phx-r] > *) { \n .sample-class { background-color: #FFFFFF; }\n }| # now write the manifest manually as we are in a test Phoenix.LiveView.ColocatedCSS.compile()