Repository: phoenixframework/phoenix
Pull request #4400 (2021-08-05 18:50:39)

Add --all flag to phx.digest.clean
This PR allows `mix phx.digest` to be fully reversed by the `phx.digest.clean` task:

```sh
mix phx.digest.clean --all
```

When run with the `--all` flag, the following files are cleaned:

* All digested versions listed in the `cache_manifest.json` file (including latest).
* All compressed originals (e.g `robots.txt.gz`).
* The `cache_manifest.json` file itself.


Patch:
diff --git a/installer/lib/mix/tasks/local.phx.ex b/installer/lib/mix/tasks/local.phx.ex
index 63de0fe16..005ab0b2b 100644
--- a/installer/lib/mix/tasks/local.phx.ex
+++ b/installer/lib/mix/tasks/local.phx.ex
@@ -6,7 +6,7 @@ defmodule Mix.Tasks.Local.Phx do
   @moduledoc """
   Updates the Phoenix project generator locally.
 
-      mix local.phx
+      $ mix local.phx
 
   Accepts the same command line options as `archive.install hex phx_new`.
   """
diff --git a/installer/lib/mix/tasks/phx.new.ecto.ex b/installer/lib/mix/tasks/phx.new.ecto.ex
index 8fdacb14d..6b8673bb0 100644
--- a/installer/lib/mix/tasks/phx.new.ecto.ex
+++ b/installer/lib/mix/tasks/phx.new.ecto.ex
@@ -38,11 +38,11 @@ defmodule Mix.Tasks.Phx.New.Ecto do
 
   ## Examples
 
-      mix phx.new.ecto hello_ecto
+      $ mix phx.new.ecto hello_ecto
 
   Is equivalent to:
 
-      mix phx.new.ecto hello_ecto --module HelloEcto
+      $ mix phx.new.ecto hello_ecto --module HelloEcto
   """
 
   @shortdoc "Creates a new Ecto project within an umbrella project"
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index c0b426295..005b17b53 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.New do
 
   It expects the path of the project as an argument.
 
-      mix phx.new PATH [--module MODULE] [--app APP]
+      $ mix phx.new PATH [--module MODULE] [--app APP]
 
   A project at the given PATH will be created. The
   application name and module name will be retrieved
@@ -74,26 +74,28 @@ defmodule Mix.Tasks.Phx.New do
 
   ## Examples
 
-      mix phx.new hello_world
+      $ mix phx.new hello_world
 
   Is equivalent to:
 
-      mix phx.new hello_world --module HelloWorld
+      $ mix phx.new hello_world --module HelloWorld
 
   Or without the HTML and JS bits (useful for APIs):
 
-      mix phx.new ~/Workspace/hello_world --no-html --no-assets
+      $ mix phx.new ~/Workspace/hello_world --no-html --no-assets
 
   As an umbrella:
 
-      mix phx.new hello --umbrella
+      $ mix phx.new hello --umbrella
 
   Would generate the following directory structure and modules:
 
-      hello_umbrella/   Hello.Umbrella
-        apps/
-          hello/        Hello
-          hello_web/    HelloWeb
+  ```text
+  hello_umbrella/   Hello.Umbrella
+    apps/
+      hello/        Hello
+      hello_web/    HelloWeb
+  ```
 
   You can read more about umbrella projects using the
   official [Elixir guide](http://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html#umbrella-projects)
diff --git a/installer/lib/mix/tasks/phx.new.web.ex b/installer/lib/mix/tasks/phx.new.web.ex
index f4cdb2084..097da7a87 100644
--- a/installer/lib/mix/tasks/phx.new.web.ex
+++ b/installer/lib/mix/tasks/phx.new.web.ex
@@ -15,11 +15,11 @@ defmodule Mix.Tasks.Phx.New.Web do
 
   ## Examples
 
-      mix phx.new.web hello_web
+      $ mix phx.new.web hello_web
 
   Is equivalent to:
 
-      mix phx.new.web hello_web --module HelloWeb
+      $ mix phx.new.web hello_web --module HelloWeb
 
   Supports the same options as the `phx.new` task.
   See `Mix.Tasks.Phx.New` for details.
diff --git a/lib/mix/tasks/compile.phoenix.ex b/lib/mix/tasks/compile.phoenix.ex
index a97a81c71..cc1109a30 100644
--- a/lib/mix/tasks/compile.phoenix.ex
+++ b/lib/mix/tasks/compile.phoenix.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Compile.Phoenix do
 
   @moduledoc """
   Compiles Phoenix source files that support code reloading.
