diff --git a/lib/live_view_native/component.ex b/lib/live_view_native/component.ex index 535015f..baacf81 100644 --- a/lib/live_view_native/component.ex +++ b/lib/live_view_native/component.ex @@ -121,54 +121,69 @@ defmodule LiveViewNative.Component do declarative_opts = Keyword.drop(opts, [:as, :format, :root]) - case LiveViewNative.fetch_plugin(format) do + component_ast = quote do + import Phoenix.LiveView.Helpers + import Kernel, except: [def: 2, defp: 2] + import Phoenix.Component, except: [ + embed_templates: 1, embed_templates: 2, + sigitl_H: 2, + + async_result: 1, + dynamic_tag: 1, + focus_wrap: 1, + form: 1, + inputs_for: 1, + intersperse: 1, + link: 1, + live_component: 1, + live_file_input: 1, + live_img_preview: 1, + live_title: 1 + ] + import Phoenix.Component.Declarative + require Phoenix.Template + + for {prefix_match, value} <- Phoenix.Component.Declarative.__setup__(__MODULE__, unquote(declarative_opts)) do + @doc false + def __global__?(prefix_match), do: value + end + + import LiveViewNative.Renderer, only: [ + delegate_to_target: 1, + delegate_to_target: 2, + embed_templates: 1, + embed_templates: 2 + ] + + def __native_opts__, do: @native_opts + end + + plugin_component_ast = case LiveViewNative.fetch_plugin(format) do {:ok, plugin} -> quote do - import Phoenix.LiveView.Helpers - import Kernel, except: [def: 2, defp: 2] - import Phoenix.Component, except: [ - embed_templates: 1, embed_templates: 2, - sigitl_H: 2, - - async_result: 1, - dynamic_tag: 1, - focus_wrap: 1, - form: 1, - inputs_for: 1, - intersperse: 1, - link: 1, - live_component: 1, - live_file_input: 1, - live_img_preview: 1, - live_title: 1 - ] - import Phoenix.Component.Declarative - require Phoenix.Template - - for {prefix_match, value} <- Phoenix.Component.Declarative.__setup__(__MODULE__, unquote(declarative_opts)) do - @doc false - def __global__?(prefix_match), do: value - end - use unquote(plugin.component) - import LiveViewNative.Renderer, only: [ - delegate_to_target: 1, - delegate_to_target: 2, - embed_templates: 1, - embed_templates: 2 - ] if (unquote(opts[:as])) do @before_compile LiveViewNative.Renderer end + @before_compile LiveViewNative.Component end :error -> - IO.warn("tried to load LiveViewNative plugin for format #{inspect(format)} but none was found") + if is_nil(format) do + quote do + import LiveViewNative.Component, only: [sigil_LVN: 2] + end + else + IO.warn("tried to load LiveViewNative plugin for format #{inspect(format)} but none was found") + + [] + end - [] end + + [component_ast, plugin_component_ast] end @doc false diff --git a/lib/mix/live_view_native/context.ex b/lib/mix/live_view_native/context.ex index 89a64a8..164fd12 100644 --- a/lib/mix/live_view_native/context.ex +++ b/lib/mix/live_view_native/context.ex @@ -1,4 +1,6 @@ defmodule Mix.LiveViewNative.Context do + @moduledoc false + defstruct context_app: nil, base_module: nil, schema_module: nil, @@ -16,14 +18,14 @@ defmodule Mix.LiveViewNative.Context do |> caller.validate_args!() |> parse_args() - ctx_app = parsed_opts[:context_app] || Mix.Phoenix.context_app() - base_module = Module.concat([Mix.Phoenix.context_base(ctx_app)]) + context_app = parsed_opts[:context_app] || Mix.Phoenix.context_app() + base_module = Module.concat([Mix.Phoenix.context_base(context_app)]) native_module = Module.concat([inspect(base_module) <> "Native"]) web_module = Mix.Phoenix.web_module(base_module) native_path = Path.join(["native", Atom.to_string(format)]) %__MODULE__{ - context_app: ctx_app, + context_app: context_app, base_module: base_module, schema_module: schema_module, native_module: native_module, @@ -112,4 +114,11 @@ defmodule Mix.LiveViewNative.Context do defp put_context_app(opts, string) do Keyword.put(opts, :context_app, String.to_atom(string)) end + + defmacro compile_string(string) do + EEx.compile_string(string) + end + + def last?(plugins, plugin), + do: Enum.at(plugins, -1) == plugin end diff --git a/lib/mix/tasks/lvn.gen.ex b/lib/mix/tasks/lvn.gen.ex index 82f18f5..aedf56e 100644 --- a/lib/mix/tasks/lvn.gen.ex +++ b/lib/mix/tasks/lvn.gen.ex @@ -1,16 +1,23 @@ defmodule Mix.Tasks.Lvn.Gen do alias Mix.LiveViewNative.Context + import Mix.LiveViewNative.Context, only: [ + compile_string: 1, + last?: 2 + ] def run(args) do context = Context.build(args, __MODULE__) - files = files_to_be_generated(context) + if Keyword.get(context.opts, :copy, true) do + files = files_to_be_generated(context) + Context.prompt_for_conflicts(files) - Context.prompt_for_conflicts(files) + copy_new_files(context, files) + end - context - |> copy_new_files(files) - |> print_shell_instructions() + if Keyword.get(context.opts, :info, true) do + print_shell_instructions(context) + end end def print_shell_instructions(context) do @@ -19,15 +26,6 @@ defmodule Mix.Tasks.Lvn.Gen do |> print_config() |> print_router() |> print_endpoint() - |> print_post_instructions() - end - - def last?(plugins, plugin) do - Enum.at(plugins, -1) == plugin - end - - defmacro compile_string(string) do - EEx.compile_string(string) end def print_instructions(context) do @@ -48,14 +46,11 @@ defmodule Mix.Tasks.Lvn.Gen do plugins? = length(plugins) > 0 - stylesheet? = - Mix.Project.deps_apps() - |> Enum.member?(:live_view_native_stylesheet) - """ \e[93;1m# config/config.exs\e[0m - # LVN - registers each available plugin<%= unless plugins? do %> + # \e[91;1mLVN - Required\e[0m + # Registers each available plugin<%= unless plugins? do %> \e[93;1m# Hint: if you add this config to your app populated with client plugins # and run `mix lvn.gen` this configuration's placeholders will be populated\e[0m<% end %> config :live_view_native, plugins: [\e[32;1m<%= if plugins? do %><%= for plugin <- plugins do %> @@ -63,9 +58,9 @@ defmodule Mix.Tasks.Lvn.Gen do # LiveViewNative.SwiftUI<% end %>\e[0m ] - # LVN - Each format must be registered as a mime type - # add to existing configuration if one exists as this will - # overwrite + # \e[91;1mLVN - Required\e[0m + # Each format must be registered as a mime type add to + # existing configuration if one exists as this will overwrite config :mime, :types, %{\e[32;1m # if you want to inspect LVN stylesheets from your browser add the `style` type # "text/styles" => ["styles"],<%= if plugins? do %><%= for plugin <- plugins do %> @@ -73,26 +68,15 @@ defmodule Mix.Tasks.Lvn.Gen do # "text/swiftui" => ["swiftui"]<% end %>\e[0m } - <%= if stylesheet? do %># LVN - Required, you must configure LiveView Native Stylesheets - # on which file path patterns class names should be extracted from - config :live_view_native_stylesheet, - content: [\e[32;1m<%= if plugins? do %><%= for plugin <- plugins do %> - <%= plugin.format %>: [ - "lib/**/*<%= plugin.format %>*" - ]<%= unless last?(plugins, plugin) do %>,<% end %><% end %><% else %> - # swiftui: ["lib/**/*swiftui*"]<% end %>\e[0m - ], - output: "priv/static/assets" - <% end %> - # LVN - Required, you must configure Phoenix to know how - # to encode for the swiftui format + # \e[91;1mLVN - Required\e[0m + # Phoenix must know how to encode each LVN format config :phoenix_template, :format_encoders, [\e[32;1m<%= if plugins? do %><%= for plugin <- plugins do %> <%= plugin.format %>: Phoenix.HTML.Engine<%= unless last?(plugins, plugin) do %>,<% end %><% end %><% else %> # swiftui: Phoenix.HTML.Engine<% end %>\e[0m ] - # LVN - Required, you must configure Phoenix so it knows - # how to compile LVN's neex templates + # \e[91;1mLVN - Required\e[0m + # Phoenix must know how to compile neex templates config :phoenix, :template_engines, [ \e[32;1mneex: LiveViewNative.Engine\e[0m ] @@ -118,8 +102,9 @@ defmodule Mix.Tasks.Lvn.Gen do )) """ - \e[93;1m# lib/<%= Phoenix.Naming.underscore(context.web_module) %>/router.ex\e[0m + \e[93;1m# lib/<%= Macro.underscore(context.web_module) %>/router.ex\e[0m + # \e[91;1mLVN - Required\e[0m # add the formats to the `accepts` plug for the pipeline used by your LiveView Native app plug :accepts, [ "html",\e[32;1m<%= if plugins? do %><%= for plugin <- plugins do %> @@ -127,6 +112,7 @@ defmodule Mix.Tasks.Lvn.Gen do # "swiftui"<% end %>\e[0m ] + # \e[91;1mLVN - Required\e[0m # add the root layout for each format plug :put_root_layout, [ html: <%= inspect(layouts[:html]) %>,\e[32;1m<%= if plugins? do %><%= for plugin <- plugins do %> @@ -142,9 +128,10 @@ defmodule Mix.Tasks.Lvn.Gen do def print_endpoint(context) do """ - \e[93;1m# lib/<%= Phoenix.Naming.underscore(context.web_module) %>/endpoint.ex\e[0m + \e[93;1m# lib/<%= Macro.underscore(context.web_module) %>/endpoint.ex\e[0m - # add the LiveViewNative.LiveRealoder to your endpoint + # \e[36mLVN - Optional\e[0m + # Add the LiveViewNative.LiveRealoder to your endpoint if code_reloading? do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket plug Phoenix.LiveReloader @@ -159,21 +146,12 @@ defmodule Mix.Tasks.Lvn.Gen do context end - def print_post_instructions(context) do - """ - After you have configured your application you should run the following: - - * `mix lvn.gen.layout ` - * `mix lvn.gen.stylsheet App` - """ - |> Mix.shell().info() - - context - end - def switches, do: [ context_app: :string, - web: :string + web: :string, + info: :boolean, + copy: :boolean, + live_form: :boolean ] def validate_args!([]), do: [nil] @@ -181,6 +159,7 @@ defmodule Mix.Tasks.Lvn.Gen do Mix.raise(""" mix lvn.gen does not take any arguments, only the following switches: + --no-live-form --context-app --web """) @@ -188,7 +167,7 @@ defmodule Mix.Tasks.Lvn.Gen do defp files_to_be_generated(context) do path = Mix.Phoenix.context_app_path(context.context_app, "lib") - file = Phoenix.Naming.underscore(context.native_module) <> ".ex" + file = Macro.underscore(context.native_module) <> ".ex" [{:eex, "app_name_native.ex", Path.join(path, file)}] end @@ -200,12 +179,18 @@ defmodule Mix.Tasks.Lvn.Gen do plugins? = length(plugins) > 0 + apps = Mix.Project.deps_apps() + + live_form? = + Keyword.get(context.opts, :live_form, true) && Enum.member?(apps, :live_view_native_live_form) + binding = [ context: context, plugins: plugins, plugins?: plugins?, last?: &last?/2, assigns: %{ + live_form?: live_form?, gettext: true, formats: formats(), layouts: layouts(context.web_module) diff --git a/lib/mix/tasks/lvn.setup.ex b/lib/mix/tasks/lvn.setup.ex new file mode 100644 index 0000000..886bb1b --- /dev/null +++ b/lib/mix/tasks/lvn.setup.ex @@ -0,0 +1,94 @@ +defmodule Mix.Tasks.Lvn.Setup do + alias Mix.LiveViewNative.Context + + def run(args) do + context = Context.build(args, __MODULE__) + + plugins = + LiveViewNative.plugins() + |> Map.values() + + plugins? = length(plugins) > 0 + + apps = Mix.Project.deps_apps() + + stylesheet_opt? = Keyword.get(context.opts, :stylesheet, true) + stylesheet_app? = Enum.member?(apps, :live_view_native_stylesheet) + stylesheet? = stylesheet_opt? && stylesheet_app? + + live_form_opt? = Keyword.get(context.opts, :live_form, true) + live_form_app? = Enum.member?(apps, :live_view_native_live_form) + + if !plugins? do + Mix.shell().info(""" + You have no client plugins configured. `mix lvn.setup` requires + at least one LiveView Native client plugin configured. Add one LVN client + as a dependency then configure it. For example, if you have both `live_view_native_swiftui` + and `live_view_native_jetpack` added as dependencies you would add the following: + + \e[93;1m# config/config.exs\e[0m + + \e[32;1mconfig :live_view_native, plugins: [ + LiveViewNative.Jetpack, + LiveViewNative.SwiftUI + ]\e[0m + """) + end + + if stylesheet_opt? && !stylesheet_app? do + Mix.shell().info(""" + `live_view_native_styelsheet` is not included as a dependency. Please add it and re-run + this setup task. If you do not wish to use `live_view_native_stylesheet` run this task as + `mix lvn.setup --no-stylesheet` + """) + end + + if live_form_opt? && !live_form_app? do + Mix.shell().info(""" + `live_view_native_live_form` is not included as a dependency. Please add it and re-run + this setup task. If you do not wish to generate `live_form` components run this task + as `mix lvn.setup --no-live-form` + """) + end + + if plugins? && + ((stylesheet_opt? && stylesheet_app?) || !stylesheet_opt?) && + ((live_form_opt? && live_form_app?) || !live_form_opt?) do + Mix.Task.run("lvn.gen", ["--no-info"]) + + Enum.each(plugins, fn(plugin) -> + format = Atom.to_string(plugin.format) + Mix.Task.run("lvn.#{format}.gen") + Mix.Task.run("lvn.gen.layout", [format]) + if stylesheet? do + Mix.Task.run("lvn.stylesheet.gen", [format, "App", "--no-info"]) + end + end) + + Mix.Task.rerun("lvn.gen", ["--no-copy"]) + + if stylesheet? do + Mix.Task.rerun("lvn.stylesheet.gen", ["--no-copy"]) + end + end + end + + def switches, do: [ + context_app: :string, + web: :string, + stylesheet: :boolean, + live_form: :boolean + ] + + def validate_args!([]), do: [nil] + def validate_args!(_args) do + Mix.raise(""" + mix lvn.gen does not take any arguments, only the following switches: + + --context-app + --web + --no-stylesheet + --no-live-form + """) + end +end diff --git a/priv/templates/lvn.gen/app_name_native.ex b/priv/templates/lvn.gen/app_name_native.ex index 06950d8..cff6a05 100644 --- a/priv/templates/lvn.gen/app_name_native.ex +++ b/priv/templates/lvn.gen/app_name_native.ex @@ -33,11 +33,9 @@ defmodule <%= inspect context.native_module %> do |> Keyword.put(:as, :render) quote do - use LiveViewNative.Component, unquote(opts)<%= if @gettext do %> - - import <%= inspect context.web_module %>.Gettext<% end %> + use LiveViewNative.Component, unquote(opts) - unquote(verified_routes()) + unquote(helpers(opts[:format])) end end @@ -45,11 +43,9 @@ defmodule <%= inspect context.native_module %> do opts = Keyword.take(opts, [:format, :root, :as]) quote do - use LiveViewNative.Component, unquote(opts)<%= if @gettext do %> - - import <%= inspect context.web_module %>.Gettext<% end %> + use LiveViewNative.Component, unquote(opts) - unquote(verified_routes()) + unquote(helpers(opts[:format])) end end @@ -59,9 +55,22 @@ defmodule <%= inspect context.native_module %> do quote do use LiveViewNative.Component, unquote(opts) - import LiveViewNative.Component, only: [csrf_token: 1]<%= if @gettext do %> - import <%= inspect context.web_module %>.Gettext<% end %> + import LiveViewNative.Component, only: [csrf_token: 1] + + unquote(helpers(opts[:format])) + end + end + defp helpers(<%= if @live_form? do %>format<% else %>_format<% end %>) do + <%= if @live_form? do %>plugin = LiveViewNative.fetch_plugin!(format) + <% end %>quote do + <%= if @gettext do %>import <%= inspect context.web_module %>.Gettext<% end %><%= if @live_form? do %> + if function_exported?(unquote(plugin.component), :__native_opts__, 0) do + import unquote(plugin.component) + end + if function_exported?(<%= inspect context.web_module %>.CoreComponents.unquote(Module.concat([plugin.module_suffix])), :__native_opts__, 0) do + import <%= inspect context.web_module %>.CoreComponents.unquote(Module.concat([plugin.module_suffix])) + end<% end %> unquote(verified_routes()) end end