Skip to content

Commit

Permalink
feature: sfc.gen.init WIP
Browse files Browse the repository at this point in the history
factored out common code between sfc.gen.component/init
implemented injection
TODO: come up with a demo component and templatize

refs #2
  • Loading branch information
lastobelus committed Apr 20, 2021
1 parent 7fec251 commit d4cbb53
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 47 deletions.
49 changes: 49 additions & 0 deletions lib/mix/sfc_gen_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,53 @@ defmodule Mix.SfcGenLive do
def generator_paths do
[".", :sfc_gen_live, :phoenix]
end

# this
def put_context_app(opts, nil) do
Keyword.put(opts, :context_app, Mix.Phoenix.context_app())
end

def put_context_app(opts, string) do
Keyword.put(opts, :context_app, String.to_atom(string))
end

def valid_namespace?(name) when is_binary(name) do
name
|> split_name()
|> valid_namespace?()
end

def valid_namespace?(namespace_parts) when is_list(namespace_parts) do
Enum.all?(namespace_parts, &valid_module?/1)
end

def split_name(name) do
name
|> Phoenix.Naming.underscore()
|> String.split("/", trim: true)
end

def inflect(namespace_parts, name_parts) do
path = Enum.concat(namespace_parts, name_parts) |> Enum.join("/")
base = Module.concat([Mix.Phoenix.base()])
web_module = base |> Mix.Phoenix.web_module()
scoped = path |> Phoenix.Naming.camelize()
namespace_module = Module.concat(namespace_parts |> Enum.map(&Phoenix.Naming.camelize/1))
module = Module.concat(web_module, scoped)
alias = Module.concat([Module.split(module) |> List.last()])
human = Enum.map(name_parts, &Phoenix.Naming.humanize/1) |> Enum.join(" ")

[
alias: alias,
human: human,
web_module: web_module,
namespace_module: namespace_module,
module: module,
path: path
]
end

defp valid_module?(name) do
Phoenix.Naming.camelize(name) =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
end
end
53 changes: 7 additions & 46 deletions lib/mix/tasks/sfc.gen.component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Sfc.Gen.Component do

alias Mix.Surface.Component.{Props, Slots}

@switches [template: :boolean, namespace: :string, slot: [:string, :keep]]
@switches [template: :boolean, namespace: :string, slot: [:string, :keep], context_app: :string]
@default_opts [template: false, namespace: "components"]
@doc false
def run(args) do
Expand All @@ -19,7 +19,7 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
slots = slots |> Slots.parse()

assigns =
inflect(namespace_parts, name_parts)
Mix.SfcGenLive.inflect(namespace_parts, name_parts)
|> Keyword.put(:props, props)
|> Keyword.put(:template, opts[:template])
|> Keyword.put(:slots, slots)
Expand Down Expand Up @@ -48,15 +48,14 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
# def build_component(args) do
# end

@spec validate_args!(String.t(), String.t()) :: {[...], [...]}
defp validate_args!(name, namespace) do
{namespace_parts, name_parts} = normalized_component_name(name, namespace)

cond do
not Enum.all?(name_parts, &valid_module?/1) ->
not SfcGenLive.valid_namespace?(name_parts) ->
raise_with_help("Expected the component, #{inspect(name)}, to be a valid module name")

not Enum.all?(namespace_parts, &valid_module?/1) ->
not SfcGenLive.valid_namespace?(namespace_parts) ->
raise_with_help(
"Expected the namespace, #{inspect(namespace)}, to be a valid module name"
)
Expand Down Expand Up @@ -134,30 +133,16 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
merged_opts =
@default_opts
|> Keyword.merge(opts)
|> put_context_app(opts[:context_app])
|> Mix.SfcGenLive.put_context_app(opts[:context_app])

[name | props] = parsed
{merged_opts, slots, name, props}
end

defp put_context_app(opts, nil) do
Keyword.put(opts, :context_app, Mix.Phoenix.otp_app())
end

defp put_context_app(opts, string) do
Keyword.put(opts, :context_app, String.to_atom(string))
end

defp split_name(name) do
name
|> Phoenix.Naming.underscore()
|> String.split("/", trim: true)
end

@spec normalized_component_name(String.t(), String.t()) :: {[String.t()], [String.t()]}
defp normalized_component_name(name, namespace) do
namespace_parts = split_name(namespace)
name_parts = split_name(name) |> strip_namespace(namespace_parts)
namespace_parts = Mix.SfcGenLive.split_name(namespace)
name_parts = Mix.SfcGenLive.split_name(name) |> strip_namespace(namespace_parts)
{namespace_parts, name_parts}
end

Expand All @@ -170,28 +155,4 @@ defmodule Mix.Tasks.Sfc.Gen.Component do
name_parts
end
end

defp valid_module?(name) do
Phoenix.Naming.camelize(name) =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
end

defp inflect(namespace_parts, name_parts) do
path = Enum.concat(namespace_parts, name_parts) |> Enum.join("/")
base = Module.concat([Mix.Phoenix.base()])
web_module = base |> Mix.Phoenix.web_module()
scoped = path |> Phoenix.Naming.camelize()
namespace_module = Module.concat(namespace_parts |> Enum.map(&Phoenix.Naming.camelize/1))
module = Module.concat(web_module, scoped)
alias = Module.concat([Module.split(module) |> List.last()])
human = Enum.map(name_parts, &Phoenix.Naming.humanize/1) |> Enum.join(" ")

