From 86cf4ea8e937dcee17ef876b6924d0383762fc74 Mon Sep 17 00:00:00 2001 From: Mayel de Borniol Date: Wed, 5 Feb 2025 09:44:41 +0000 Subject: [PATCH] https://github.com/bonfire-networks/bonfire-app/issues/707 --- lib/mix_tasks/extension/extension.new.ex | 69 ----------- lib/mix_tasks/extension/widget.new.ex | 84 -------------- lib/mix_tasks/generators/gen.component.ex | 57 +++++++++ lib/mix_tasks/generators/gen.extension.ex | 108 ++++++++++++++++++ lib/mix_tasks/generators/gen.extension_ui.ex | 38 ++++++ lib/mix_tasks/generators/gen.routes_module.ex | 73 ++++++++++++ lib/mix_tasks/generators/gen.ui.ex | 52 +++++++++ lib/mix_tasks/generators/gen.view.ex | 73 ++++++++++++ lib/mix_tasks/generators/gen.widget.ex | 45 ++++++++ lib/mix_tasks/helpers.ex | 79 +++++++++++++ 10 files changed, 525 insertions(+), 153 deletions(-) delete mode 100644 lib/mix_tasks/extension/extension.new.ex delete mode 100644 lib/mix_tasks/extension/widget.new.ex create mode 100644 lib/mix_tasks/generators/gen.component.ex create mode 100644 lib/mix_tasks/generators/gen.extension.ex create mode 100644 lib/mix_tasks/generators/gen.extension_ui.ex create mode 100644 lib/mix_tasks/generators/gen.routes_module.ex create mode 100644 lib/mix_tasks/generators/gen.ui.ex create mode 100644 lib/mix_tasks/generators/gen.view.ex create mode 100644 lib/mix_tasks/generators/gen.widget.ex create mode 100644 lib/mix_tasks/helpers.ex diff --git a/lib/mix_tasks/extension/extension.new.ex b/lib/mix_tasks/extension/extension.new.ex deleted file mode 100644 index 4fe24b0..0000000 --- a/lib/mix_tasks/extension/extension.new.ex +++ /dev/null @@ -1,69 +0,0 @@ -defmodule Mix.Tasks.Bonfire.Extension.New do - use Mix.Task - - def run([extension_name]) do - snake_name = Macro.underscore(extension_name) - - camel_name = - extension_name - |> String.replace("bonfire_", "bonfire/") - |> Macro.camelize() - - if File.exists?("extensions/bonfire_extension_template") do - File.cp_r!("extensions/bonfire_extension_template", "extensions/#{snake_name}") - else - System.cmd( - "git", - [ - "clone", - "https://github.com/bonfire-networks/bonfire_extension_template.git", - snake_name - ], - cd: "extensions" - ) - end - - rename_modules(snake_name, camel_name) - rename_config_file(snake_name) - reset_git(snake_name) - - IO.puts("Done! You can now start developing your extension in ./extensions/#{snake_name}/") - end - - defp rename_modules(snake_name, camel_name) do - # Get all .ex, .exs, and .md files in the extension directory - ["**/*.ex", "**/*.exs", "**/*.md", "**/*.sface"] - |> Enum.flat_map(&Path.wildcard("extensions/#{snake_name}/" <> &1)) - |> Enum.each(fn path -> - # Read the file - file_content = File.read!(path) - - # Replace the module names - new_content = - String.replace(file_content, "bonfire_extension_template", snake_name) - - new_content = - String.replace( - new_content, - "Bonfire.ExtensionTemplate", - camel_name - ) - - # Write the new content to the file - File.write!(path, new_content) - end) - end - - defp rename_config_file(extension_name) do - old_name = "extensions/#{extension_name}/config/bonfire_extension_template.exs" - new_name = "extensions/#{extension_name}/config/#{extension_name}.exs" - File.rename(old_name, new_name) - end - - defp reset_git(extension_name) do - System.cmd("rm", ["-rf", ".git"], cd: "extensions/#{extension_name}") - System.cmd("git", ["init"], cd: "extensions/#{extension_name}") - System.cmd("git", ["add", "."], cd: "extensions/#{extension_name}") - System.cmd("git", ["commit", "-m", "new extension"], cd: "extensions/#{extension_name}") - end -end diff --git a/lib/mix_tasks/extension/widget.new.ex b/lib/mix_tasks/extension/widget.new.ex deleted file mode 100644 index 5f6753a..0000000 --- a/lib/mix_tasks/extension/widget.new.ex +++ /dev/null @@ -1,84 +0,0 @@ -defmodule Mix.Tasks.Bonfire.Widget.New do - @moduledoc """ - `just mix bonfire.widget.new Bonfire.MyUIExtension.MyWidget` - - will present you with a diff and create new files - """ - import Bonfire.Common.Extend - use_if_enabled(Igniter.Mix.Task) - - def igniter(igniter, [module_name | _] = _argv) do - # app_name = Bonfire.Application.name() - - module_name = - String.trim_trailing(module_name, "Live") - |> Kernel.<>("Live") - |> Igniter.Project.Module.parse() - - # |> IO.inspect() - - path_prefix = "lib/web/widgets" - - igniter - |> Igniter.create_new_file(ext_path_for_module(module_name, path_prefix), """ - defmodule #{inspect(module_name)} do - use Bonfire.UI.Common.Web, :stateless_component - - prop widget_title, :string, default: nil - prop class, :css_class, default: nil - - # to add extra props or slots, see https://surface-ui.org/properties and https://surface-ui.org/slots - end - """) - |> Igniter.create_new_file(ext_path_for_module(module_name, path_prefix, "sface"), """ - - Hello world! - - """) - end - - def ext_path_for_module( - module_name, - kind_or_prefix \\ "lib", - file_ext \\ nil, - path_prefix \\ "extensions" - ) do - path = - case module_name - |> Module.split() - |> IO.inspect() do - ["Bonfire", ext | rest] -> ["Bonfire#{ext}"] ++ rest - other -> other - end - |> Enum.map(&Macro.underscore/1) - - first = List.first(path) - last = List.last(path) - leading = path |> Enum.drop(1) |> Enum.drop(-1) - - first_prefix = [path_prefix, first] - - case kind_or_prefix do - :test -> - if String.ends_with?(last, "_test") do - Path.join(first_prefix ++ ["test" | leading] ++ ["#{last}.#{file_ext || "exs"}"]) - else - Path.join(first_prefix ++ ["test" | leading] ++ ["#{last}_test.#{file_ext || "exs"}"]) - end - - "test/support" -> - case leading do - [] -> - Path.join(first_prefix ++ ["test/support", "#{last}.#{file_ext || "ex"}"]) - - [_prefix | leading_rest] -> - Path.join( - first_prefix ++ ["test/support" | leading_rest] ++ ["#{last}.#{file_ext || "ex"}"] - ) - end - - source_folder -> - Path.join(first_prefix ++ [source_folder | leading] ++ ["#{last}.#{file_ext || "ex"}"]) - end - end -end diff --git a/lib/mix_tasks/generators/gen.component.ex b/lib/mix_tasks/generators/gen.component.ex new file mode 100644 index 0000000..6f8ed6b --- /dev/null +++ b/lib/mix_tasks/generators/gen.component.ex @@ -0,0 +1,57 @@ +defmodule Mix.Tasks.Bonfire.Gen.Component do + @moduledoc """ + `just mix bonfire.gen.component stateless Bonfire.MyUIExtension MyComponent` + or + `just mix bonfire.gen.component stateful Bonfire.MyUIExtension MyComponent` + + will present you with a diff and create new files + """ + use Igniter.Mix.Task + alias Bonfire.Common.Mix.Tasks.Helpers + + def igniter(igniter, [state, extension, module_name | _] = _argv) do + gen_component(igniter, extension, module_name, state) + end + + def gen_component(igniter, extension, module_name, state) + when state in ["stateful", "stateless"] do + ext_module = + extension + |> Macro.camelize() + + snake_name = Macro.underscore(extension) + + module_name = + String.trim_trailing(ext_module <> "." <> module_name, "Live") + |> Kernel.<>("Live") + |> Igniter.Project.Module.parse() + + # |> IO.inspect() + + lib_path_prefix = "lib/web/components" + + igniter + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix), + """ + defmodule #{inspect(module_name)} do + use Bonfire.UI.Common.Web, :#{state}_component + + prop name, :string, default: nil + end + """ + ) + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix, "sface") + |> IO.inspect(), + """ +
+ Hello, This is a new #{state} component for #{ext_module}. + + You can include a other components by uncommenting the line below and updating it with your other component module name and then passing the assigns you need: + {!-- <#{ext_module}.SimpleComponentLive name="#{ext_module}" /> --} +
+ """ + ) + end +end diff --git a/lib/mix_tasks/generators/gen.extension.ex b/lib/mix_tasks/generators/gen.extension.ex new file mode 100644 index 0000000..c096ea1 --- /dev/null +++ b/lib/mix_tasks/generators/gen.extension.ex @@ -0,0 +1,108 @@ +defmodule Mix.Tasks.Bonfire.Gen.Extension do + @moduledoc """ + `just mix bonfire.gen.extension Bonfire.MyExtension` + + will present you with a diff of new files to create your new extension and create a repo for it in `extensions/` + """ + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + # description: "Creates a new Bonfire extension from a template", + positional: [ + extension_name: [ + type: :string, + required: true, + doc: "Name of the extension to create" + ] + ] + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + [extension_name] = igniter.args.argv + snake_name = Macro.underscore(extension_name) + + camel_name = + extension_name + |> String.replace("bonfire_", "bonfire/") + |> Macro.camelize() + + igniter + |> clone_template(snake_name) + |> rename_modules(snake_name, camel_name) + |> rename_config_file(snake_name) + |> reset_git(snake_name) + |> Igniter.add_notice( + "Done! You can now start developing your extension in ./extensions/#{snake_name}/" + ) + end + + defp clone_template(igniter, snake_name) do + if File.exists?("extensions/bonfire_extension_template") do + System.cmd("sh", [ + "-c", + "cd extensions/bonfire_extension_template && find . -name '.git' -prune -o -print | cpio -pdm ../#{snake_name}" + ]) + else + System.cmd( + "git", + [ + "clone", + "--depth", + "1", + "https://github.com/bonfire-networks/bonfire_extension_template.git", + snake_name + ], + cd: "extensions" + ) + end + + igniter + end + + defp rename_modules(igniter, snake_name, camel_name) do + patterns = ["**/*.ex", "**/*.exs", "**/*.md", "**/*.sface"] + base_path = "extensions/#{snake_name}/" + + Enum.reduce(patterns, igniter, fn pattern, acc -> + Path.wildcard(base_path <> pattern) + |> Enum.reduce(acc, fn path, inner_acc -> + inner_acc + |> Igniter.include_existing_file(path) + |> Igniter.update_file(path, fn source -> + Rewrite.Source.update(source, :content, fn + content when is_binary(content) -> + content + |> String.replace("bonfire_extension_template", snake_name) + |> String.replace("Bonfire.ExtensionTemplate", camel_name) + + content -> + content + end) + end) + end) + end) + end + + defp rename_config_file(igniter, extension_name) do + old_name = "extensions/#{extension_name}/config/bonfire_extension_template.exs" + new_name = "extensions/#{extension_name}/config/#{extension_name}.exs" + + Igniter.move_file(igniter, old_name, new_name) + end + + defp reset_git(igniter, extension_name) do + cd_path = "extensions/#{extension_name}" + + System.cmd("rm", ["-rf", ".git"], cd: cd_path) + System.cmd("git", ["init"], cd: cd_path) + System.cmd("git", ["add", "."], cd: cd_path) + System.cmd("git", ["commit", "-m", "new extension"], cd: cd_path) + + igniter + end +end diff --git a/lib/mix_tasks/generators/gen.extension_ui.ex b/lib/mix_tasks/generators/gen.extension_ui.ex new file mode 100644 index 0000000..e923ba4 --- /dev/null +++ b/lib/mix_tasks/generators/gen.extension_ui.ex @@ -0,0 +1,38 @@ +defmodule Mix.Tasks.Bonfire.Gen.Extension.Ui do + @moduledoc """ + `just mix bonfire.gen.extension.ui Bonfire.MyExtension` + + will present you with a diff of new files to create your new extension and create a repo for it in `extensions/` + """ + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + # description: "Creates a new Bonfire extension from a template", + positional: [ + extension_name: [ + type: :string, + required: true, + doc: "Name of the UI extension to create" + ] + ] + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + [extension_name] = igniter.args.argv + # snake_name = Macro.underscore(extension_name) + + camel_name = + extension_name + |> String.replace("bonfire_", "bonfire/") + |> Macro.camelize() + + igniter + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.Extension, [camel_name]) + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.Ui, [camel_name]) + end +end diff --git a/lib/mix_tasks/generators/gen.routes_module.ex b/lib/mix_tasks/generators/gen.routes_module.ex new file mode 100644 index 0000000..45138d6 --- /dev/null +++ b/lib/mix_tasks/generators/gen.routes_module.ex @@ -0,0 +1,73 @@ +defmodule Mix.Tasks.Bonfire.Gen.RoutesModule do + @moduledoc """ + `just mix bonfire.gen.routes_module Bonfire.MyUIExtension` + + will present you with a diff and create new file(s) + """ + use Igniter.Mix.Task + alias Bonfire.Common.Mix.Tasks.Helpers + + def igniter(igniter, [module_name | _] = _argv) do + # app_name = Bonfire.Application.name() + + snake_name = Macro.underscore(module_name) + + ext_module = + module_name + |> Macro.camelize() + + module_name = + ext_module + |> Kernel.<>(".Web.Routes") + |> Igniter.Project.Module.parse() + + # |> IO.inspect() + + lib_path_prefix = "lib/web" + + igniter + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix), + """ + defmodule #{inspect(module_name)} do + @behaviour Bonfire.UI.Common.RoutesModule + + defmacro __using__(_) do + quote do + # pages anyone can view + scope "/#{snake_name}/", #{ext_module} do + pipe_through(:browser) + + live("/", HomeLive) + end + + # pages only guests can view + scope "/#{snake_name}/", #{ext_module} do + pipe_through(:browser) + pipe_through(:guest_only) + end + + # pages you need an account to view + scope "/#{snake_name}/", #{ext_module} do + pipe_through(:browser) + pipe_through(:account_required) + end + + # pages you need to view as a user + scope "/#{snake_name}/", #{ext_module} do + pipe_through(:browser) + pipe_through(:user_required) + end + + # pages only admins can view + scope "/#{snake_name}/admin", #{ext_module} do + pipe_through(:browser) + pipe_through(:admin_required) + end + end + end + end + """ + ) + end +end diff --git a/lib/mix_tasks/generators/gen.ui.ex b/lib/mix_tasks/generators/gen.ui.ex new file mode 100644 index 0000000..f8b21c0 --- /dev/null +++ b/lib/mix_tasks/generators/gen.ui.ex @@ -0,0 +1,52 @@ +defmodule Mix.Tasks.Bonfire.Gen.Ui do + @moduledoc """ + `just mix bonfire.gen.ui Bonfire.MyExtension` + + will present you with a diff of new files to create your new extension and create a repo for it in `extensions/` + """ + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + # description: "Creates a new Bonfire extension from a template", + positional: [ + extension_name: [ + type: :string, + required: true, + doc: "Name of the extension" + ] + ] + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + [extension_name] = igniter.args.argv + # snake_name = Macro.underscore(extension_name) + + camel_name = + extension_name + |> String.replace("bonfire_", "bonfire/") + |> Macro.camelize() + + igniter + # TODO: include first component in this one + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.Component, [ + "stateless", + camel_name, + "SimpleComponent" + ]) + # TODO: include first component in this one + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.Component, [ + "stateful", + camel_name, + "AdvancedComponent" + ]) + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.Widget, [camel_name, "MyWidget"]) + # TODO: include component in view and widget in sidebar + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.View, [camel_name, "Home"]) + |> Igniter.compose_task(Mix.Tasks.Bonfire.Gen.RoutesModule, [camel_name]) + end +end diff --git a/lib/mix_tasks/generators/gen.view.ex b/lib/mix_tasks/generators/gen.view.ex new file mode 100644 index 0000000..450eb0f --- /dev/null +++ b/lib/mix_tasks/generators/gen.view.ex @@ -0,0 +1,73 @@ +defmodule Mix.Tasks.Bonfire.Gen.View do + @moduledoc """ + `just mix bonfire.gen.view Bonfire.MyUIExtension MyView` + + will present you with a diff and create new files + """ + use Igniter.Mix.Task + alias Bonfire.Common.Mix.Tasks.Helpers + + def igniter(igniter, [extension, module_name | _] = _argv) do + # app_name = Bonfire.Application.name() + + ext_module = + extension + |> Macro.camelize() + + snake_name = Macro.underscore(extension) + + module_name = + String.trim_trailing(ext_module <> "." <> module_name, "Live") + |> Kernel.<>("Live") + |> Igniter.Project.Module.parse() + + # |> IO.inspect() + + lib_path_prefix = "lib/web/views" + + igniter + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix), + """ + defmodule #{inspect(module_name)} do + use Bonfire.UI.Common.Web, :surface_live_view + + declare_nav_link(l("#{ext_module} Home"), page: "#{snake_name}", icon: "ri:home-line", emoji: "🧩") + + on_mount {LivePlugs, [Bonfire.UI.Me.LivePlugs.LoadCurrentUser]} + + def mount(_params, _session, socket) do + {:ok, + assign( + socket, + page: "#{snake_name}", + page_title: "#{ext_module}" + )} + end + + def handle_event( + "custom_event", + _attrs, + socket + ) do + # handle the event here + {:noreply, socket} + end + end + + """ + ) + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix, "sface") + |> IO.inspect(), + """ +
+ Hello, This is a new view for #{ext_module}. + + You can include a component by uncommenting the line below and updating it with your component module name and then passing the assigns you need: + {!-- <#{ext_module}.AdvancedComponentLive name="#{ext_module}" /> --} +
+ """ + ) + end +end diff --git a/lib/mix_tasks/generators/gen.widget.ex b/lib/mix_tasks/generators/gen.widget.ex new file mode 100644 index 0000000..5acf764 --- /dev/null +++ b/lib/mix_tasks/generators/gen.widget.ex @@ -0,0 +1,45 @@ +defmodule Mix.Tasks.Bonfire.Gen.Widget do + @moduledoc """ + `just mix bonfire.gen.widget Bonfire.MyUIExtension MyWidget` + + will present you with a diff and create new files + """ + use Igniter.Mix.Task + alias Bonfire.Common.Mix.Tasks.Helpers + + def igniter(igniter, [extension, module_name | _] = _argv) do + # app_name = Bonfire.Application.name() + + module_name = + (Macro.camelize(extension) <> "." <> String.trim_trailing(module_name, "Live")) + |> Kernel.<>("Live") + |> Igniter.Project.Module.parse() + + # |> IO.inspect() + + lib_path_prefix = "lib/web/widgets" + + igniter + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix), + """ + defmodule #{inspect(module_name)} do + use Bonfire.UI.Common.Web, :stateless_component + + prop widget_title, :string, default: nil + prop class, :css_class, default: nil + + # to add extra props or slots, see https://surface-ui.org/properties and https://surface-ui.org/slots + end + """ + ) + |> Igniter.create_new_file( + Helpers.igniter_path_for_module(igniter, module_name, lib_path_prefix, "sface"), + """ + + Hello world! + + """ + ) + end +end diff --git a/lib/mix_tasks/helpers.ex b/lib/mix_tasks/helpers.ex new file mode 100644 index 0000000..fa62bfd --- /dev/null +++ b/lib/mix_tasks/helpers.ex @@ -0,0 +1,79 @@ +defmodule Bonfire.Common.Mix.Tasks.Helpers do + def igniter_path_for_module( + igniter, + module_name, + kind_or_prefix \\ "lib", + file_ext \\ nil, + ext_prefix \\ "extensions" + ) do + ext_path_for_module( + module_name, + kind_or_prefix, + file_ext, + ext_prefix, + igniter + ) + end + + def ext_path_for_module( + module_name, + kind_or_prefix \\ "lib", + file_ext \\ nil, + ext_prefix \\ "extensions", + igniter \\ nil + ) do + path = + case module_name + |> Module.split() do + ["Bonfire", ext | rest] -> ["Bonfire#{ext}"] ++ rest + other -> other + end + |> Enum.map(&Macro.underscore/1) + + first = List.first(path) + last = List.last(path) + leading = path |> Enum.drop(1) |> Enum.drop(-1) + path_prefixes = [ext_prefix, first] + + case kind_or_prefix do + :test -> + file_ext = file_ext || "exs" + + # TODO: does Igniter proper_location support this? + if String.ends_with?(last, "_test") do + ["test" | leading] ++ ["#{last}.#{file_ext}"] + else + ["test" | leading] ++ ["#{last}_test.#{file_ext}"] + end + + "test/support" -> + file_ext = file_ext || "ex" + + if file_ext == "ex" and igniter do + Igniter.Project.Module.proper_location(igniter, module_name, :test_support) + else + case leading do + [] -> + ["test/support", "#{last}.#{file_ext}"] + + [_prefix | leading_rest] -> + ["test/support" | leading_rest] ++ ["#{last}.#{file_ext}"] + end + end + + source_folder -> + file_ext = file_ext || "ex" + + if file_ext == "ex" and kind_or_prefix == "lib" and igniter do + Igniter.Project.Module.proper_location(igniter, module_name) + else + [source_folder | leading] ++ ["#{last}.#{file_ext}"] + end + end + |> join_prefixes(path_prefixes) + end + + defp join_prefixes(paths, path_prefixes) do + Path.join(path_prefixes ++ List.wrap(paths)) + end +end