-  
+
   If you are using Elixir v1.11+ or later, there is no longer
   a need to use this module as this functionality is now provided
   by Elixir. Just remember to update `__phoenix_recompile__?` to
diff --git a/lib/mix/tasks/phx.digest.clean.ex b/lib/mix/tasks/phx.digest.clean.ex
index 5ec2e5d46..8dc5195f2 100644
--- a/lib/mix/tasks/phx.digest.clean.ex
+++ b/lib/mix/tasks/phx.digest.clean.ex
@@ -14,9 +14,9 @@ defmodule Mix.Tasks.Phx.Digest.Clean do
   2 previous versions as well as any digest created
   in the last hour.
 
-      mix phx.digest.clean
-      mix phx.digest.clean -o /www/public
-      mix phx.digest.clean --age 600 --keep 3
+      $ mix phx.digest.clean
+      $ mix phx.digest.clean -o /www/public
+      $ mix phx.digest.clean --age 600 --keep 3
 
   ## Options
 
@@ -30,19 +30,27 @@ defmodule Mix.Tasks.Phx.Digest.Clean do
     * `--keep` - specifies how many previous versions of assets to keep.
       Defaults to 2 previous versions
 
+    * `--all` - specifies that all compiled assets (including the manifest)
+      will be removed. Note this overrides the age and keep switches.
   """
 
   @doc false
   def run(args) do
-    switches = [output: :string, age: :integer, keep: :integer]
+    switches = [output: :string, age: :integer, keep: :integer, all: :boolean]
     {opts, _, _} = OptionParser.parse(args, switches: switches, aliases: [o: :output])
     output_path = opts[:output] || @default_output_path
     age = opts[:age] || @default_age
     keep = opts[:keep] || @default_keep
+    all? = opts[:all] || false
 
     {:ok, _} = Application.ensure_all_started(:phoenix)
 
-    case Phoenix.Digester.clean(output_path, age, keep) do
+    result =
+      if all?,
+        do: Phoenix.Digester.clean_all(output_path),
+        else: Phoenix.Digester.clean(output_path, age, keep)
+
+    case result do
       :ok ->
         # We need to call build structure so everything we have cleaned from
         # priv is removed from _build in case we have build_embedded set to
diff --git a/lib/mix/tasks/phx.digest.ex b/lib/mix/tasks/phx.digest.ex
index e17271268..da9004587 100644
--- a/lib/mix/tasks/phx.digest.ex
+++ b/lib/mix/tasks/phx.digest.ex
@@ -8,8 +8,8 @@ defmodule Mix.Tasks.Phx.Digest do
   @moduledoc """
   Digests and compresses static files.
 
-      mix phx.digest
-      mix phx.digest priv/static -o /www/public
+      $ mix phx.digest
+      $ mix phx.digest priv/static -o /www/public
 
   The first argument is the path where the static files are located. The
   `-o` option indicates the path that will be used to save the digested and
