Repository: woylie/doggo
Pull request #321 (2024-07-23 02:50:48)

allow writing only a single component story


Patch:
diff --git a/README.md b/README.md
index df5d9e3..23c7b06 100644
--- a/README.md
+++ b/README.md
@@ -115,7 +115,7 @@ generated components. After you followed the installation instructions of
 Phoenix Storybook, you can run a mix task to generate the stories:
 
 ```bash
-mix dog.gen.stories -m MyAppWeb.CoreComponents -o storybook
+mix dog.gen.stories -m MyAppWeb.CoreComponents -o storybook --all
 ```
 
 Here, `MyAppWeb.CoreComponents` is the module in which you added
@@ -124,6 +124,18 @@ Here, `MyAppWeb.CoreComponents` is the module in which you added
 The task will only generate story modules for the components that you
 configured. The stories will include variations for all configured modifiers.
 
+You don't need to update the stories after changing the modifiers of a
+component. However, you'll need to run the task again after adding new
+components to your module, or potentially after a new Doggo version was
+released.
+
+The task will ask for confirmation to overwrite existing stories. To only
+write the story for a single component, you can run:
+
+```bash
+mix dog.gen.stories -m MyAppWeb.CoreComponents -o storybook -c button
+```
+
 ### PurgeCSS
 
 If you use PurgeCSS, you will need to add `deps/doggo/lib/doggo.ex` to your
diff --git a/lib/mix/tasks/dog.gen.stories.ex b/lib/mix/tasks/dog.gen.stories.ex
index 0208f12..cb89c14 100644
--- a/lib/mix/tasks/dog.gen.stories.ex
+++ b/lib/mix/tasks/dog.gen.stories.ex
@@ -6,20 +6,34 @@ defmodule Mix.Tasks.Dog.Gen.Stories do
 
   ## Usage
 
-      mix dog.gen.stories -m MyAppWeb.CoreComponents -o storybook
+  Write stories for all configured components:
+
+      mix dog.gen.stories -m MyAppWeb.CoreComponents -o storybook -a
+
+  Write the story for a single component:
+
+      mix dog.gen.stories -m MyAppWeb.CoreComponents -o storybook -c button
 
   ## Options
 
   - -m, --module: The module where the Doggo components are compiled.
   - -o, --output: The Storybook folder.
+  - -c, --component: The name of the component.
+  - -a, --all: Write stories for all configured components.
   - -f, --force: Force folder creation and overwrite existing stories.
   """
 
   use Mix.Task
 
   @switches [
-    aliases: [f: :force, m: :module, o: :output],
-    strict: [force: :boolean, module: :string, output: :string]
+    aliases: [a: :all, c: :component, f: :force, m: :module, o: :output],
+    strict: [
+      all: :boolean,
+      component: :string,
+      force: :boolean,
+      module: :string,
+      output: :string
+    ]
   ]
 
   @requirements ["app.config"]
@@ -30,19 +44,55 @@ defmodule Mix.Tasks.Dog.Gen.Stories do
 
     with {:ok, module} <- Keyword.fetch(opts, :module),
          {:ok, path} <- Keyword.fetch(opts, :output) do
+      component = component_option(opts)
       force = Keyword.get(opts, :force, false)
       ensure_folder_exists(path, force: force)
       module = Module.safe_concat([module])
+      components = Enum.to_list(module.__dog_components__())
 
-      module.__dog_components__()
-      |> Enum.to_list()
-      |> write_stories(module, path, force: force)
+      if component == :all do
+        write_stories(components, module, path, force: force)
+      else
+        component = find_component(components, component)
+        write_stories([component], module, path, force: force)
+      end
     else
       _ ->
         IO.puts(@moduledoc)
     end
   end
 
+  defp component_option(opts) do
+    all = Keyword.get(opts, :all, false)
+    component = Keyword.get(opts, :component)
+
+    if all && component do
+      IO.puts("Error: expected either --all or --component, got both.")
+      exit({:shutdown, 1})
+    end
+
+    if !all && is_nil(component) do
+      IO.puts("Error: expected either --all or --component, got none.")
+      exit({:shutdown, 1})
+    end
+
+    if all, do: :all, else: component
+  end
+
+  defp find_component(components, component) when is_binary(component) do
+    result =
+      Enum.find(components, fn {name, _info} ->
+        Atom.to_string(name) == component
+      end)
+
+    if result do
+      result
+    else
+      IO.puts("Error: Component '#{component}' not found.")
+      exit({:shutdown, 1})
+    end
+  end
+
   defp ensure_folder_exists(path, opts) do
     if File.exists?(path) do
       if File.dir?(path) do


Test patch:
diff --git a/test/mix/tasks/dog.gen.stories_test.exs b/test/mix/tasks/dog.gen.stories_test.exs
index 1e27aac..69564eb 100644
--- a/test/mix/tasks/dog.gen.stories_test.exs
+++ b/test/mix/tasks/dog.gen.stories_test.exs
@@ -18,13 +18,14 @@ defmodule Mix.Tasks.Dog.GenStoriesTest do
   end
 
   @tag :tmp_dir
-  test "saves modifiers to file", %{tmp_dir: tmp_dir} do
+  test "writes stories for all configured components", %{tmp_dir: tmp_dir} do
     assert capture_io(fn ->
              Stories.run([
                "--module",
                "Mix.Tasks.Dog.GenStoriesTest.TestComponents",
                "-o",
-               tmp_dir
+               tmp_dir,
+               "--all"
              ])
            end)
 
@@ -45,4 +46,30 @@ defmodule Mix.Tasks.Dog.GenStoriesTest do
                Doggo.Storybook.story_template(TestComponents, :context_menu)
              )
   end
+
+  @tag :tmp_dir
+  test "writes stories for single component", %{tmp_dir: tmp_dir} do
+    assert capture_io(fn ->
+             Stories.run([
+               "--module",
+               "Mix.Tasks.Dog.GenStoriesTest.TestComponents",
+               "-o",
+               tmp_dir,
+               "--component",
+               "button"
+             ])
+           end)
+
+    button_path = Path.join([tmp_dir, "buttons", "button.story.exs"])
+    menu_path = Path.join([tmp_dir, "menu", "context_menu.story.exs"])
+
+    assert File.exists?(button_path)
+
+    {formatter, _} = Mix.Tasks.Format.formatter_for_file(button_path)
+
+    assert File.read!(button_path) ==
+             formatter.(Doggo.Storybook.story_template(TestComponents, :button))
+
+    refute File.exists?(menu_path)
+  end
 end