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