diff --git a/lib/mix/tasks/phx.ex b/lib/mix/tasks/phx.ex
index b76275317..2316dd098 100644
--- a/lib/mix/tasks/phx.ex
+++ b/lib/mix/tasks/phx.ex
@@ -6,11 +6,11 @@ defmodule Mix.Tasks.Phx do
   @moduledoc """
   Prints Phoenix tasks and their information.
 
-      mix phx
+      $ mix phx
 
   To print the Phoenix version, pass `-v` or `--version`, for example:
 
-      mix phx --version
+      $ mix phx --version
 
   """
 
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index 7ffba1d28..18bf8ee7c 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   @moduledoc """
   Generates authentication logic for a resource.
 
-      mix phx.gen.auth Accounts User users
+      $ mix phx.gen.auth Accounts User users
 
   The first argument is the context module followed by the schema module
   and its plural name (used as the schema table name).
@@ -34,7 +34,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   You can customize the web module namespace by passing the `--web` flag with a
   module name, for example:
 
-      mix phx.gen.auth Accounts User users --web Warehouse
+      $ mix phx.gen.auth Accounts User users --web Warehouse
 
   Which would generate the controllers, views, templates and associated tests in nested in the `MyAppWeb.Warehouse` namespace:
 
@@ -70,7 +70,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   the plural name provided for the resource. To customize this value,
   a `--table` option may be provided. For example:
 
-      mix phx.gen.auth Accounts User users --table accounts_users
+      $ mix phx.gen.auth Accounts User users --table accounts_users
 
   This will cause the generated tables to be named `"accounts_users"` and `"accounts_users_tokens"`.
   """
@@ -81,9 +81,14 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   alias Mix.Tasks.Phx.Gen
   alias Mix.Tasks.Phx.Gen.Auth.{HashingLibrary, Injector, Migration}
 
-  @switches [web: :string, binary_id: :boolean, hashing_lib: :string,
-             table: :string, merge_with_existing_context: :boolean,
-             prefix: :string]
+  @switches [
+    web: :string,
+    binary_id: :boolean,
+    hashing_lib: :string,
+    table: :string,
+    merge_with_existing_context: :boolean,
+    prefix: :string
+  ]
 
   @doc false
   def run(args, test_opts \\ []) do
diff --git a/lib/mix/tasks/phx.gen.cert.ex b/lib/mix/tasks/phx.gen.cert.ex
index 9b3e411f1..84b6d1e1b 100644
--- a/lib/mix/tasks/phx.gen.cert.ex
+++ b/lib/mix/tasks/phx.gen.cert.ex
@@ -18,8 +18,8 @@ defmodule Mix.Tasks.Phx.Gen.Cert do
   @moduledoc """
   Generates a self-signed certificate for HTTPS testing.
 
-      mix phx.gen.cert
-      mix phx.gen.cert my-app my-app.local my-app.internal.example.com
+      $ mix phx.gen.cert
+      $ mix phx.gen.cert my-app my-app.local my-app.internal.example.com
 
   Creates a private key and a self-signed certificate in PEM format. These
   files can be referenced in the `certfile` and `keyfile` parameters of an
diff --git a/lib/mix/tasks/phx.gen.channel.ex b/lib/mix/tasks/phx.gen.channel.ex
index eb4944a71..1700e8b3d 100644
--- a/lib/mix/tasks/phx.gen.channel.ex
+++ b/lib/mix/tasks/phx.gen.channel.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Channel do
   @moduledoc """
   Generates a Phoenix channel.
 
-      mix phx.gen.channel Room
+      $ mix phx.gen.channel Room
 
   Accepts the module name for the channel
 
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 2193d6561..9930d4b12 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   @moduledoc """
   Generates a context with functions around an Ecto schema.
 
-      mix phx.gen.context Accounts User users name:string age:integer
+      $ mix phx.gen.context Accounts User users name:string age:integer
 
   The first argument is the context module followed by the schema module
   and its plural name (used as the schema table name).
@@ -40,7 +40,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   the plural name provided for the resource. To customize this value,
   a `--table` option may be provided. For example:
 
-      mix phx.gen.context Accounts User users --table cms_users
+      $ mix phx.gen.context Accounts User users --table cms_users
 
   ## binary_id
 
diff --git a/lib/mix/tasks/phx.gen.notifier.ex b/lib/mix/tasks/phx.gen.notifier.ex
index 886e2dd87..c84301bcf 100644
--- a/lib/mix/tasks/phx.gen.notifier.ex
+++ b/lib/mix/tasks/phx.gen.notifier.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Notifier do
   @moduledoc """
   Generates a notifier that delivers emails by default.
 
-      mix phx.gen.notifier Accounts User welcome_user reset_password confirmation_instructions
+      $ mix phx.gen.notifier Accounts User welcome_user reset_password confirmation_instructions
 
   This task expects a context module name, followed by a
   notifier name and one or more message names. Messages