[
alias: alias,
human: human,
web_module: web_module,
namespace_module: namespace_module,
module: module,
path: path
]
end
end
125 changes: 125 additions & 0 deletions lib/mix/tasks/sfc.gen.init.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
defmodule Mix.Tasks.Sfc.Gen.Init do
@moduledoc """
Generates a Surface component.
"""
use Mix.Task

@switches [template: :boolean, namespace: :string, demo: :boolean, context_app: :string]
@default_opts [template: true, namespace: "components", demo: true]
@aliases [t: :template, n: :namespace, d: :demo]
@doc false
def run(args) do
opts = parse_opts(args)

namespace_parts = validate_namespace!(opts[:namespace])

assigns = Mix.SfcGenLive.inflect(namespace_parts, "counter")

maybe_include_demo(opts, assigns)
end

defp parse_opts(args) do
{opts, _parsed} =
OptionParser.parse!(args,
strict: @switches,
aliases: @aliases
)

merged_opts =
@default_opts
|> Keyword.merge(opts)
|> Mix.SfcGenLive.put_context_app(opts[:context_app])

merged_opts
end

defp validate_namespace!(namespace) do
cond do
not Mix.SfcGenLive.valid_namespace?(namespace) ->
raise_with_help(
"Expected the namespace, #{inspect(namespace)}, to be a valid module name"
)

true ->
namespace
end
end

@spec raise_with_help(String.t()) :: no_return()
defp raise_with_help(msg) do
Mix.raise("""
#{msg}
mix sfc.gen.init takes
- a `--demo` option, default true, that controls whether
a demo component will be generated in the app.
- a `--template` boolean option, default true, which specifies whether the
demo component template will be in a `.sface` file or in a `~H` sigil in
the component module
- an optional `--namespace` option that is a relative path
in `lib/my_app_web` where the demo component will be created. The default
value is `components`. The `--namespace` option is ignored if
`--demo false` is passed.
For example:
mix sfc.gen.init --namespace my_components
will create `lib/my_app_web/my_components/counter.ex` and `lib/my_app_web/my_components/counter.sface`
""")
end

defp maybe_include_demo(opts, assigns) do
if opts[:demo] do
web_dir = Mix.Phoenix.web_path(opts[:context_app])
paths = Mix.SfcGenLive.generator_paths()

files = [
{:eex, "demo.ex", Path.join(web_dir, "#{assigns[:path]}.ex")}
]

template_files = [
{:eex, "demo.sface", Path.join(web_dir, "#{assigns[:path]}.sface")}
]

Mix.Phoenix.copy_from(paths, "priv/templates/sfc.gen.init", assigns, files)

if opts[:template] do
Mix.Phoenix.copy_from(paths, "priv/templates/sfc.gen.init", assigns, template_files)
end
end
end

def inject_in_formatter_exs do
file_path = ".formatter.exs"
file = File.read!(file_path)

unless Regex.match?(~r/import_deps:[^]]+:surface/, file) do
Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)])
String.replace(file, ~r/(import_deps:\s*\[[^]]+)\]/, "\\1, :surface]")
File.write!(file_path, file)
end
end

def inject_live_reload_config(web_dir) do
file_path = "config/dev.exs"
file = File.read!(file_path)

unless Regex.match?(~r/live_reload:[^]]+\(sface\)/s, file) do
Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)])

file =
String.replace(
file,
~r/(live_reload: .*\n)( *~r)([^]]+")(\s*)\]/s,
"\\1\\2\\3,\n\\2\"#{web_dir}/live/.*(sface)$\"\\4]"
)

File.write!(file_path, file)
end
end
end
21 changes: 21 additions & 0 deletions priv/templates/sfc.gen.init/demo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule <%= inspect module %> do
@moduledoc """
Document <%= inspect module %> here.
"""
use Surface.Component
<%= for {name, prop} <- props do %>
@doc "property <%= name %>"
prop <%= name %>, <%= inspect prop.type %><%= Enum.join(prop.opts, ", ") %>
<% end %><%= for slot <- slots do %>
@doc "property <%= slot.name %>"
slot <%= slot.name %><%= Enum.join(slot.opts, ", ") %>
<% end %><%= unless template do %>

def render(assigns) do
~H"""
<!-- <%= human %> --><%= for slot <- slots do %>
<slot<%= slot.attr_name %><%= slot.attr_props %>/><% end %>
"""
end
<% end %>end
Empty file.
12 changes: 12 additions & 0 deletions test/mix/tasks/sfc.gen.init_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Code.require_file("../../mix_helper.exs", __DIR__)

defmodule Mix.Tasks.Sfc.Gen.InitTest do
use ExUnit.Case
import MixHelper
alias Mix.Tasks.Sfc.Gen

setup do
Mix.Task.clear()
:ok
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Code.require_file("../../mix_helper.exs", __DIR__)

defmodule Mix.Tasks.Phx.Gen.LiveTest do
defmodule Mix.Tasks.Sfc.Gen.LiveTest do
use ExUnit.Case
import MixHelper
alias Mix.Tasks.Sfc.Gen
Expand Down
2 changes: 2 additions & 0 deletions todos/main.todo
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ sfc.gen.live:
`git reset --hard HEAD; git clean -fd; mix deps.get; mix deps.compile; mix ecto.reset; MIX_ENV=test mix ecto.reset`
☐ write blog post showing & explaining the generated pieces

sfc.gen.init:

phx.gen.live:
☐ report bug with `mix phx.gen.live Blog Post posts title alarm:time`
☐ check if mix sfc.gen.live Blog Post posts title tags:array:text results in failing test OOB
Expand Down

0 comments on commit d4cbb53

Please sign in to comment.