forked from phoenixframework/phoenix
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request phoenixframework#758 from laurocaetano/introduce-d…
…igest-task Introduce the digest task
- Loading branch information
Showing
4 changed files
with
218 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
defmodule Mix.Tasks.Phoenix.Digest do | ||
use Mix.Task | ||
@default_input_path "priv/static" | ||
|
||
@shortdoc "Digests and compress static files." | ||
|
||
@moduledoc """ | ||
Digests and compress static files. | ||
mix phoenix.digest priv/static -o public/assets | ||
The first argument is the path where the static files are located. The | ||
`-o` option indicates the path that will be used to save the digested and | ||
compressed files. | ||
If no path is given, it will use `priv/static` as the input and output path. | ||
The output folder will contain: | ||
* the original file | ||
* a compressed file with gzip | ||
* a file containing the original file name and its digest | ||
* a compressed file containing the file name and its digest | ||
* a manifest file | ||
Example of generated files: | ||
* application.js.erb | ||
* application.js.erb.gz | ||
* application.js-eb0a5b9302e8d32828d8a73f137cc8f0.erb | ||
* application.js-eb0a5b9302e8d32828d8a73f137cc8f0.erb.gz | ||
* manifest.json | ||
""" | ||
|
||
@doc false | ||
def run([input|args]) do | ||
{args, _, _} = OptionParser.parse(args, aliases: [o: :output]) | ||
input_path = input || @default_input_path | ||
output_path = args[:output] || input_path | ||
|
||
case Phoenix.Digester.compile(input_path, output_path) do | ||
:ok -> Mix.shell.info [:green, "Check your digested files at '#{output_path}'."] | ||
{:error, :invalid_path} -> Mix.raise "The input path '#{input_path}' does not exist." | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
defmodule Phoenix.Digester do | ||
@digested_file_regex ~r/(-[a-fA-F\d]{32})/ | ||
|
||
@moduledoc """ | ||
Digests and compress static files. | ||
For each file under the given input path, Phoenix will generate a digest | ||
and also compress in `.gz` format. The filename and its digest will be | ||
used to generate the manifest file. It also avoid duplications checking | ||
for already digested files. | ||
""" | ||
|
||
@doc """ | ||
Digests and compress the static files and save them in the given output path. | ||
* `input_path` - The path where the assets are located | ||
* `output_path` - The path where the compiled/compressed files will be saved | ||
""" | ||
@spec compile(String.t, String.t) :: :ok | {:error, :invalid_path} | ||
def compile(input_path, output_path) do | ||
if File.exists?(input_path) do | ||
unless File.exists?(output_path), do: File.mkdir_p!(output_path) | ||
|
||
input_path | ||
|> filter_files | ||
|> do_compile(output_path) | ||
|> generate_manifest(output_path) | ||
:ok | ||
else | ||
{:error, :invalid_path} | ||
end | ||
end | ||
|
||
defp filter_files(input_path) do | ||
input_path | ||
|> Path.join("**") | ||
|> Path.wildcard | ||
|> Enum.filter(&(!File.dir?(&1) && !compiled_file?(&1))) | ||
|> Enum.map(&(map_file(&1, input_path))) | ||
end | ||
|
||
defp do_compile(files, output_path) do | ||
Enum.map(files, fn (file) -> | ||
file | ||
|> digest | ||
|> compress | ||
|> write_to_disk(output_path) | ||
end) | ||
end | ||
|
||
defp generate_manifest(files, output_path) do | ||
entries = Enum.reduce(files, %{}, fn (file, acc) -> | ||
Map.put(acc, Path.join(file.relative_path, file.filename), | ||
Path.join(file.relative_path, file.digested_filename)) | ||
end) | ||
|
||
manifest_content = Poison.Encoder.encode(entries, []) | ||
File.write!(Path.join(output_path, "manifest.json"), manifest_content) | ||
end | ||
|
||
defp compiled_file?(file_path) do | ||
Regex.match?(@digested_file_regex, Path.basename(file_path)) || | ||
Path.extname(file_path) == ".gz" | ||
end | ||
|
||
defp map_file(file_path, input_path) do | ||
%{absolute_path: file_path, | ||
relative_path: Path.relative_to(file_path, input_path) |> Path.dirname, | ||
filename: Path.basename(file_path), | ||
content: File.read!(file_path)} | ||
end | ||
|
||
defp compress(file) do | ||
Map.put(file, :compressed_content, :zlib.gzip(file.content)) | ||
end | ||
|
||
defp digest(file) do | ||
name = Path.rootname(file.filename) | ||
extension = Path.extname(file.filename) | ||
digest = Base.encode16(:erlang.md5(file.content), case: :lower) | ||
Map.put(file, :digested_filename, "#{name}-#{digest}#{extension}") | ||
end | ||
|
||
defp write_to_disk(file, output_path) do | ||
File.mkdir_p!(Path.join(output_path, file.relative_path)) | ||
path = Path.join(output_path, file.relative_path) | ||
|
||
# compressed files | ||
File.write!(Path.join(path, file.digested_filename <> ".gz"), file.compressed_content) | ||
File.write!(Path.join(path, file.filename <> ".gz"), file.compressed_content) | ||
# uncompressed files | ||
File.write!(Path.join(path, file.digested_filename), file.content) | ||
File.write!(Path.join(path, file.filename), file.content) | ||
|
||
file | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
defmodule Mix.Tasks.Phoenix.DigestTest do | ||
use ExUnit.Case, async: true | ||
|
||
test "fails when the given paths are invalid" do | ||
assert_raise Mix.Error, "The input path 'invalid_path' does not exist.", fn -> | ||
Mix.Tasks.Phoenix.Digest.run(["invalid_path"]) | ||
end | ||
end | ||
|
||
test "digests and compress files" do | ||
output_path = Path.join("tmp", "digested") | ||
input_path = "priv/static" | ||
|
||
File.mkdir_p!(output_path) | ||
|
||
Mix.Tasks.Phoenix.Digest.run([input_path, "-o", output_path]) | ||
assert_received {:mix_shell, :info, ["Check your digested files at 'tmp/digested'."]} | ||
end | ||
|
||
test "uses the input path as output path when no outputh path is given" do | ||
input_path = Path.join("tmp", "input_path") | ||
File.mkdir_p!(input_path) | ||
|
||
Mix.Tasks.Phoenix.Digest.run([input_path]) | ||
assert_received {:mix_shell, :info, ["Check your digested files at 'tmp/input_path'."]} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
defmodule Phoenix.DigesterTest do | ||
use ExUnit.Case, async: true | ||
|
||
test "fails when the given paths are invalid" do | ||
assert {:error, :invalid_path} = Phoenix.Digester.compile("inexistent path", "/ ?? /path") | ||
end | ||
|
||
test "digests and compress files" do | ||
output_path = Path.join("tmp", "phoenix_digest") | ||
input_path = "priv/static/" | ||
|
||
assert :ok = Phoenix.Digester.compile(input_path, output_path) | ||
|
||
output_files = assets_files(output_path) | ||
|
||
assert Enum.member?(output_files, "phoenix.png.gz") | ||
assert Enum.member?(output_files, "phoenix.png") | ||
assert Enum.member?(output_files, "manifest.json") | ||
assert Enum.any?(output_files, &(String.match?(&1, ~r/(phoenix-[a-fA-F\d]{32}.png)/))) | ||
assert Enum.any?(output_files, &(String.match?(&1, ~r/(phoenix-[a-fA-F\d]{32}.png.gz)/))) | ||
end | ||
|
||
test "doesn't duplicate files when digesting and compressing twice" do | ||
input_path = Path.join("tmp", "phoenix_digest_twice") | ||
input_file = Path.join(input_path, "file.js") | ||
File.mkdir_p!(input_path) | ||
File.write!(input_file, "console.log('test');") | ||
|
||
assert :ok = Phoenix.Digester.compile(input_path, input_path) | ||
assert :ok = Phoenix.Digester.compile(input_path, input_path) | ||
|
||
output_files = assets_files(input_path) | ||
|
||
duplicated_digested_file_regex = ~r/(file-[a-fA-F\d]{32}.[\w|\d]*.[-[a-fA-F\d]{32})/ | ||
assert Enum.any?(output_files, fn (f) -> | ||
!String.match?(f, duplicated_digested_file_regex) | ||
!String.match?(f, ~r/(file.js.gz.gz)/) | ||
end) | ||
end | ||
|
||
defp assets_files(path) do | ||
path | ||
|> Path.join("**") | ||
|> Path.wildcard | ||
|> Enum.filter(&(!File.dir?(&1))) | ||
|> Enum.map(&(Path.basename(&1))) | ||
end | ||
end |