@@ -15,7 +15,7 @@ defmodule Mix.Tasks.Phx.Gen.Notifier do
   `--context-app`, which is useful if the notifier is being
   generated in a different app under an umbrella.
 
-      mix phx.gen.notifier Accounts User welcome_user --context-app marketing
+      $ mix phx.gen.notifier Accounts User welcome_user --context-app marketing
 
   The app "marketing" must exist before the command is executed.
   """
diff --git a/lib/mix/tasks/phx.gen.presence.ex b/lib/mix/tasks/phx.gen.presence.ex
index 4dbb6d327..ba3b49024 100644
--- a/lib/mix/tasks/phx.gen.presence.ex
+++ b/lib/mix/tasks/phx.gen.presence.ex
@@ -4,8 +4,8 @@ defmodule Mix.Tasks.Phx.Gen.Presence do
   @moduledoc """
   Generates a Presence tracker.
 
-      mix phx.gen.presence
-      mix phx.gen.presence MyPresence
+      $ mix phx.gen.presence
+      $ mix phx.gen.presence MyPresence
 
   The argument, which defaults to `Presence`, defines the module name of the
   Presence tracker.
diff --git a/lib/mix/tasks/phx.gen.schema.ex b/lib/mix/tasks/phx.gen.schema.ex
index 627f7173e..8ce05b557 100644
--- a/lib/mix/tasks/phx.gen.schema.ex
+++ b/lib/mix/tasks/phx.gen.schema.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   @moduledoc """
   Generates an Ecto schema and migration.
 
-      mix phx.gen.schema Blog.Post blog_posts title:string views:integer
+      $ mix phx.gen.schema Blog.Post blog_posts title:string views:integer
 
   The first argument is the schema module followed by its plural
   name (used as the table name).
@@ -23,11 +23,13 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   specify the context app with the `--context-app` option with
   the CLI.
 
-      # Via config
+  Via config:
+
       config :marketing_web, :generators, context_app: :marketing
 
-      # Via CLI
-      mix phx.gen.schema Blog.Post blog_posts title:string views:integer --context-app marketing
+  Via CLI:
+
+      $ mix phx.gen.schema Blog.Post blog_posts title:string views:integer --context-app marketing
 
   ## Attributes
 
@@ -35,7 +37,7 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   where type are the types supported by Ecto. Omitting
   the type makes it default to `:string`:
 
-      mix phx.gen.schema Blog.Post blog_posts title views:integer
+      $ mix phx.gen.schema Blog.Post blog_posts title views:integer
 
   The following types are supported:
 
@@ -46,7 +48,7 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   associate the given column to the primary key column of the
   referenced table:
 
-      mix phx.gen.schema Blog.Post blog_posts title user_id:references:users
+      $ mix phx.gen.schema Blog.Post blog_posts title user_id:references:users
 
   This will result in a migration with an `:integer` column
   of `:user_id` and create an index.
@@ -55,19 +57,19 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   supported by your database, although it requires the
   type of the underlying array element to be given too:
 
-      mix phx.gen.schema Blog.Post blog_posts tags:array:string
+      $ mix phx.gen.schema Blog.Post blog_posts tags:array:string
 
   Unique columns can be automatically generated by using:
 
-      mix phx.gen.schema Blog.Post blog_posts title:unique unique_int:integer:unique
+      $ mix phx.gen.schema Blog.Post blog_posts title:unique unique_int:integer:unique
 
   Redact columns can be automatically generated by using:
 
-      mix phx.gen.schema Accounts.Superhero superheroes secret_identity:redact password:string:redact
+      $ mix phx.gen.schema Accounts.Superhero superheroes secret_identity:redact password:string:redact
 
   Ecto.Enum fields can be generated by using:
 
-      mix phx.gen.schema Blog.Post blog_posts title status:enum:unpublished:published:deleted
+      $ mix phx.gen.schema Blog.Post blog_posts title status:enum:unpublished:published:deleted
 
   If no data type is given, it defaults to a string.
 
@@ -77,7 +79,7 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
   the plural name provided for the resource. To customize this value,
   a `--table` option may be provided. For example:
 
-      mix phx.gen.schema Blog.Post posts --table cms_posts
+      $ mix phx.gen.schema Blog.Post posts --table cms_posts
 
   ## binary_id
 
diff --git a/lib/mix/tasks/phx.gen.secret.ex b/lib/mix/tasks/phx.gen.secret.ex
index 277ff088e..91e39a24e 100644
--- a/lib/mix/tasks/phx.gen.secret.ex
+++ b/lib/mix/tasks/phx.gen.secret.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Secret do
   @moduledoc """
   Generates a secret and prints it to the terminal.
 
