Current section

46 Versions

Jump to

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…