Packages
surface
0.7.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
46 Versions
Jump to
Current section
46 Versions
Compare versions
55
files changed
+3124
additions
-473
deletions
| @@ -1,5 +1,15 @@ | |
| 1 1 | # Changelog |
| 2 2 | |
| 3 | + ## v0.7.0 (2022-01-13) |
| 4 | + |
| 5 | + * Support Liveview `v0.17` |
| 6 | + * Support rendering `.sface` templates for regular (dead) views (#543) |
| 7 | + * Support passing properties to slots using the shorthand format, e.g. `<:col label="Name">` |
| 8 | + * Add built-in formatter supporting integration with `mix format` (#535) |
| 9 | + * New `<LiveComponent>` component to inject dynamic live components (#518) |
| 10 | + * Optimize rendering of HTML class attributes literals so they can be treated as static text |
| 11 | + * Add property `for` to `<#slot/>` so it can render the slot content directly (without using `index`) |
| 12 | + |
| 3 13 | ## v0.6.1 (2021-10-26) |
| 4 14 | |
| 5 15 | * Add `surface_formatter` dependency to `mix.exs` when running `mix surface.init` (#507) |
| @@ -76,7 +76,7 @@ Add `surface` to the list of dependencies in `mix.exs`: | |
| 76 76 | ```elixir |
| 77 77 | def deps do |
| 78 78 | [ |
| 79 | - {:surface, "~> 0.6.1"} |
| 79 | + {:surface, "~> 0.7.0"} |
| 80 80 | ] |
| 81 81 | end |
| 82 82 | ``` |
| @@ -4,10 +4,11 @@ | |
| 4 4 | {<<"elixir">>,<<"~> 1.11">>}. |
| 5 5 | {<<"files">>, |
| 6 6 | [<<"lib">>,<<"lib/surface">>,<<"lib/surface/renderer.ex">>, |
| 7 | - <<"lib/surface/compiler.ex">>,<<"lib/surface/macro_component.ex">>, |
| 8 | - <<"lib/surface/io_helper.ex">>,<<"lib/surface/live_view.ex">>, |
| 9 | - <<"lib/surface/base_component.ex">>,<<"lib/surface/components">>, |
| 10 | - <<"lib/surface/components/events.ex">>,<<"lib/surface/components/form">>, |
| 7 | + <<"lib/surface/formatter.ex">>,<<"lib/surface/compiler.ex">>, |
| 8 | + <<"lib/surface/macro_component.ex">>,<<"lib/surface/io_helper.ex">>, |
| 9 | + <<"lib/surface/live_view.ex">>,<<"lib/surface/base_component.ex">>, |
| 10 | + <<"lib/surface/components">>,<<"lib/surface/components/events.ex">>, |
| 11 | + <<"lib/surface/components/form">>, |
| 11 12 | <<"lib/surface/components/form/datetime_local_input.ex">>, |
| 12 13 | <<"lib/surface/components/form/reset.ex">>, |
| 13 14 | <<"lib/surface/components/form/time_input.ex">>, |
| @@ -43,6 +44,7 @@ | |
| 43 44 | <<"lib/surface/components/form/error_tag.ex">>, |
| 44 45 | <<"lib/surface/components/dynamic">>, |
| 45 46 | <<"lib/surface/components/dynamic/component.ex">>, |
| 47 | + <<"lib/surface/components/dynamic/live_component.ex">>, |
| 46 48 | <<"lib/surface/components/live_file_input.ex">>, |
| 47 49 | <<"lib/surface/components/live_redirect.ex">>, |
| 48 50 | <<"lib/surface/components/link.ex">>,<<"lib/surface/components/raw.ex">>, |
| @@ -51,7 +53,18 @@ | |
| 51 53 | <<"lib/surface/components/link/button.ex">>, |
| 52 54 | <<"lib/surface/components/context.ex">>, |
| 53 55 | <<"lib/surface/components/utils.ex">>,<<"lib/surface/components/form.ex">>, |
| 54 | - <<"lib/surface/component.ex">>,<<"lib/surface/directive">>, |
| 56 | + <<"lib/surface/component.ex">>,<<"lib/surface/formatter">>, |
| 57 | + <<"lib/surface/formatter/phase.ex">>, |
| 58 | + <<"lib/surface/formatter/node_translator.ex">>, |
| 59 | + <<"lib/surface/formatter/phases">>, |
| 60 | + <<"lib/surface/formatter/phases/tag_whitespace.ex">>, |
| 61 | + <<"lib/surface/formatter/phases/newlines.ex">>, |
| 62 | + <<"lib/surface/formatter/phases/spaces_to_newlines.ex">>, |
| 63 | + <<"lib/surface/formatter/phases/final_newline.ex">>, |
| 64 | + <<"lib/surface/formatter/phases/render.ex">>, |
| 65 | + <<"lib/surface/formatter/phases/block_exceptions.ex">>, |
| 66 | + <<"lib/surface/formatter/phases/indent.ex">>, |
| 67 | + <<"lib/surface/formatter/plugin.ex">>,<<"lib/surface/directive">>, |
| 55 68 | <<"lib/surface/directive/events.ex">>,<<"lib/surface/directive/hook.ex">>, |
| 56 69 | <<"lib/surface/directive/tag_attrs.ex">>, |
| 57 70 | <<"lib/surface/directive/show.ex">>,<<"lib/surface/directive/values.ex">>, |
| @@ -63,7 +76,8 @@ | |
| 63 76 | <<"lib/surface/catalogue.ex">>,<<"lib/surface/api.ex">>, |
| 64 77 | <<"lib/surface/catalogue">>,<<"lib/surface/catalogue/playground.ex">>, |
| 65 78 | <<"lib/surface/catalogue/data.ex">>,<<"lib/surface/catalogue/example.ex">>, |
| 66 | - <<"lib/surface/compiler">>,<<"lib/surface/compiler/converter.ex">>, |
| 79 | + <<"lib/surface/view.ex">>,<<"lib/surface/compiler">>, |
| 80 | + <<"lib/surface/compiler/converter.ex">>, |
| 67 81 | <<"lib/surface/compiler/parse_tree_translator.ex">>, |
| 68 82 | <<"lib/surface/compiler/node_translator.ex">>, |
| 69 83 | <<"lib/surface/compiler/parse_error.ex">>, |
| @@ -85,6 +99,7 @@ | |
| 85 99 | <<"lib/surface/type_handler/phx_event.ex">>, |
| 86 100 | <<"lib/surface/type_handler/atom.ex">>, |
| 87 101 | <<"lib/surface/type_handler/default.ex">>, |
| 102 | + <<"lib/surface/type_handler/dynamic.ex">>, |
| 88 103 | <<"lib/surface/type_handler/map.ex">>, |
| 89 104 | <<"lib/surface/type_handler/keyword.ex">>, |
| 90 105 | <<"lib/surface/type_handler/generator.ex">>, |
| @@ -93,6 +108,7 @@ | |
| 93 108 | <<"lib/surface/type_handler/css_class.ex">>, |
| 94 109 | <<"lib/surface/type_handler/bindings.ex">>,<<"lib/surface/ast.ex">>, |
| 95 110 | <<"lib/mix">>,<<"lib/mix/tasks">>,<<"lib/mix/tasks/surface">>, |
| 111 | + <<"lib/mix/tasks/surface/surface.format.ex">>, |
| 96 112 | <<"lib/mix/tasks/surface/surface.init">>, |
| 97 113 | <<"lib/mix/tasks/surface/surface.init/ex_patcher.ex">>, |
| 98 114 | <<"lib/mix/tasks/surface/surface.init/patchers">>, |
| @@ -139,10 +155,10 @@ | |
| 139 155 | {<<"name">>,<<"phoenix_live_view">>}, |
| 140 156 | {<<"optional">>,false}, |
| 141 157 | {<<"repository">>,<<"hexpm">>}, |
| 142 | - {<<"requirement">>,<<"~> 0.16.0">>}], |
| 158 | + {<<"requirement">>,<<"~> 0.17.4">>}], |
| 143 159 | [{<<"app">>,<<"sourceror">>}, |
| 144 160 | {<<"name">>,<<"sourceror">>}, |
| 145 161 | {<<"optional">>,false}, |
| 146 162 | {<<"repository">>,<<"hexpm">>}, |
| 147 | - {<<"requirement">>,<<"~> 0.8.5">>}]]}. |
| 148 | - {<<"version">>,<<"0.6.1">>}. |
| 163 | + {<<"requirement">>,<<"~> 0.9">>}]]}. |
| 164 | + {<<"version">>,<<"0.7.0">>}. |
| @@ -0,0 +1,499 @@ | |
| 1 | + defmodule Mix.Tasks.Surface.Format do |
| 2 | + @shortdoc "Formats Surface ~F sigils and .sface files in the given files/patterns" |
| 3 | + |
| 4 | + @moduledoc """ |
| 5 | + **To format Surface code using Elixir 1.13 or later, use |
| 6 | + `Surface.Formatter.Plugin`.** |
| 7 | + |
| 8 | + Formats Surface `~F` sigils and `.sface` files in the given files and patterns. |
| 9 | + |
| 10 | + ```bash |
| 11 | + $ mix surface.format "lib/**/*.{ex,exs}" "test/**/*.{ex,exs}" |
| 12 | + $ cat path/to/file.ex | mix surface.format - |
| 13 | + ``` |
| 14 | + |
| 15 | + Takes the same options as `mix format` except for `--check-equivalent`. |
| 16 | + |
| 17 | + ## Formatting options |
| 18 | + |
| 19 | + Like `mix format`, the Surface formatter reads a `.formatter.exs` file in the |
| 20 | + current directory for formatter configuration. The Surface formatter accepts |
| 21 | + the same options as `mix format`. Read more about the expected format of |
| 22 | + `.formatter.exs` and the shared configuration options |
| 23 | + [documented here](https://hexdocs.pm/mix/master/Mix.Tasks.Format.html#module-formatting-options). |
| 24 | + |
| 25 | + The Surface formatter also takes the following two additional options |
| 26 | + specified in `.formatter.exs`: |
| 27 | + |
| 28 | + - `:surface_line_length` overrides `:line_length` only for `mix surface.format` |
| 29 | + (`:line_length` is used otherwise, or defaults to 98) |
| 30 | + - `:surface_inputs` overrides `:inputs` only for `mix surface.format` |
| 31 | + (`:inputs` is used otherwise) |
| 32 | + |
| 33 | + ## Task-specific options |
| 34 | + |
| 35 | + The Surface formatter accepts the same task-specific options as `mix format`. |
| 36 | + [Read documentation for the options documented here.](https://hexdocs.pm/mix/master/Mix.Tasks.Format.html#module-task-specific-options). |
| 37 | + |
| 38 | + For quick reference, here are some examples of using these options: |
| 39 | + |
| 40 | + ```bash |
| 41 | + $ mix surface.format --check-formatted |
| 42 | + ** (Mix) mix surface.format failed due to --check-formatted. |
| 43 | + The following files are not formatted: |
| 44 | + * path/to/component.ex |
| 45 | + * path/to/file.sface |
| 46 | + ``` |
| 47 | + |
| 48 | + ```bash |
| 49 | + $ mix surface.format --dry-run |
| 50 | + ``` |
| 51 | + |
| 52 | + ```bash |
| 53 | + $ mix surface.format --dot-formatter path/to/.formatter.exs |
| 54 | + ``` |
| 55 | + |
| 56 | + You can also use the same syntax as `mix format` for specifying which files to |
| 57 | + format: |
| 58 | + |
| 59 | + ```bash |
| 60 | + $ mix surface.format path/to/file.ex "lib/**/*.{ex,exs}" "test/**/*.{ex,exs}" |
| 61 | + ``` |
| 62 | + """ |
| 63 | + |
| 64 | + use Mix.Task |
| 65 | + |
| 66 | + alias Surface.Formatter |
| 67 | + |
| 68 | + # |
| 69 | + # Functions unique to surface.format (Everything else is taken from Mix.Tasks.Format) |
| 70 | + # |
| 71 | + |
| 72 | + defp format_file_contents!(:stdin, input, opts) do |
| 73 | + # determine whether the input is Elixir or Surface code by checking if `Code.string_to_quoted` can parse it |
| 74 | + case Code.string_to_quoted(input) do |
| 75 | + {:ok, _} -> |
| 76 | + format_ex_string!(input, opts) |
| 77 | + |
| 78 | + {:error, _} -> |
| 79 | + Formatter.format_string!(input, opts) |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + defp format_file_contents!(file, input, opts) do |
| 84 | + case Path.extname(file) do |
| 85 | + ".sface" -> |
| 86 | + Formatter.format_string!(input, opts) |
| 87 | + |
| 88 | + _ -> |
| 89 | + format_ex_string!(input, opts) |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + defp format_ex_string!(input, opts) do |
| 94 | + string = |
| 95 | + ~r/\n( *)~F"""(.*?)"""/s |
| 96 | + |> Regex.replace(input, fn _match, indentation, surface_code -> |
| 97 | + # Indent the entire ~F sigil contents based on the indentation of `~F"""` |
| 98 | + tabs = |
| 99 | + indentation |
| 100 | + |> String.length() |
| 101 | + |> Kernel.div(2) |
| 102 | + |
| 103 | + opts = Keyword.put(opts, :indent, tabs) |
| 104 | + |
| 105 | + "\n#{indentation}~F\"\"\"\n#{Formatter.format_string!(surface_code, opts)}#{indentation}\"\"\"" |
| 106 | + end) |
| 107 | + |
| 108 | + # We do not match on ~F"..." sigils where the first character is `\` |
| 109 | + # in order to allow for a use case in surface_site, where Surface |
| 110 | + # code examples are being rendered inside a Markdown component and |
| 111 | + # contains the string: `~F"\""` |
| 112 | + string = |
| 113 | + Regex.replace(~r/~F\"([^\"\\].*?)\"/s, string, fn _match, code -> |
| 114 | + "~F\"#{Formatter.format_string!(code, opts)}\"" |
| 115 | + end) |
| 116 | + |
| 117 | + string = |
| 118 | + Regex.replace(~r/~F\[(.*?)\]/s, string, fn _match, code -> |
| 119 | + "~F[#{Formatter.format_string!(code, opts)}]" |
| 120 | + end) |
| 121 | + |
| 122 | + string = |
| 123 | + Regex.replace(~r/~F\((.*?)\)/s, string, fn _match, code -> |
| 124 | + "~F(#{Formatter.format_string!(code, opts)})" |
| 125 | + end) |
| 126 | + |
| 127 | + Regex.replace(~r/~F\{(.*?)\}/s, string, fn _match, code -> |
| 128 | + "~F{#{Formatter.format_string!(code, opts)}}" |
| 129 | + end) |
| 130 | + end |
| 131 | + |
| 132 | + # |
| 133 | + # The below functions are taken directly from Mix.Tasks.Format with insignificant modification |
| 134 | + # |
| 135 | + |
| 136 | + @switches [ |
| 137 | + check_formatted: :boolean, |
| 138 | + dot_formatter: :string, |
| 139 | + dry_run: :boolean |
| 140 | + ] |
| 141 | + |
| 142 | + @manifest "cached_dot_formatter" |
| 143 | + @manifest_vsn 1 |
| 144 | + |
| 145 | + @impl true |
| 146 | + def run(args) do |
| 147 | + {opts, args} = OptionParser.parse!(args, strict: @switches) |
| 148 | + {dot_formatter, formatter_opts} = eval_dot_formatter(opts) |
| 149 | + |
| 150 | + {{formatter_opts, subdirectories}, _sources} = |
| 151 | + eval_deps_and_subdirectories(dot_formatter, [], formatter_opts, [dot_formatter]) |
| 152 | + |
| 153 | + # surface_line_length can be used to override the line_length option |
| 154 | + formatter_opts = |
| 155 | + if line_length = formatter_opts[:surface_line_length] do |
| 156 | + Keyword.put(formatter_opts, :line_length, line_length) |
| 157 | + else |
| 158 | + formatter_opts |
| 159 | + end |
| 160 | + |
| 161 | + args |
| 162 | + |> expand_args(dot_formatter, {formatter_opts, subdirectories}) |
| 163 | + |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: 30000) |
| 164 | + |> Enum.reduce({[], [], []}, &collect_status/2) |
| 165 | + |> check!() |
| 166 | + end |
| 167 | + |
| 168 | + defp format_file({file, formatter_opts}, task_opts) do |
| 169 | + {input, extra_opts} = read_file(file) |
| 170 | + formatted = format_file_contents!(file, input, extra_opts ++ formatter_opts) |
| 171 | + output = IO.iodata_to_binary([formatted]) |
| 172 | + |
| 173 | + check_equivalent? = Keyword.get(task_opts, :check_equivalent, false) |
| 174 | + check_formatted? = Keyword.get(task_opts, :check_formatted, false) |
| 175 | + dry_run? = Keyword.get(task_opts, :dry_run, false) |
| 176 | + |
| 177 | + cond do |
| 178 | + check_equivalent? and not equivalent?(input, output) -> |
| 179 | + {:not_equivalent, file} |
| 180 | + |
| 181 | + check_formatted? -> |
| 182 | + if input == output, do: :ok, else: {:not_formatted, file} |
| 183 | + |
| 184 | + dry_run? -> |
| 185 | + :ok |
| 186 | + |
| 187 | + true -> |
| 188 | + write_or_print(file, input, output) |
| 189 | + end |
| 190 | + rescue |
| 191 | + exception -> |
| 192 | + {:exit, file, exception, __STACKTRACE__} |
| 193 | + end |
| 194 | + |
| 195 | + # This function reads exported configuration from the imported |
| 196 | + # dependencies and subdirectories and deals with caching the result |
| 197 | + # of reading such configuration in a manifest file. |
| 198 | + defp eval_deps_and_subdirectories(dot_formatter, prefix, formatter_opts, sources) do |
| 199 | + deps = Keyword.get(formatter_opts, :import_deps, []) |
| 200 | + subs = Keyword.get(formatter_opts, :subdirectories, []) |
| 201 | + |
| 202 | + if not is_list(deps) do |
| 203 | + Mix.raise("Expected :import_deps to return a list of dependencies, got: #{inspect(deps)}") |
| 204 | + end |
| 205 | + |
| 206 | + if not is_list(subs) do |
| 207 | + Mix.raise("Expected :subdirectories to return a list of directories, got: #{inspect(subs)}") |
| 208 | + end |
| 209 | + |
| 210 | + if deps == [] and subs == [] do |
| 211 | + {{formatter_opts, []}, sources} |
| 212 | + else |
| 213 | + manifest = Path.join(Mix.Project.manifest_path(), @manifest) |
| 214 | + |
| 215 | + maybe_cache_in_manifest(dot_formatter, manifest, fn -> |
| 216 | + {subdirectories, sources} = eval_subs_opts(subs, prefix, sources) |
| 217 | + {{eval_deps_opts(formatter_opts, deps), subdirectories}, sources} |
| 218 | + end) |
| 219 | + end |
| 220 | + end |
| 221 | + |
| 222 | + defp eval_deps_opts(formatter_opts, deps) do |
| 223 | + deps_paths = Mix.Project.deps_paths() |
| 224 | + |
| 225 | + parenless_calls = |
| 226 | + for dep <- deps, |
| 227 | + dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths), |
| 228 | + dep_dot_formatter = Path.join(dep_path, ".formatter.exs"), |
| 229 | + File.regular?(dep_dot_formatter), |
| 230 | + dep_opts = eval_file_with_keyword_list(dep_dot_formatter), |
| 231 | + parenless_call <- dep_opts[:export][:locals_without_parens] || [], |
| 232 | + uniq: true, |
| 233 | + do: parenless_call |
| 234 | + |
| 235 | + Keyword.update( |
| 236 | + formatter_opts, |
| 237 | + :locals_without_parens, |
| 238 | + parenless_calls, |
| 239 | + &(&1 ++ parenless_calls) |
| 240 | + ) |
| 241 | + end |
| 242 | + |
| 243 | + defp eval_subs_opts(subs, prefix, sources) do |
| 244 | + {subs, sources} = |
| 245 | + Enum.flat_map_reduce(subs, sources, fn sub, sources -> |
| 246 | + prefix = Path.join(prefix ++ [sub]) |
| 247 | + {Path.wildcard(prefix), [Path.join(prefix, ".formatter.exs") | sources]} |
| 248 | + end) |
| 249 | + |
| 250 | + Enum.flat_map_reduce(subs, sources, fn sub, sources -> |
| 251 | + sub_formatter = Path.join(sub, ".formatter.exs") |
| 252 | + |
| 253 | + if File.exists?(sub_formatter) do |
| 254 | + formatter_opts = eval_file_with_keyword_list(sub_formatter) |
| 255 | + |
| 256 | + {formatter_opts_and_subs, sources} = |
| 257 | + eval_deps_and_subdirectories(:in_memory, [sub], formatter_opts, sources) |
| 258 | + |
| 259 | + {[{sub, formatter_opts_and_subs}], sources} |
| 260 | + else |
| 261 | + {[], sources} |
| 262 | + end |
| 263 | + end) |
| 264 | + end |
| 265 | + |
| 266 | + defp assert_valid_dep_and_fetch_path(dep, deps_paths) when is_atom(dep) do |
| 267 | + case Map.fetch(deps_paths, dep) do |
| 268 | + {:ok, path} -> |
| 269 | + if File.dir?(path) do |
| 270 | + path |
| 271 | + else |
| 272 | + Mix.raise( |
| 273 | + "Unavailable dependency #{inspect(dep)} given to :import_deps in the formatter configuration. " <> |
| 274 | + "The dependency cannot be found in the file system, please run \"mix deps.get\" and try again" |
| 275 | + ) |
| 276 | + end |
| 277 | + |
| 278 | + :error -> |
| 279 | + Mix.raise( |
| 280 | + "Unknown dependency #{inspect(dep)} given to :import_deps in the formatter configuration. " <> |
| 281 | + "The dependency is not listed in your mix.exs for environment #{inspect(Mix.env())}" |
| 282 | + ) |
| 283 | + end |
| 284 | + end |
| 285 | + |
| 286 | + defp maybe_cache_in_manifest(dot_formatter, manifest, fun) do |
| 287 | + cond do |
| 288 | + is_nil(Mix.Project.get()) or dot_formatter != ".formatter.exs" -> fun.() |
| 289 | + entry = read_manifest(manifest) -> entry |
| 290 | + true -> write_manifest!(manifest, fun.()) |
| 291 | + end |
| 292 | + end |
| 293 | + |
| 294 | + defp read_manifest(manifest) do |
| 295 | + with {:ok, binary} <- File.read(manifest), |
| 296 | + {:ok, {@manifest_vsn, entry, sources}} <- safe_binary_to_term(binary), |
| 297 | + expanded_sources = Enum.flat_map(sources, &Path.wildcard(&1, match_dot: true)), |
| 298 | + false <- Mix.Utils.stale?([Mix.Project.config_mtime() | expanded_sources], [manifest]) do |
| 299 | + {entry, sources} |
| 300 | + else |
| 301 | + _ -> nil |
| 302 | + end |
| 303 | + end |
| 304 | + |
| 305 | + defp safe_binary_to_term(binary) do |
| 306 | + {:ok, :erlang.binary_to_term(binary)} |
| 307 | + rescue |
| 308 | + _ -> :error |
| 309 | + end |
| 310 | + |
| 311 | + defp write_manifest!(manifest, {entry, sources}) do |
| 312 | + File.mkdir_p!(Path.dirname(manifest)) |
| 313 | + File.write!(manifest, :erlang.term_to_binary({@manifest_vsn, entry, sources})) |
| 314 | + {entry, sources} |
| 315 | + end |
| 316 | + |
| 317 | + defp eval_dot_formatter(opts) do |
| 318 | + cond do |
| 319 | + dot_formatter = opts[:dot_formatter] -> |
| 320 | + {dot_formatter, eval_file_with_keyword_list(dot_formatter)} |
| 321 | + |
| 322 | + File.regular?(".formatter.exs") -> |
| 323 | + {".formatter.exs", eval_file_with_keyword_list(".formatter.exs")} |
| 324 | + |
| 325 | + true -> |
| 326 | + {".formatter.exs", []} |
| 327 | + end |
| 328 | + end |
| 329 | + |
| 330 | + defp eval_file_with_keyword_list(path) do |
| 331 | + {opts, _} = Code.eval_file(path) |
| 332 | + |
| 333 | + unless Keyword.keyword?(opts) do |
| 334 | + Mix.raise("Expected #{inspect(path)} to return a keyword list, got: #{inspect(opts)}") |
| 335 | + end |
| 336 | + |
| 337 | + opts |
| 338 | + end |
| 339 | + |
| 340 | + defp read_file(:stdin) do |
| 341 | + {IO.stream(:stdio, :line) |> Enum.to_list() |> IO.iodata_to_binary(), file: "stdin"} |
| 342 | + end |
| 343 | + |
| 344 | + defp read_file(file) do |
| 345 | + {File.read!(file), file: file} |
| 346 | + end |
| 347 | + |
| 348 | + defp expand_args([], dot_formatter, formatter_opts_and_subs) do |
| 349 | + if no_entries_in_formatter_opts?(formatter_opts_and_subs) do |
| 350 | + Mix.raise( |
| 351 | + "Expected one or more files/patterns to be given to mix format " <> |
| 352 | + "or for a .formatter.exs file to exist with an :inputs, :surface_inputs or :subdirectories key" |
| 353 | + ) |
| 354 | + end |
| 355 | + |
| 356 | + dot_formatter |
| 357 | + |> expand_dot_inputs([], formatter_opts_and_subs, %{}) |
| 358 | + |> Enum.map(fn {file, {_dot_formatter, formatter_opts}} -> {file, formatter_opts} end) |
| 359 | + end |
| 360 | + |
| 361 | + defp expand_args(files_and_patterns, _dot_formatter, {formatter_opts, subs}) do |
| 362 | + files = |
| 363 | + for file_or_pattern <- files_and_patterns, |
| 364 | + file <- stdin_or_wildcard(file_or_pattern), |
| 365 | + uniq: true, |
| 366 | + do: file |
| 367 | + |
| 368 | + if files == [] do |
| 369 | + Mix.raise( |
| 370 | + "Could not find a file to format. The files/patterns given to command line " <> |
| 371 | + "did not point to any existing file. Got: #{inspect(files_and_patterns)}" |
| 372 | + ) |
| 373 | + end |
| 374 | + |
| 375 | + for file <- files do |
| 376 | + if file == :stdin do |
| 377 | + {file, formatter_opts} |
| 378 | + else |
| 379 | + split = file |> Path.relative_to_cwd() |> Path.split() |
| 380 | + {file, find_formatter_opts_for_file(split, {formatter_opts, subs})} |
| 381 | + end |
| 382 | + end |
| 383 | + end |
| 384 | + |
| 385 | + defp expand_dot_inputs(dot_formatter, prefix, {formatter_opts, subs}, acc) do |
| 386 | + if no_entries_in_formatter_opts?({formatter_opts, subs}) do |
| 387 | + Mix.raise("Expected :inputs, :surface_inputs or :subdirectories key in #{dot_formatter}") |
| 388 | + end |
| 389 | + |
| 390 | + map = |
| 391 | + for input <- List.wrap(formatter_opts[:surface_inputs] || formatter_opts[:inputs]), |
| 392 | + file <- Path.wildcard(Path.join(prefix ++ [input]), match_dot: true), |
| 393 | + do: {expand_relative_to_cwd(file), {dot_formatter, formatter_opts}}, |
| 394 | + into: %{} |
| 395 | + |
| 396 | + acc = |
| 397 | + Map.merge(acc, map, fn file, {dot_formatter1, _}, {dot_formatter2, formatter_opts} -> |
| 398 | + Mix.shell().error( |
| 399 | + "Both #{dot_formatter1} and #{dot_formatter2} specify the file " <> |
| 400 | + "#{Path.relative_to_cwd(file)} in their :inputs or :surface_inputs option. To resolve the " <> |
| 401 | + "conflict, the configuration in #{dot_formatter1} will be ignored. " <> |
| 402 | + "Please change the list of :inputs (or :surface_inputs) in one of the formatter files so only " <> |
| 403 | + "one of them matches #{Path.relative_to_cwd(file)}" |
| 404 | + ) |
| 405 | + |
| 406 | + {dot_formatter2, formatter_opts} |
| 407 | + end) |
| 408 | + |
| 409 | + Enum.reduce(subs, acc, fn {sub, formatter_opts_and_subs}, acc -> |
| 410 | + sub_formatter = Path.join(sub, ".formatter.exs") |
| 411 | + expand_dot_inputs(sub_formatter, [sub], formatter_opts_and_subs, acc) |
| 412 | + end) |
| 413 | + end |
| 414 | + |
| 415 | + defp expand_relative_to_cwd(path) do |
| 416 | + case File.cwd() do |
| 417 | + {:ok, cwd} -> Path.expand(path, cwd) |
| 418 | + _ -> path |
| 419 | + end |
| 420 | + end |
| 421 | + |
| 422 | + defp find_formatter_opts_for_file(split, {formatter_opts, subs}) do |
| 423 | + Enum.find_value(subs, formatter_opts, fn {sub, formatter_opts_and_subs} -> |
| 424 | + if List.starts_with?(split, Path.split(sub)) do |
| 425 | + find_formatter_opts_for_file(split, formatter_opts_and_subs) |
| 426 | + end |
| 427 | + end) |
| 428 | + end |
| 429 | + |
| 430 | + defp stdin_or_wildcard("-"), do: [:stdin] |
| 431 | + defp stdin_or_wildcard(path), do: path |> Path.expand() |> Path.wildcard(match_dot: true) |
| 432 | + |
| 433 | + defp no_entries_in_formatter_opts?({formatter_opts, subs}) do |
| 434 | + is_nil(formatter_opts[:inputs]) and is_nil(formatter_opts[:surface_inputs]) and subs == [] |
| 435 | + end |
| 436 | + |
| 437 | + defp write_or_print(file, input, output) do |
| 438 | + cond do |
| 439 | + file == :stdin -> IO.write(output) |
| 440 | + input == output -> :ok |
| 441 | + true -> File.write!(file, output) |
| 442 | + end |
| 443 | + |
| 444 | + :ok |
| 445 | + end |
| 446 | + |
| 447 | + defp collect_status({:ok, :ok}, acc), do: acc |
| 448 | + |
| 449 | + defp collect_status({:ok, {:exit, _, _, _} = exit}, {exits, not_equivalent, not_formatted}) do |
| 450 | + {[exit | exits], not_equivalent, not_formatted} |
| 451 | + end |
| 452 | + |
| 453 | + defp collect_status({:ok, {:not_equivalent, file}}, {exits, not_equivalent, not_formatted}) do |
| 454 | + {exits, [file | not_equivalent], not_formatted} |
| 455 | + end |
| 456 | + |
| 457 | + defp collect_status({:ok, {:not_formatted, file}}, {exits, not_equivalent, not_formatted}) do |
| 458 | + {exits, not_equivalent, [file | not_formatted]} |
| 459 | + end |
| 460 | + |
| 461 | + defp check!({[], [], []}) do |
| 462 | + :ok |
| 463 | + end |
| 464 | + |
| 465 | + defp check!({[{:exit, :stdin, exception, stacktrace} | _], _not_equivalent, _not_formatted}) do |
| 466 | + Mix.shell().error("mix surface.format failed for stdin") |
| 467 | + reraise exception, stacktrace |
| 468 | + end |
| 469 | + |
| 470 | + defp check!({[{:exit, file, exception, stacktrace} | _], _not_equivalent, _not_formatted}) do |
| 471 | + Mix.shell().error("mix surface.format failed for file: #{Path.relative_to_cwd(file)}") |
| 472 | + reraise exception, stacktrace |
| 473 | + end |
| 474 | + |
| 475 | + defp check!({_exits, [_ | _] = not_equivalent, _not_formatted}) do |
| 476 | + Mix.raise(""" |
| 477 | + mix surface.format failed due to --check-equivalent. |
| 478 | + The following files are not equivalent: |
| 479 | + #{to_bullet_list(not_equivalent)} |
| 480 | + Please report this bug with the input files at https://github.com/surface-ui/surface_formatter/issues |
| 481 | + """) |
| 482 | + end |
| 483 | + |
| 484 | + defp check!({_exits, _not_equivalent, [_ | _] = not_formatted}) do |
| 485 | + Mix.raise(""" |
| 486 | + mix surface.format failed due to --check-formatted. |
| 487 | + The following files are not formatted: |
| 488 | + #{to_bullet_list(not_formatted)} |
| 489 | + """) |
| 490 | + end |
| 491 | + |
| 492 | + defp to_bullet_list(files) do |
| 493 | + Enum.map_join(files, "\n", &" * #{&1 |> to_string() |> Path.relative_to_cwd()}") |
| 494 | + end |
| 495 | + |
| 496 | + defp equivalent?(input, output) do |
| 497 | + input == output |
| 498 | + end |
| 499 | + end |
| @@ -142,6 +142,15 @@ defmodule Mix.Tasks.Surface.Init do | |
| 142 142 | end |
| 143 143 | |
| 144 144 | defp patches_for(:formatter, %{formatter: true}) do |
| 145 | + if Version.match?(System.version(), ">= 1.13.0") do |
| 146 | + %{ |
| 147 | + ".formatter.exs" => [ |
| 148 | + Patches.add_sface_files_to_inputs_in_formatter_config(), |
| 149 | + Patches.add_surface_to_import_deps_in_formatter_config(), |
| 150 | + Patches.add_formatter_plugin_to_formatter_config() |
| 151 | + ] |
| 152 | + } |
| 153 | + else |
| 145 154 | %{ |
| 146 155 | "mix.exs" => [ |
| 147 156 | Patches.add_surface_formatter_to_mix_deps() |
| @@ -152,6 +161,7 @@ defmodule Mix.Tasks.Surface.Init do | |
| 152 161 | ] |
| 153 162 | } |
| 154 163 | end |
| 164 | + end |
| 155 165 | |
| 156 166 | defp patches_for(:error_tag, %{error_tag: true, using_gettext?: true, web_module: web_module}) do |
| 157 167 | %{ |
Loading more files…