-      mix phx.gen.secret [length]
+      $ mix phx.gen.secret [length]
 
   By default, mix phx.gen.secret generates a key 64 characters long.
 
diff --git a/lib/mix/tasks/phx.gen.socket.ex b/lib/mix/tasks/phx.gen.socket.ex
index a3d1df2fa..6aaca6d99 100644
--- a/lib/mix/tasks/phx.gen.socket.ex
+++ b/lib/mix/tasks/phx.gen.socket.ex
@@ -4,7 +4,7 @@ defmodule Mix.Tasks.Phx.Gen.Socket do
   @moduledoc """
   Generates a Phoenix socket handler.
 
-      mix phx.gen.socket User
+      $ mix phx.gen.socket User
 
   Accepts the module name for the socket
 
diff --git a/lib/mix/tasks/phx.server.ex b/lib/mix/tasks/phx.server.ex
index 4a317d3d7..048982adb 100644
--- a/lib/mix/tasks/phx.server.ex
+++ b/lib/mix/tasks/phx.server.ex
@@ -18,7 +18,7 @@ defmodule Mix.Tasks.Phx.Server do
 
   For example, to run `phx.server` without recompiling:
 
-      mix phx.server --no-compile
+      $ mix phx.server --no-compile
 
   The `--no-halt` flag is automatically added.
 
@@ -27,7 +27,7 @@ defmodule Mix.Tasks.Phx.Server do
 
   To run `phx.server` without checking dependencies, you can run:
 
-      mix do deps.loadpaths --no-deps-check, phx.server
+      $ mix do deps.loadpaths --no-deps-check, phx.server
   """
 
   @impl true
diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex
index f20fe7dde..27727c197 100644
--- a/lib/phoenix/digester.ex
+++ b/lib/phoenix/digester.ex
@@ -94,6 +94,10 @@ defmodule Phoenix.Digester do
     File.write!(Path.join(output_path, "cache_manifest.json"), manifest_content)
   end
 
+  defp remove_manifest(output_path) do
+    File.rm(Path.join(output_path, "cache_manifest.json"))
+  end
+
   defp generate_digests(files) do
     Map.new(
       files,
@@ -294,6 +298,35 @@ defmodule Phoenix.Digester do
     end
   end
 
+  @doc """
+  Deletes compiled/compressed asset files, including the cache manifest.
+
+  ## Arguments
+
+    * `path` - The path where the compiled/compressed files are saved
+
+  """
+  @spec clean_all(String.t()) :: :ok | {:error, :invalid_path}
+  def clean_all(path) do
+    if File.exists?(path) do
+      %{"digests" => digests} = load_manifest(path)
+      grouped_digests = group_by_logical_path(digests)
+      logical_paths = Map.keys(grouped_digests)
+
+      files =
+        for {_, versions} <- grouped_digests,
+            file <- Enum.map(versions, fn {path, _attrs} -> path end),
+            do: file
+
+      remove_files(files, path)
+      remove_compressed_files(logical_paths, path)
+      remove_manifest(path)
+      :ok
+    else
+      {:error, :invalid_path}
+    end
+  end
+
   defp files_to_clean(latest, digests, max_age, keep) do
     digests = Map.drop(digests, Map.values(latest))
 
@@ -321,9 +354,17 @@ defmodule Phoenix.Digester do
       |> Path.join(file)
       |> File.rm()
 
-      output_path
-      |> Path.join("#{file}.gz")
-      |> File.rm()
+      remove_compressed_file(file, output_path)
     end
   end
+
+  defp remove_compressed_files(files, output_path) do
+    for file <- files, do: remove_compressed_file(file, output_path)
+  end
+
+  defp remove_compressed_file(file, output_path) do
+    output_path
+    |> Path.join("#{file}.gz")
+    |> File.rm()
+  end
 end
diff --git a/mix.lock b/mix.lock
index 5c53f41c6..70e962544 100644
--- a/mix.lock
+++ b/mix.lock
@@ -5,11 +5,11 @@
   "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
   "db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"},
   "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
