├── .jrubyrc ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .rspec ├── .yardopts ├── spec ├── support │ ├── sources │ │ ├── serve │ │ │ └── index.js │ │ └── myapp │ │ │ ├── app │ │ │ └── assets │ │ │ │ ├── css │ │ │ │ └── app.css │ │ │ │ └── js │ │ │ │ └── app.ts │ │ │ └── slices │ │ │ └── admin │ │ │ └── assets │ │ │ └── js │ │ │ └── app.ts │ ├── sources.rb │ └── app.rb ├── spec_helper.rb ├── unit │ └── hanami │ │ └── assets │ │ └── public_assets_dir_spec.rb └── integration │ └── hanami │ └── assets │ └── manifest_spec.rb ├── lib ├── hanami-assets.rb └── hanami │ ├── assets │ ├── version.rb │ ├── errors.rb │ ├── base_url.rb │ ├── asset.rb │ └── config.rb │ └── assets.rb ├── .rubocop.yml ├── Rakefile ├── .gitignore ├── Gemfile ├── .repobot.yml ├── package.json ├── LICENSE.md ├── hanami-assets.gemspec ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /.jrubyrc: -------------------------------------------------------------------------------- 1 | debug.fullTrace=true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | - 2 | README.md 3 | LICENSE.md 4 | lib/**/*.rb 5 | -------------------------------------------------------------------------------- /spec/support/sources/serve/index.js: -------------------------------------------------------------------------------- 1 | console.log('serve'); 2 | -------------------------------------------------------------------------------- /spec/support/sources/myapp/app/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | background: #f00; 3 | } 4 | -------------------------------------------------------------------------------- /lib/hanami-assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "hanami/assets" 4 | -------------------------------------------------------------------------------- /spec/support/sources/myapp/slices/admin/assets/js/app.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello from admin/index.ts"); 2 | -------------------------------------------------------------------------------- /spec/support/sources/myapp/app/assets/js/app.ts: -------------------------------------------------------------------------------- 1 | import "../css/app.css"; 2 | 3 | console.log("Hello from index.ts"); 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Please keep AllCops, Bundler, Style, Metrics groups and then order cops 2 | # alphabetically 3 | inherit_from: 4 | - https://raw.githubusercontent.com/hanami/devtools/main/.rubocop.yml 5 | -------------------------------------------------------------------------------- /lib/hanami/assets/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hanami 4 | class Assets 5 | # Defines the version 6 | # 7 | # @since 0.1.0 8 | VERSION = "2.3.0" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/sources.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Sources 5 | PATH = Pathname(__dir__).join(".", "sources").freeze 6 | private_constant :PATH 7 | 8 | def self.path(*dir) 9 | PATH.join(*dir) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rake" 4 | require "bundler/gem_tasks" 5 | require "rspec/core/rake_task" 6 | require "hanami/devtools/rake_tasks" 7 | 8 | RSpec::Core::RakeTask.new(:spec) 9 | 10 | require "rubocop/rake_task" 11 | 12 | RuboCop::RakeTask.new 13 | 14 | task default: %i[spec rubocop] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | .rubocop-* 24 | node_modules/ 25 | .byebug_history 26 | yarn.lock 27 | package-lock.json 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec 5 | 6 | unless ENV["CI"] 7 | gem "byebug", require: false, platforms: :mri 8 | gem "yard", require: false 9 | end 10 | 11 | gem "hanami-view", github: "hanami/view", branch: "main", require: false 12 | gem "hanami-devtools", github: "hanami/devtools", branch: "main", require: false 13 | -------------------------------------------------------------------------------- /.repobot.yml: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # This is a config synced from hanami/template-gem repo 5 | ########################################################### 6 | 7 | sources: 8 | - repo: hanami/template-gem 9 | sync: 10 | - ".repobot.yml.erb" 11 | - ".github/FUNDING.yml" 12 | - "CODE_OF_CONDUCT.md" 13 | - "LICENSE.md.erb" 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hanami-assets", 3 | "version": "2.1.0", 4 | "description": "Hanami Assets", 5 | "main": "index.js", 6 | "repository": "git@github.com:hanami/assets.git", 7 | "author": "Luca Guidi ", 8 | "license": "MIT", 9 | "packageManager": "yarn@3.2.0", 10 | "devDependencies": { 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "typescript": "^5.2.2" 14 | }, 15 | "dependencies": { 16 | "hanami-assets": "github:hanami/assets-js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/support/app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | require "securerandom" 5 | require "fileutils" 6 | 7 | module App 8 | PATH = Pathname(__dir__).join("..", "..", "tmp").freeze 9 | private_constant :PATH 10 | 11 | def self.create(app) 12 | root = PATH.join(SecureRandom.uuid).tap(&:mkpath) 13 | 14 | sources = [ 15 | app.join("app"), 16 | app.join("slices") 17 | ] 18 | 19 | public_dir = root.join("public") 20 | assets_dir = public_dir.join("assets") 21 | 22 | Dir.chdir(root) do 23 | sources.each do |source| 24 | FileUtils.cp_r(source, root) 25 | end 26 | 27 | FileUtils.mkdir_p(assets_dir) 28 | end 29 | 30 | root 31 | end 32 | 33 | def self.clean 34 | return true unless PATH.exist? 35 | 36 | FileUtils.remove_entry_secure( 37 | PATH 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec" 4 | require "hanami-assets" 5 | require "pathname" 6 | 7 | SPEC_ROOT = Pathname(__FILE__).dirname 8 | 9 | RSpec.configure do |config| 10 | config.expect_with :rspec do |expectations| 11 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 12 | end 13 | 14 | config.mock_with :rspec do |mocks| 15 | mocks.verify_partial_doubles = true 16 | end 17 | 18 | config.shared_context_metadata_behavior = :apply_to_host_groups 19 | 20 | config.filter_run_when_matching :focus 21 | config.disable_monkey_patching! 22 | 23 | config.warnings = true 24 | 25 | config.default_formatter = "doc" if config.files_to_run.one? 26 | 27 | config.profile_examples = 10 28 | 29 | config.order = :random 30 | Kernel.srand config.seed 31 | end 32 | 33 | Dir.glob(File.join(Dir.pwd, "spec", "support", "*.rb"), &method(:require)) 34 | -------------------------------------------------------------------------------- /spec/unit/hanami/assets/public_assets_dir_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Hanami::Assets, ".public_assets_dir" do 4 | subject(:public_assets_dir) { described_class.public_assets_dir(slice) } 5 | 6 | let(:slice) { double(:slice, slice_name: double(to_s: slice_name), app: double(:app)) } 7 | let(:slice_name) { "main" } 8 | 9 | describe "top-level slices" do 10 | it "underscores the slice name" do 11 | expect(public_assets_dir).to eq "_main" 12 | end 13 | end 14 | 15 | describe "nested slices" do 16 | let(:slice_name) { "main/nested" } 17 | 18 | it "underscores all name segments" do 19 | expect(public_assets_dir).to eq "_main/_nested" 20 | end 21 | end 22 | 23 | describe "app" do 24 | before do 25 | allow(slice).to receive(:app) { slice } 26 | end 27 | 28 | it "returns nil" do 29 | expect(public_assets_dir).to be nil 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/hanami/assets/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hanami 4 | class Assets 5 | # Base error for Hanami::Assets. 6 | # 7 | # @api public 8 | # @since 0.1.0 9 | class Error < ::StandardError 10 | end 11 | 12 | # Error returned when the assets manifest file is missing. 13 | # 14 | # @api public 15 | # @since 2.1.0 16 | class ManifestMissingError < Error 17 | def initialize(manifest_path) 18 | super(<<~TEXT) 19 | Missing manifest file at #{manifest_path.inspect} 20 | 21 | Have you run `hanami assets compile` or `hanami assets watch`? 22 | TEXT 23 | end 24 | end 25 | 26 | # Error raised when no asset can be found for a source path. 27 | # 28 | # @api public 29 | # @since 2.1.0 30 | class AssetMissingError < Error 31 | def initialize(source_path) 32 | super(<<~TEXT) 33 | No asset found for #{source_path.inspect} 34 | TEXT 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2014-2021 Luca Guidi 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/integration/hanami/assets/manifest_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tmpdir" 4 | 5 | RSpec.describe "manifest handling" do 6 | subject(:assets) { 7 | Hanami::Assets.new( 8 | config: Hanami::Assets::Config.new(**config_kwargs), 9 | root: root 10 | ) 11 | } 12 | 13 | let(:config_kwargs) { {} } 14 | 15 | context "manifest_path configured and real file exists" do 16 | let(:root) { @dir } 17 | 18 | before do 19 | @dir = Dir.mktmpdir 20 | File.write(File.join(@dir, "assets.json"), <<~JSON) 21 | { 22 | "app.js": {"url": "/path/to/app.js"} 23 | } 24 | JSON 25 | end 26 | 27 | after do 28 | FileUtils.remove_entry @dir 29 | end 30 | 31 | it "returns asset paths from the manifest" do 32 | expect(assets["app.js"].to_s).to eq "/path/to/app.js" 33 | end 34 | 35 | it "raises an AssetMissingError if an asset can not be found" do 36 | expect { assets["missing.js"] } 37 | .to raise_error Hanami::Assets::AssetMissingError, /missing.js/ 38 | end 39 | end 40 | 41 | context "no file at configured manifest_path" do 42 | let(:root) { "/missing/dir" } 43 | 44 | it "raises a ManifestMissingError" do 45 | expect { assets["app.js"] } 46 | .to raise_error Hanami::Assets::ManifestMissingError, %r{/missing/dir/assets.json} 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /hanami-assets.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "hanami/assets/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "hanami-assets" 9 | spec.version = Hanami::Assets::VERSION 10 | spec.authors = ["Hanakai team"] 11 | spec.email = ["info@hanakai.org"] 12 | spec.summary = "Assets management" 13 | spec.description = "Assets management for Ruby web applications" 14 | spec.homepage = "http://hanamirb.org" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -- lib/* bin/* CHANGELOG.md LICENSE.md README.md hanami-assets.gemspec`.split($/) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | spec.metadata["rubygems_mfa_required"] = "true" 21 | spec.required_ruby_version = ">= 3.2" 22 | 23 | spec.add_dependency "zeitwerk", "~> 2.6" 24 | 25 | spec.add_development_dependency "rake", "~> 13" 26 | spec.add_development_dependency "rspec", "~> 3.9" 27 | spec.add_development_dependency "rubocop", "~> 1.0" 28 | spec.add_development_dependency "rack", "~> 2.2" 29 | spec.add_development_dependency "rack-test", "~> 1.1" 30 | spec.add_development_dependency "dry-configurable", "~> 1.1" 31 | spec.add_development_dependency "dry-inflector", "~> 1.0" 32 | end 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "30 4 * * *" 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: 16 | - "4.0.0-preview3" 17 | - "3.4" 18 | - "3.3" 19 | - "3.2" 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | bundler-cache: true 28 | - name: Cache Node modules 29 | id: cache-node-modules 30 | uses: actions/cache@v4 31 | with: 32 | path: node_modules 33 | key: node-modules-${{ runner.OS }}-${{ hashFiles('package.json') }} 34 | - name: Install 35 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 36 | run: npm install 37 | - name: Set up Node 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 20.x 41 | registry-url: https://registry.npmjs.org 42 | - name: Run the test suite 43 | run: bundle exec rake 44 | 45 | workflow-keepalive: 46 | if: github.event_name == 'schedule' 47 | runs-on: ubuntu-latest 48 | permissions: 49 | actions: write 50 | steps: 51 | - uses: liskin/gh-workflow-keepalive@v1 52 | -------------------------------------------------------------------------------- /lib/hanami/assets/base_url.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | 5 | module Hanami 6 | class Assets 7 | # Base URL 8 | # 9 | # @since 2.1.0 10 | # @api private 11 | class BaseUrl 12 | # @since 2.1.0 13 | # @api private 14 | attr_reader :url 15 | private :url 16 | 17 | # Initialize a base URL 18 | # 19 | # @param url [String] the URL 20 | # @param prefix [String,NilClass] the prefix 21 | # 22 | # @since 2.1.0 23 | # @api private 24 | def initialize(url, prefix = nil) 25 | @url = URI(url + prefix.to_s).to_s 26 | freeze 27 | end 28 | 29 | # Join the base URL with the given paths 30 | # 31 | # @param other [String] the paths 32 | # @return [String] the joined URL 33 | # 34 | # @since 2.1.0 35 | # @api private 36 | def join(other) 37 | (url + other).to_s 38 | end 39 | 40 | # @since 2.1.0 41 | # @api private 42 | def to_s 43 | @url 44 | end 45 | 46 | # Check if the source is a cross origin 47 | # 48 | # @param source [String] the source 49 | # @return [Boolean] true if the source is a cross origin 50 | # 51 | # @since 2.1.0 52 | # @api private 53 | def crossorigin?(source) 54 | # TODO: review if this is the right way to check for cross origin 55 | return true if @url.empty? 56 | 57 | !source.start_with?(@url) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/hanami/assets/asset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hanami 4 | class Assets 5 | # Represents a single front end asset. 6 | # 7 | # @api public 8 | # @since 2.1.0 9 | class Asset 10 | # @api private 11 | # @since 2.1.0 12 | attr_reader :config 13 | private :config 14 | 15 | # Returns the asset's absolute URL path. 16 | # 17 | # @example Asset from local dev server 18 | # asset.path # => "/assets/app.js" 19 | # 20 | # @example Deployed asset with fingerprinted name 21 | # asset.path # => "/assets/app-28a6b886de2372ee3922fcaf3f78f2d8.js" 22 | # 23 | # @return [String] 24 | # 25 | # @api public 26 | # @since 2.1.0 27 | attr_reader :path 28 | 29 | # @api private 30 | # @since 2.1.0 31 | attr_reader :base_url 32 | private :base_url 33 | 34 | # Returns the asset's subresource integrity value, or nil if none is available. 35 | # 36 | # @return [String, nil] 37 | # 38 | # @api public 39 | # @since 2.1.0 40 | attr_reader :sri 41 | 42 | # @api private 43 | # @since 2.1.0 44 | def initialize(path:, base_url:, sri: nil) 45 | @path = path 46 | @base_url = base_url 47 | @sri = sri 48 | end 49 | 50 | # @api public 51 | # @since 2.1.0 52 | alias_method :subresource_integrity_value, :sri 53 | 54 | # Returns the asset's full URL. 55 | # 56 | # @example Asset from local dev server 57 | # asset.path # => "https://example.com/assets/app.js" 58 | # 59 | # @example Deployed asset with fingerprinted name 60 | # asset.path # => "https://example.com/assets/app-28a6b886de2372ee3922fcaf3f78f2d8.js" 61 | # 62 | # @return [String] 63 | # 64 | # @api public 65 | # @since 2.1.0 66 | def url 67 | base_url.join(path) 68 | end 69 | 70 | # Returns the asset's full URL 71 | # 72 | # @return [String] 73 | # 74 | # @see #url 75 | # 76 | # @api public 77 | # @since 2.1.0 78 | def to_s 79 | url 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/hanami/assets/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/configurable" 4 | require_relative "base_url" 5 | 6 | module Hanami 7 | class Assets 8 | # Hanami assets configuration. 9 | # 10 | # @api public 11 | # @since 0.1.0 12 | class Config 13 | include Dry::Configurable 14 | 15 | # @api public 16 | # @since 2.1.0 17 | BASE_URL = "" 18 | private_constant :BASE_URL 19 | 20 | # @!attribute [rw] node_command 21 | # @return [String] 22 | # 23 | # @api public 24 | # @since 2.1.0 25 | setting :node_command, default: "node" 26 | 27 | # @!attribute [rw] path_prefix 28 | # @return [String] 29 | # 30 | # @api public 31 | # @since 2.1.0 32 | setting :path_prefix, default: "/assets" 33 | 34 | # @!attribute [rw] subresource_integrity 35 | # @return [Array] 36 | # 37 | # @example 38 | # config.subresource_integrity # => [:sha256, :sha512] 39 | # 40 | # @api public 41 | # @since 2.1.0 42 | setting :subresource_integrity, default: [] 43 | 44 | # @!attribute [rw] base_url 45 | # @return [BaseUrl] 46 | # 47 | # @example 48 | # config.base_url = "http://some-cdn.com/assets" 49 | # 50 | # @api public 51 | # @since 2.1.0 52 | setting :base_url, constructor: -> url { BaseUrl.new(url.to_s) } 53 | 54 | # @api public 55 | # @since 2.1.0 56 | def initialize(**values) 57 | super() 58 | 59 | config.update(values.select { |k| _settings.key?(k) }) 60 | 61 | yield(config) if block_given? 62 | end 63 | 64 | # Returns true if the given source is linked via Cross-Origin policy (or in other words, if 65 | # the given source does not satisfy the Same-Origin policy). 66 | # 67 | # @param source [String] 68 | # 69 | # @return [Boolean] 70 | # 71 | # @see https://en.wikipedia.org/wiki/Same-origin_policy#Origin_determination_rules 72 | # @see https://en.wikipedia.org/wiki/Same-origin_policy#document.domain_property 73 | # 74 | # @api private 75 | # @since 1.2.0 76 | def crossorigin?(source) 77 | base_url.crossorigin?(source) 78 | end 79 | 80 | private 81 | 82 | def method_missing(name, ...) 83 | if config.respond_to?(name) 84 | config.public_send(name, ...) 85 | else 86 | super 87 | end 88 | end 89 | 90 | def respond_to_missing?(name, _incude_all = false) 91 | config.respond_to?(name) || super 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/hanami/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "json" 4 | require "pathname" 5 | require "zeitwerk" 6 | 7 | module Hanami 8 | # Assets management for Ruby web applications 9 | # 10 | # @since 0.1.0 11 | class Assets 12 | # @since 2.1.0 13 | # @api private 14 | def self.gem_loader 15 | @gem_loader ||= Zeitwerk::Loader.new.tap do |loader| 16 | root = File.expand_path("..", __dir__) 17 | loader.tag = "hanami-assets" 18 | loader.push_dir(root) 19 | loader.ignore( 20 | "#{root}/hanami-assets.rb", 21 | "#{root}/hanami/assets/version.rb", 22 | "#{root}/hanami/assets/errors.rb" 23 | ) 24 | loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-assets.rb") 25 | end 26 | end 27 | 28 | gem_loader.setup 29 | require_relative "assets/version" 30 | require_relative "assets/errors" 31 | 32 | # Returns the directory (under `public/assets/`) to be used for storing a slice's compiled 33 | # assets. 34 | # 35 | # This is shared logic used by both Hanami (for the assets provider) and Hanami::CLI (for the 36 | # assets commands). 37 | # 38 | # @since 2.1.0 39 | # @api private 40 | def self.public_assets_dir(slice) 41 | return nil if slice.app.eql?(slice) 42 | 43 | slice.slice_name.to_s.split("/").map { |name| "_#{name}" }.join("/") 44 | end 45 | 46 | # @api private 47 | # @since 2.1.0 48 | MANIFEST_PATH = "assets.json" 49 | private_constant :MANIFEST_PATH 50 | 51 | # @api private 52 | # @since 2.1.0 53 | attr_reader :config 54 | 55 | # @api private 56 | # @since 2.1.0 57 | attr_reader :root 58 | 59 | # @api public 60 | # @since 2.1.0 61 | def initialize(config:, root:) 62 | @config = config 63 | @root = Pathname(root) 64 | end 65 | 66 | # Returns the asset at the given path. 67 | # 68 | # @return [Hanami::Assets::Asset] the asset 69 | # 70 | # @raise AssetMissingError if no asset can be found at the path 71 | # 72 | # @api public 73 | # @since 2.1.0 74 | def [](path) 75 | asset_attrs = manifest 76 | .fetch(path) { raise AssetMissingError.new(path) } 77 | .transform_keys(&:to_sym) 78 | .tap { |attrs| 79 | # The `url` attribute we receive from the manifest is actually a path; rename it as such 80 | # so our `Asset` attributes make more sense on their own. 81 | attrs[:path] = attrs.delete(:url) 82 | } 83 | 84 | Asset.new( 85 | **asset_attrs, 86 | base_url: config.base_url 87 | ) 88 | end 89 | 90 | # Returns true if subresource integrity is configured. 91 | # 92 | # @return [Boolean] 93 | # 94 | # @api public 95 | # @since 2.1.0 96 | def subresource_integrity? 97 | config.subresource_integrity.any? 98 | end 99 | 100 | # Returns true if the given source path is a cross-origin request. 101 | # 102 | # @return [Boolean] 103 | # 104 | # @api public 105 | # @since 2.1.0 106 | def crossorigin?(source_path) 107 | config.crossorigin?(source_path) 108 | end 109 | 110 | private 111 | 112 | def manifest 113 | return @manifest if instance_variable_defined?(:@manifest) 114 | 115 | full_manifest_path = root.join(MANIFEST_PATH) 116 | 117 | unless full_manifest_path.exist? 118 | raise ManifestMissingError.new(full_manifest_path.to_s) 119 | end 120 | 121 | @manifest = JSON.parse(File.read(full_manifest_path)) 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Hanami::Assets 2 | Assets management for Ruby web applications 3 | 4 | ## v2.3.0 - 2025-11-12 5 | 6 | ## v2.3.0.beta2 - 2025-10-17 7 | 8 | ### Changed 9 | 10 | - Drop support for Ruby 3.1 11 | 12 | ## v2.3.0.beta1 - 2025-10-03 13 | 14 | ## v2.2.0 - 2024-11-05 15 | 16 | ## v2.2.0.rc1 - 2024-10-29 17 | 18 | ## v2.2.0.beta2 - 2024-09-25 19 | 20 | ## v2.2.0.beta1 - 2024-07-16 21 | 22 | ### Changed 23 | 24 | - Drop support for Ruby 3.0 25 | 26 | ## v2.1.0 - 2024-02-27 27 | 28 | ## v2.1.0.rc3 - 2024-02-16 29 | ### Changed 30 | - [Tim Riley] Require a `root:` argument when initializing `Hanami::Assets`. This should be the directory containing the compiled assets and their `assets.json` manifest file. 31 | - [Tim Riley] Removed `manifest_path` setting; the manifest path is no longer user-configurable. 32 | - [Tim Riley] Replaced `package_manager_run_command` setting with `node_command` setting. 33 | - [Tim Riley] Removed unused `sources`, `entry_point_patterns` and `destination` settings. 34 | - [Tim Riley] Removed `bin/hanami-assets` executable. 35 | 36 | ## v2.1.0.rc2 - 2023-11-08 37 | 38 | ## v2.1.0.rc1 - 2023-11-01 39 | 40 | ## v2.1.0.beta2 - 2023-10-04 41 | ### Added 42 | - [Luca Guidi] Official support for Ruby: Ruby 3.1, and 3.2 43 | 44 | ### Changed 45 | - [Luca Guidi] Drop support for Ruby: MRI 2 and JRuby 46 | - [Luca Guidi] This gem now requires a working Node and Yarn installation 47 | - [Tim Riley] Changed the gem to load using Zeitwerk, via `require "hanami/assets"` 48 | - [Tim Riley] Changed `Hanami::Assets` to a class, initialized with a `Hanami::Assets::Config` (see below) and providing a `#[]` method returning a `Hanami::Assets::Asset` instance per asset. 49 | - [Tim Riley] Moved `Hanami::Assets::Helpers` to `Hanami::Helpers::AssetsHelper` in the hanami gem (along with various helper methods renamed; see the hanami CHANGELOG for details) 50 | - [Luca Guidi] Renamed `Hanami::Assets::Configuration` to `Config` 51 | - [Luca Guidi] Removed `Hanami::Assets.configure`, use `Hanami::Assets::Config.new` 52 | - [Luca Guidi] Removed `Hanami::Assets.deploy`, `.precompile`, `.load!` as precompile process is now handled via JavaScript 53 | - [Luca Guidi] Removed `Hanami::Assets.sources`, as third-party libraries should be handled via Yarn 54 | - [Luca Guidi] Removed `Hanami::Assets::Config#fingerprint`, as fingerprinting will be always activated 55 | - [Luca Guidi] Changed `Hanami::Assets::Config#subresource_integrity`. To activate the feature, pass an array of algorithms to use (e.g. `config.subresource_integrity = ["sha-384"]`) 56 | - [Luca Guidi] Removed `Hanami::Assets::Config#cdn`. To activate the feature, pass the CDN base URL to the initializer of the configuration (`base_url` keyword argument). 57 | - [Luca Guidi] Removed `Hanami::Assets::Config#javascript_compressor` and `stylesheet_compressor`, as the compression is now handled via JavaScript 58 | - [Luca Guidi] Removed `Hanami::Assets::Config#scheme`, `#host`, `#port`, and `#prefix`. Use `base_url` keyword argument to pass to configuration initializer 59 | - [Luca Guidi] Removed `Hanami::Assets::Config#root`, `#public_directory`, `#destination_directory`, and `#manifest` as they will now looked up via conventions 60 | - [Luca Guidi] Moved `Hanami::Assets::Precompiler` and `Watcher` to `hanami-cli` 61 | 62 | ## v1.3.5 - 2021-01-14 63 | ### Added 64 | - [Luca Guidi] Official support for Ruby: MRI 3.0 65 | - [Luca Guidi] Official support for Ruby: MRI 2.7 66 | 67 | ## v1.3.4 - 2019-10-11 68 | ### Fixed 69 | - [unleashy] Precompile assets using binary mode to ensure compatibility with Windows 70 | 71 | ## v1.3.3 - 2019-09-13 72 | ### Fixed 73 | - [Landon Grindheim] Lazily load `sassc` only when required 74 | - [Landon Grindheim] Ensure assets precompilation to not crash when SASS stylesheet doesn't have dependencies 75 | 76 | ## v1.3.2 - 2019-08-02 77 | ### Added 78 | - [Landon Grindheim & Sean Collins] Added support for `sassc` gem, because `sass` is no longer maintained 79 | 80 | ## v1.3.1 - 2019-01-18 81 | ### Added 82 | - [Luca Guidi] Official support for Ruby: MRI 2.6 83 | - [Luca Guidi] Support `bundler` 2.0+ 84 | 85 | ### Fixed 86 | - [Luca Guidi] Make optional nested assets feature to maintain backward compatibility with `1.2.x` 87 | 88 | ## v1.3.0 - 2018-10-24 89 | 90 | ## v1.3.0.beta1 - 2018-08-08 91 | ### Added 92 | - [Paweł Świątkowski] Preserve directory structure of assets at the precompile time. 93 | - [Luca Guidi] Official support for JRuby 9.2.0.0 94 | 95 | ## v1.2.0 - 2018-04-11 96 | 97 | ## v1.2.0.rc2 - 2018-04-06 98 | 99 | ## v1.2.0.rc1 - 2018-03-30 100 | 101 | ## v1.2.0.beta2 - 2018-03-23 102 | 103 | ## v1.2.0.beta1 - 2018-02-28 104 | ### Added 105 | - [Luca Guidi] Collect assets informations for Early Hints (103) 106 | - [Luca Guidi] Send automatically javascripts and stylesheets via Push Promise / Early Hints 107 | - [Luca Guidi] Add the ability to send audio, video, and generic assets for Push Promise / Early Hints 108 | 109 | ## v1.1.1 - 2018-02-27 110 | ### Added 111 | - [Luca Guidi] Official support for Ruby: MRI 2.5 112 | 113 | ### Fixed 114 | - [Malina Sulca] Print `href` and `src` first in output HTML 115 | 116 | ## v1.1.0 - 2017-10-25 117 | ### Fixed 118 | - [Luca Guidi] Don't let `#javascript` and `#stylesheet` helpers to append file extension if the URL contains a query string 119 | 120 | ## v1.1.0.rc1 - 2017-10-16 121 | 122 | ## v1.1.0.beta3 - 2017-10-04 123 | 124 | ## v1.1.0.beta2 - 2017-10-03 125 | 126 | ## v1.1.0.beta1 - 2017-08-11 127 | 128 | ## v1.0.0 - 2017-04-06 129 | 130 | ## v1.0.0.rc1 - 2017-03-31 131 | 132 | ## v1.0.0.beta2 - 2017-03-17 133 | 134 | ## v1.0.0.beta1 - 2017-02-14 135 | ### Added 136 | - [Luca Guidi] Official support for Ruby: MRI 2.4 137 | 138 | ## v0.4.0 - 2016-11-15 139 | ### Fixed 140 | - [Luca Guidi] Ensure `NullManifest` to be pretty printable 141 | 142 | ### Changed 143 | - [Luca Guidi] Official support for Ruby: MRI 2.3+ and JRuby 9.1.5.0+ 144 | - [Sean Collins] Rename digest into fingerprint 145 | 146 | ## v0.3.0 - 2016-07-22 147 | ### Added 148 | - [Matthew Gibbons & Sean Collins] Subresource Integrity (SRI) 149 | - [Matthew Gibbons & Sean Collins] Allow `javascript` and `stylesheet` helpers to accept a Hash representing HTML attributes. Eg. `<%= javascript 'application', async: true %>` 150 | 151 | ### Fixed 152 | - [Alexander Gräfe] Safely precompile assets from directories with a dot in their name. 153 | - [Luca Guidi] Detect changes for Sass/SCSS dependencies. 154 | - [Maxim Dorofienko & Luca Guidi] Preserve static assets under public directory, by removing only assets directory and manifest at the precompile time. 155 | 156 | ### Changed 157 | – [Luca Guidi] Drop support for Ruby 2.0 and 2.1. Official support for JRuby 9.0.5.0+. 158 | - [Luca Guidi] Don't create digest version of files under public directory, but only for precompiled files. 159 | 160 | ## v0.2.1 - 2016-02-05 161 | ### Changed 162 | - [Derk-Jan Karrenbeld] Don't precompile `.map` files 163 | 164 | ### Fixed 165 | - [Luca Guidi] Fix recursive Sass imports 166 | - [Luca Guidi] Ensure to truncate assets in public before to precompile/copy them 167 | 168 | ## v0.2.0 - 2016-01-22 169 | ### Changed 170 | - [Luca Guidi] Renamed the project 171 | 172 | ## v0.1.0 - 2016-01-12 173 | ### Added 174 | - [Luca Guidi] Configurable assets compressors 175 | - [Luca Guidi] Builtin JavaScript and stylesheet compressors 176 | - [deepj & Michael Deol] Added `Lotus::Assets::Helpers#favicon` 177 | - [Leigh Halliday] Added `Lotus::Assets::Helpers#video` 178 | - [Kleber Correia] Added `Lotus::Assets::Helpers#audio` 179 | - [Gonzalo Rodríguez-Baltanás Díaz] Added `Lotus::Assets::Helpers#image` 180 | - [Luca Guidi] Added `Lotus::Assets::Helpers#javascript` and `#stylesheet` 181 | - [Luca Guidi] Added `Lotus::Assets::Helpers#asset_path` and `#asset_url` 182 | - [Luca Guidi] "CDN Mode" let helpers to generate CDN URLs (eg. `https://123.cloudfront.net/assets/application-d1829dc353b734e3adc24855693b70f9.js`) 183 | - [Luca Guidi] "Digest Mode" let helpers to generate digest URLs (eg. `/assets/application-d1829dc353b734e3adc24855693b70f9.js`) 184 | - [Luca Guidi] Added `hanami-assets` command to precompile assets at the deploy time 185 | - [Luca Guidi] Added support for third party gems that want to ship gemified assets for Lotus 186 | - [Luca Guidi] Assets preprocessors (eg. Sass, ES6, CoffeeScript, Opal, JSX) 187 | - [Luca Guidi] Official support for Ruby 2.0+ 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hanami::Assets 2 | 3 | Assets management for Ruby web projects 4 | 5 | ## Status 6 | 7 | [![Gem Version](https://badge.fury.io/rb/hanami-assets.svg)](https://badge.fury.io/rb/hanami-assets) 8 | [![CI](https://github.com/hanami/hanami-assets/actions/workflows/ci.yml/badge.svg)](https://github.com/hanami/hanami-assets/actions?query=workflow%3Aci+branch%3Amain) 9 | 10 | ## Contact 11 | 12 | * Home page: http://hanamirb.org 13 | * Community: http://hanamirb.org/community 14 | * Guides: https://guides.hanamirb.org 15 | * Mailing List: http://hanamirb.org/mailing-list 16 | * API Doc: http://rubydoc.info/gems/hanami-assets 17 | * Forum: https://discourse.hanamirb.org 18 | * Chat: http://chat.hanamirb.org 19 | 20 | 21 | ## Installation 22 | 23 | Add this line to your application's Gemfile: 24 | 25 | ```ruby 26 | gem "hanami-assets" 27 | ``` 28 | 29 | And then execute: 30 | 31 | ```shell 32 | $ bundle 33 | ``` 34 | 35 | Or install it yourself as: 36 | 37 | ```shell 38 | $ gem install hanami-assets 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Command Line (CLI) 44 | 45 | During development run `bundle exec hanami server`. 46 | Your app will start the assets management. 47 | 48 | ### Helpers 49 | 50 | Hanami Assets provides asset-specific helpers to be used in templates. 51 | They resolve one or multiple sources into corresponding HTML tags. 52 | Those sources can be either a name of a local asset or an absolute URL. 53 | 54 | Given the following template: 55 | 56 | ```erb 57 | 58 | 59 | 60 | Assets example 61 | <%= stylesheet_tag "reset", "app" %> 62 | 63 | 64 | 65 | 66 | <%= javascript_tag "app" %> 67 | <%= javascript_tag "https://cdn.somethirdparty.script/foo.js", async: true %> 68 | 69 | 70 | ``` 71 | 72 | It will output this markup: 73 | 74 | ```html 75 | 76 | 77 | 78 | Assets example 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | ### Available Helpers 92 | 93 | The `hanami` gem ships with the following helpers for assets: 94 | 95 | * `asset_url` 96 | * `javascript_tag` 97 | * `stylesheet_tag` 98 | * `favicon_tag` 99 | * `image_tag` 100 | * `video_tag` 101 | * `audio_tag` 102 | * `path_tag` 103 | 104 | ## App Structure 105 | 106 | Hanami applications are generated via `hanami new` CLI command. 107 | 108 | Among other directories, it generates a specific structure for assets: 109 | 110 | ```shell 111 | $ tree app/assets 112 | ├── images 113 | │   └── favicon.ico 114 | ├── js 115 | │   └── app.ts 116 | └── css 117 |    └── app.css 118 | ``` 119 | 120 | #### Entry Points 121 | 122 | Entry Points are the JavaScript files or modules that serve as the starting points of your application. 123 | They define the scope of your bundling process and determine which parts of your code will be included in the final output. 124 | By understanding the dependencies of your entry points, Hanami Assets can create efficient and optimized bundles for your JavaScript or TypeScript applications. 125 | 126 | When Hanami Assets encounters an import or require statement for an asset, it process the asset file to the output directory. 127 | This process includes any kind of asset: other JavaScript files, stylesheets, images **referenced from the Entry Point**. 128 | 129 | The default entry points are: 130 | 131 | * `app/assets/js/app.ts` 132 | * `slices/[slice-name]/assets/js/app.ts` 133 | 134 | You can specify custom Entry Points, by adding an `app.{js,ts,mjs,mts,tsx,jsx}` file into the assets directory of the app or a slice. 135 | 136 | An example is: `app/assets/js/login/app.ts` to define a new Entry Point for a Login page where you want to have a more lightweight bundle. 137 | 138 | #### Static Assets 139 | 140 | Except for `js` and `css` directories, all the other directories are considered **static**. 141 | Their files will be copied as they are to the destination directory. 142 | 143 | If you have a custom directory `app/assets/fonts`, all the fonts are copied to the destination direcotry. 144 | 145 | #### Destination Directory 146 | 147 | The destination directory is `public/assets`. 148 | 149 | ### Deployment 150 | 151 | To process the assets during deployment run `bundle exec hanami assets compile`. 152 | 153 | The destination directory will contain the processed assets with an hashed name. 154 | 155 | #### Fingerprint Mode 156 | 157 | Asset fingerprinting is a technique that involves adding a unique identifier to the filenames of static assets to ensure cache-busting. 158 | By doing so, you can safely cache and deliver updated versions of assets to client browsers, avoiding the use of outdated cached versions and ensuring a consistent and up-to-date user experience. 159 | 160 | During the deployment process, Hanami Assets appends to the file name a unique hash. 161 | 162 | Example: `app/assets/js/app.ts` -> `public/assets/app-QECGTTYG.js` 163 | 164 | It creates a `/public/assets.json` to map the original asset name to the fingerprint name. 165 | 166 | The simple usage of the `js` helper, will be automatically mapped for you: 167 | 168 | ```erb 169 | <%= assets.js "app" %> 170 | ``` 171 | 172 | ```html 173 | 174 | ``` 175 | 176 | #### Subresource Integrity (SRI) Mode 177 | 178 | Subresource Integrity (SRI) is a security mechanism that allows browsers to verify the integrity of external resources by comparing their content against a cryptographic hash. It helps protect against unauthorized modifications to external scripts and enhances the security and trustworthiness of web applications. 179 | 180 | ```ruby 181 | module MyApp 182 | class App < Hanami::App 183 | config.assets.subresource_integrity = ["sha-384"] 184 | end 185 | end 186 | ``` 187 | 188 | Once turned on, it will look at `/public/assets.json`, and helpers such as `javascript` will include an `integrity` and `crossorigin` attribute. 189 | 190 | ```erb 191 | <%= assets.js "app" %> 192 | ``` 193 | 194 | ```html 195 | 196 | ``` 197 | 198 | #### Content Delivery Network (CDN) Mode 199 | 200 | A Content Delivery Network (CDN) is a globally distributed network of servers strategically located in multiple geographical locations. 201 | CDNs are designed to improve the performance, availability, and scalability of websites and web applications by reducing latency and efficiently delivering content to end users. 202 | 203 | A Hanami project can serve assets via a Content Delivery Network (CDN). 204 | 205 | ```ruby 206 | module MyApp 207 | class App < Hanami::App 208 | config.assets.base_url = "https://123.cloudfront.net" 209 | end 210 | end 211 | ``` 212 | 213 | From now on, helpers will return the absolute URL for the asset, hosted on the CDN specified. 214 | 215 | ```erb 216 | <%= javascript 'application' %> 217 | ``` 218 | 219 | ```html 220 | 221 | ``` 222 | 223 | ```erb 224 | <%= assets.js "app" %> 225 | ``` 226 | 227 | ```html 228 | 229 | ``` 230 | 231 | NOTE: We suggest to use SRI mode when using CDN. 232 | 233 | ## Development 234 | 235 | Install: 236 | 237 | * Node 238 | * NPM 239 | 240 | ```bash 241 | $ npm install 242 | $ bundle exec rake test 243 | ``` 244 | 245 | ## Versioning 246 | 247 | __Hanami::Assets__ uses [Semantic Versioning 2.0.0](http://semver.org) 248 | 249 | ## Contributing 250 | 251 | 1. Fork it (https://github.com/hanami/assets/fork) 252 | 2. Create your feature branch (`git checkout -b my-new-feature`) 253 | 3. Commit your changes (`git commit -am 'Add some feature'`) 254 | 4. Push to the branch (`git push origin my-new-feature`) 255 | 5. Create new Pull Request 256 | 257 | ## Copyright 258 | 259 | Copyright © 2014–2024 Hanami Team – Released under MIT License 260 | --------------------------------------------------------------------------------