Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --fingerprint option to collectassets #211

Merged
merged 5 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@
/spec/marten/cli/generator/model_spec/main_app/*
/spec/marten/cli/generator/schema_spec/main_app/*
/spec/marten/cli/manage/command/new_spec
/spec/test_project/manifest.json
/spec/test_project/collect/manifest.json
6 changes: 6 additions & 0 deletions docs/docs/assets/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ Considering the above manifest example, trying to resolve `app/home.css` would p
Marten.assets.url("app/home.css") # => "/assets/app/home.9495841be78cdf06c45d.css"
```

:::info
Additionally, the [`collectassets`](../development/reference/management-commands.md#collectassets) command provides a `--fingerprint` option. Using this option automatically fingerprints the collected assets and generates a `manifest.json` file, which maps the original file paths to their fingerprinted versions.

When the `--fingerprint` option is used, it's important to include the path to the generated "manifest.json" in the appropriate [`assets.manifests`](../development/reference/settings#manifests) environment config file, otherwise the collected assets can't be found when the URL is resolved.
:::

## Resolving asset URLs

As mentioned previously, assets are collected and persisted in a specific storage. When building HTML [templates](../templates/introduction.md), you will usually need to "resolve" the URL of assets to generate the absolute URLs that should be inserted into stylesheet or script tags (for example).
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/development/reference/management-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Please refer to [Asset handling](../../assets/introduction.md) to learn more abo
### Options

* `--no-input` - Does not show prompts to the user
* `--fingerprint` - Attaches a fingerprint to the collected assets
* `--manifest-path` - Configures where the manifest.json is stored. Only relevant if `--fingerprint` is activated. (default to `src/manifest.json`)

### Examples

Expand Down
75 changes: 75 additions & 0 deletions spec/marten/cli/manage/command/collect_assets_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,79 @@ describe Marten::CLI::Manage::Command::CollectAssets do
File.exists?("spec/assets/css/test.css").should be_true
end
end

describe "#create_manifest_file" do
with_main_app_location "#{__DIR__}/../../../../test_project"

it "copies the assets as expected, adds a fingerprint and creates the correct mapping inside a manifest.json" do
treagod marked this conversation as resolved.
Show resolved Hide resolved
stdin = IO::Memory.new("yes")
stdout = IO::Memory.new

original_css_asset_path = "spec/test_project/assets/css/test.css"

command = Marten::CLI::Manage::Command::CollectAssets.new(
options: ["--no-color", "--no-input", "--fingerprint"],
stdin: stdin,
stdout: stdout
)

sha = Digest::MD5.new
sha.file original_css_asset_path
file_digest = sha.hexfinal[...12]

command.handle

output = stdout.rewind.gets_to_end
output.includes?("Copying css/test.css (#{file_digest})...").should be_true
output.includes?("Creating spec/test_project/manifest.json...").should be_true

File.exists?("spec/assets/css/test.#{file_digest}.css").should be_true
File.exists?("spec/test_project/manifest.json").should be_true

json = File.open("spec/test_project/manifest.json") do |file|
JSON.parse(file)
end

manifest = json.as_h

manifest.has_key?("css/test.css").should be_true
manifest["css/test.css"].to_s.should eq "css/test.#{file_digest}.css"
end

it "copies the assets as expected, adds a fingerprint and creates the correct mapping inside a specified file" do
stdin = IO::Memory.new("yes")
stdout = IO::Memory.new

original_css_asset_path = "spec/test_project/assets/css/test.css"
manifest_path = "spec/test_project/collect/manifest.json"

command = Marten::CLI::Manage::Command::CollectAssets.new(
options: ["--no-color", "--no-input", "--fingerprint", "--manifest-path", manifest_path],
stdin: stdin,
stdout: stdout
)

sha = Digest::MD5.new
sha.file original_css_asset_path
file_digest = sha.hexfinal[...12]

command.handle

output = stdout.rewind.gets_to_end
output.includes?("Copying css/test.css (#{file_digest})...").should be_true
output.includes?("Creating #{manifest_path}...").should be_true

File.exists?("spec/assets/css/test.#{file_digest}.css").should be_true
File.exists?(manifest_path).should be_true

json = File.open(manifest_path) do |file|
JSON.parse(file)
end

manifest = json.as_h

manifest.has_key?("css/test.css").should be_true
manifest["css/test.css"].to_s.should eq "css/test.#{file_digest}.css"
end
end
end
62 changes: 60 additions & 2 deletions src/marten/cli/manage/command/collect_assets.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,26 @@ module Marten
command_name :collectassets
help "Collect all the assets and copy them in a unique storage."

@fingerprint : Bool = false
@fingerprint_mapping = Hash(String, String).new
@no_input : Bool = false
@manifest_path : String = File.join(Marten.apps.main.class._marten_app_location, "manifest.json")

def setup
on_option(
"fingerprint",
"Fingerprint the collected assets and generate a corresponding manifest file"
) do
@fingerprint = true
end
on_option("no-input", "Do not show prompts to the user") { @no_input = true }
on_option_with_arg(
"manifest-path",
arg: "Filepath",
description: "Specify where the manifest file should be stored (defaults to \"src/manifest.json\")."
) do |v|
@manifest_path = v
end
end

def run
Expand All @@ -29,6 +45,23 @@ module Marten
collect
end

private def calculate_fingerprint(relative_path, io)
last_dot_index = relative_path.rindex(".")
old_path = relative_path
fingerprint = nil

if last_dot_index
sha = Digest::MD5.new
sha.update io
io.rewind
fingerprint = sha.hexfinal[...12]
relative_path = relative_path[0...last_dot_index] + ".#{fingerprint}" + relative_path[last_dot_index..]
@fingerprint_mapping[old_path] = relative_path
end

return relative_path, fingerprint
end

private def collect
collected_count = 0

Expand All @@ -42,16 +75,41 @@ module Marten
if collected_count == 0
print("No assets to collect...")
end

if fingerprint? && collected_count > 0
create_manifest_file
end
end

private def copy_asset_file(relative_path, absolute_path)
File.open(absolute_path) do |io|
print(" › Copying #{style(relative_path, mode: :dim)}...", ending: "")
Marten.assets.storage.write(relative_path, io)
original_relative_path = relative_path
relative_path, fingerprint = calculate_fingerprint(relative_path, io) if fingerprint?

print(" › Copying #{style(original_relative_path, mode: :dim)}", ending: "")
print(style(" (#{fingerprint})", mode: :dim), ending: "") if fingerprint
print("...", ending: "")

Marten.assets.storage.write(relative_path.not_nil!, io)
print(style(" DONE", fore: :light_green, mode: :bold))
end
end

private def create_manifest_file
FileUtils.mkdir_p(Path[@manifest_path].dirname) # Ensure path exists

relative_manifest_path = Path[@manifest_path].relative_to(Marten::Apps::Config.compilation_root_path)
print(" › Creating #{style(relative_manifest_path, mode: :dim)}...", ending: "")
File.open(@manifest_path, "w") do |file|
file.print @fingerprint_mapping.to_json
end
print(style(" DONE", fore: :light_green, mode: :bold))
end

private def fingerprint?
@fingerprint
end

private def no_input?
@no_input
end
Expand Down
Loading