-  "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
+  "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
   "ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
   "ecto_sql": {:hex, :ecto_sql, "3.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"},
   "esbuild": {:hex, :esbuild, "0.1.1", "055212528facbc1b05518ce998b636a2402f75c87d5ba90ba41c5b97e4c791f5", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "b864ce1b4d87bd6e7084b902ebb77c98e12aa03d4dc71db6b15fd2668db8f3b2"},
-  "ex_doc": {:hex, :ex_doc, "0.24.1", "15673de99154f93ca7f05900e4e4155ced1ee0cd34e0caeee567900a616871a4", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "07972f17bdf7dc7b5bd76ec97b556b26178ed3f056e7ec9288eb7cea7f91cce2"},
+  "ex_doc": {:hex, :ex_doc, "0.25.1", "4b736fa38dc76488a937e5ef2944f5474f3eff921de771b25371345a8dc810bc", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3200b0a69ddb2028365281fbef3753ea9e728683863d8cdaa96580925c891f67"},
   "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm", "d17ac3aa40835f7feb92913fcdf655edea59938fa8b25f5ce5d423e76a695c3b"},
   "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b96c400e04b7b765c0854c05a4966323e90c0d11fee0483b1567cda079abb205"},
   "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},


Test patch:
diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs
index b52971502..f5a02d2e6 100644
--- a/test/phoenix/digester_test.exs
+++ b/test/phoenix/digester_test.exs
@@ -434,6 +434,51 @@ defmodule Phoenix.DigesterTest do
     end
   end
 
+  describe "clean_all" do
+    test "fails when the given path is invalid" do
+      assert {:error, :invalid_path} = Phoenix.Digester.clean_all("nonexistent path")
+    end
+
+    test "no-op when the given path does not contain cache_manifest.json" do
+      output_path = "test/fixtures/digest/priv/static/css"
+      assert assets_files(output_path) == ["app.css"]
+      assert :ok = Phoenix.Digester.clean_all(output_path)
+      assert assets_files(output_path) == ["app.css"]
+    end
+
+    test "removes all compressed/compiled files including latest and manifest" do
+      manifest_path = "test/fixtures/digest/cleaner/cache_manifest.json"
+      File.mkdir_p!(@output_path)
+      File.cp(manifest_path, "#{@output_path}/cache_manifest.json")
+      File.touch("#{@output_path}/app.css")
+      File.touch("#{@output_path}/app.css.gz")
+      File.touch("#{@output_path}/app-1.css")
+      File.touch("#{@output_path}/app-1.css.gz")
+      File.touch("#{@output_path}/app-2.css")
+      File.touch("#{@output_path}/app-2.css.gz")
+      File.touch("#{@output_path}/app-3.css")
+      File.touch("#{@output_path}/app-3.css.gz")
+      File.touch("#{@output_path}/manifest.json")
+      File.touch("#{@output_path}/manifest.json.gz")
+      File.touch("#{@output_path}/app.css")
+
+      assert :ok = Phoenix.Digester.clean_all(@output_path)
+      output_files = assets_files(@output_path)
+
+      assert "app.css" in output_files
+      refute "app.css.gz" in output_files
+      refute "app-3.css" in output_files
+      refute "app-3.css.gz" in output_files
+      refute "app-2.css" in output_files
+      refute "app-2.css.gz" in output_files
+      refute "app-1.css" in output_files
+      refute "app-1.css.gz" in output_files
+      assert "manifest.json" in output_files
+      assert "manifest.json.gz" in output_files
+      refute "cache_manifest.json" in output_files
+    end
+  end
+
   defp assets_files(path) do
     path
     |> Path.join("**/*")