require 'pathname' require 'json' require 'hanami/utils/string' require 'hanami/utils/class' require 'hanami/utils/path_prefix' require 'hanami/utils/basic_object' require 'hanami/assets/config/manifest' require 'hanami/assets/config/sources' module Hanami module Assets # Framework configuration # # @since 0.1.0 class Configuration # rubocop:disable Metrics/ClassLength # @since 0.1.0 # @api private DEFAULT_SCHEME = 'http'.freeze # @since 0.1.0 # @api private DEFAULT_HOST = 'localhost'.freeze # @since 0.1.0 # @api private DEFAULT_PORT = '2300'.freeze # @since 0.1.0 # @api private DEFAULT_PUBLIC_DIRECTORY = 'public'.freeze # @since 0.1.0 # @api private DEFAULT_MANIFEST = 'assets.json'.freeze # @since 0.1.0 # @api private DEFAULT_PREFIX = '/assets'.freeze # @since 0.1.0 # @api private URL_SEPARATOR = '/'.freeze # @since 0.1.0 # @api private HTTP_SCHEME = 'http'.freeze # @since 0.1.0 # @api private HTTP_PORT = '80'.freeze # @since 0.1.0 # @api private HTTPS_SCHEME = 'https'.freeze # @since 0.1.0 # @api private HTTPS_PORT = '443'.freeze # @since 0.3.0 # @api private DEFAULT_SUBRESOURCE_INTEGRITY_ALGORITHM = :sha256 # @since 0.3.0 # @api private SUBRESOURCE_INTEGRITY_SEPARATOR = ' '.freeze # Return a copy of the configuration of the framework instance associated # with the given class. # # When multiple instances of Hanami::Assets are used in the same # application, we want to make sure that a controller or an action will # receive the expected configuration. # # @param base [Class, Module] a controller or an action # # @return [Hanami::Assets::Configuration] the configuration associated # to the given class. # # @since 0.1.0 # @api private def self.for(base) # TODO: this implementation is similar to Hanami::Controller::Configuration consider to extract it into Hanami::Utils namespace = Utils::String.namespace(base) framework = Utils::Class.load_from_pattern!("(#{namespace}|Hanami)::Assets") framework.configuration end # @since 0.4.0 # @api private attr_reader :public_manifest # Return a new instance # # @return [Hanami::Assets::Configuration] a new instance # # @since 0.1.0 # @api private def initialize(&blk) reset! instance_eval(&blk) if block_given? end # Compile mode # # Determine if compile assets from sources to destination. # Usually this is turned off in production mode. # # @since 0.1.0 def compile(value = nil) if value.nil? @compile else @compile = value end end # Fingerprint mode # # Determine if the helpers should generate the fingerprinted path for an asset. # Usually this is turned on in production mode. # # @since 0.1.0 def fingerprint(value = nil) if value.nil? @fingerprint else @fingerprint = value end end # Subresource integrity mode # # Determine if the helpers should generate the integrity attribute for an # asset. Usually this is turned on in production mode. # # @since 0.3.0 def subresource_integrity(*values) if values.empty? @subresource_integrity elsif values.length == 1 @subresource_integrity = values.first else @subresource_integrity = values end end # CDN mode # # Determine if the helpers should always generate absolute URL. # This is useful in production mode. # # @since 0.1.0 def cdn(value = nil) if value.nil? @cdn else @cdn = !!value # rubocop:disable Style/DoubleNegation end end # JavaScript compressor # # Determine which compressor to use for JavaScript files during deploy. # # By default it's nil, that means it doesn't compress JavaScripts at deploy time. # # It accepts a Symbol or an object that respond to #compress(file). # # The following symbols are accepted: # # * :builtin - Ruby based implementation of jsmin. It doesn't require any external gem. # * :yui - YUI Compressor, it depends on yui-compressor gem and it requires Java 1.4+ # * :uglifier - UglifyJS, it depends on uglifier gem and it requires Node.js # * :closure - Google Closure Compiler, it depends on closure-compiler gem and it requires Java # # @param value [Symbol,#compress] the compressor # # @since 0.1.0 # # @see http://yui.github.io/yuicompressor # @see https://rubygems.org/gems/yui-compressor # # @see http://lisperator.net/uglifyjs # @see https://rubygems.org/gems/uglifier # # @see https://developers.google.com/closure/compiler # @see https://rubygems.org/gems/closure-compiler # # @example YUI Compressor # require 'hanami/assets' # # Hanami::Assets.configure do # # ... # javascript_compressor :yui # end.load! # # @example Custom Compressor # require 'hanami/assets' # # Hanami::Assets.configure do # # ... # javascript_compressor MyCustomJavascriptCompressor.new # end.load! def javascript_compressor(value = nil) if value.nil? @javascript_compressor else @javascript_compressor = value end end # Stylesheet compressor # # Determine which compressor to use for Stylesheet files during deploy. # # By default it's nil, that means it doesn't compress Stylesheets at deploy time. # # It accepts a Symbol or an object that respond to #compress(file). # # The following symbols are accepted: # # * :builtin - Ruby based compressor. It doesn't require any external gem. It's fast, but not an efficient compressor. # * :yui - YUI-Compressor, it depends on yui-compressor gem and requires Java 1.4+ # * :sass - Sass, it depends on sass gem # # @param value [Symbol,#compress] the compressor # # @since 0.1.0 # # @see http://yui.github.io/yuicompressor # @see https://rubygems.org/gems/yui-compressor # # @see http://sass-lang.com # @see https://rubygems.org/gems/sass # # @example YUI Compressor # require 'hanami/assets' # # Hanami::Assets.configure do # # ... # stylesheet_compressor :yui # end.load! # # @example Custom Compressor # require 'hanami/assets' # # Hanami::Assets.configure do # # ... # stylesheet_compressor MyCustomStylesheetCompressor.new # end.load! def stylesheet_compressor(value = nil) if value.nil? @stylesheet_compressor else @stylesheet_compressor = value end end # URL scheme for the application # # This is used to generate absolute URL from helpers. # # @since 0.1.0 def scheme(value = nil) if value.nil? @scheme else @scheme = value end end # URL host for the application # # This is used to generate absolute URL from helpers. # # @since 0.1.0 def host(value = nil) if value.nil? @host else @host = value end end # URL port for the application # # This is used to generate absolute URL from helpers. # # @since 0.1.0 def port(value = nil) if value.nil? @port else @port = value.to_s end end # URL port for the application # # This is used to generate absolute or relative URL from helpers. # # @since 0.1.0 def prefix(value = nil) if value.nil? @prefix else @prefix = Utils::PathPrefix.new(value) end end # Sources root # # @since 0.1.0 def root(value = nil) if value.nil? @root else @root = Pathname.new(value).realpath sources.root = @root end end # Application public directory # # @since 0.1.0 def public_directory(value = nil) if value.nil? @public_directory else @public_directory = Pathname.new(::File.expand_path(value)) end end # Destination directory # # It's the combination of public_directory and prefix. # # @since 0.1.0 # @api private def destination_directory @destination_directory ||= public_directory.join(*prefix.split(URL_SEPARATOR)) end # Manifest path from public directory # # @since 0.1.0 def manifest(value = nil) if value.nil? @manifest else @manifest = value.to_s end end # Absolute manifest path # # @since 0.1.0 # @api private def manifest_path public_directory.join(manifest) end # Application's assets sources # # @since 0.1.0 # @api private def sources @sources ||= Hanami::Assets::Config::Sources.new(root) end # Application's assets # # @since 0.1.0 # @api private def files sources.files end # @since 0.3.0 # @api private def source(file) pathname = Pathname.new(file) pathname.absolute? ? pathname : find(file) end # Find a file from sources # # @since 0.1.0 # @api private def find(file) @sources.find(file) end # Relative URL # # @since 0.1.0 # @api private def asset_path(source) if cdn asset_url(source) else compile_path(source) end end # Absolute URL # # @since 0.1.0 # @api private def asset_url(source) "#{@base_url}#{compile_path(source)}" end # An array of crypographically secure hashing algorithms to use for # generating asset subresource integrity checks # # @since 0.3.0 def subresource_integrity_algorithms if @subresource_integrity == true [DEFAULT_SUBRESOURCE_INTEGRITY_ALGORITHM] else # Using Array() allows us to accept Array or Symbol, and '|| nil' lets # us return an empty array when @subresource_integrity is `false` Array(@subresource_integrity || nil) end end # Subresource integrity attribute # # @since 0.3.0 # @api private def subresource_integrity_value(source) return unless subresource_integrity public_manifest.subresource_integrity_values( prefix.join(source) ).join(SUBRESOURCE_INTEGRITY_SEPARATOR) end # Load Javascript compressor # # @return [Hanami::Assets::Compressors::Javascript] a compressor # # @raise [Hanami::Assets::Compressors::UnknownCompressorError] when the # given name refers to an unknown compressor engine # # @since 0.1.0 # @api private # # @see Hanami::Assets::Configuration#javascript_compressor # @see Hanami::Assets::Compressors::Javascript#for def js_compressor require 'hanami/assets/compressors/javascript' Hanami::Assets::Compressors::Javascript.for(javascript_compressor) end # Load Stylesheet compressor # # @return [Hanami::Assets::Compressors::Stylesheet] a compressor # # @raise [Hanami::Assets::Compressors::UnknownCompressorError] when the # given name refers to an unknown compressor engine # # @since 0.1.0 # @api private # # @see Hanami::Assets::Configuration#stylesheet_compressor # @see Hanami::Assets::Compressors::Stylesheet#for def css_compressor require 'hanami/assets/compressors/stylesheet' Hanami::Assets::Compressors::Stylesheet.for(stylesheet_compressor) end # @since 0.1.0 # @api private def duplicate # rubocop:disable Metrics/AbcSize, Metrics/MethodLength Configuration.new.tap do |c| c.root = root c.scheme = scheme c.host = host c.port = port c.prefix = prefix c.subresource_integrity = subresource_integrity c.cdn = cdn c.compile = compile c.public_directory = public_directory c.manifest = manifest c.sources = sources.dup c.javascript_compressor = javascript_compressor c.stylesheet_compressor = stylesheet_compressor end end # @since 0.1.0 # @api private def reset! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength @scheme = DEFAULT_SCHEME @host = DEFAULT_HOST @port = DEFAULT_PORT @prefix = Utils::PathPrefix.new(DEFAULT_PREFIX) @subresource_integrity = false @cdn = false @fingerprint = false @compile = false @base_url = nil @destination_directory = nil @public_manifest = Config::NullManifest.new(self) @javascript_compressor = nil @stylesheet_compressor = nil root Dir.pwd public_directory root.join(DEFAULT_PUBLIC_DIRECTORY) manifest DEFAULT_MANIFEST end # Load the configuration # # This MUST be executed before to accept the first HTTP request # # @since 0.1.0 # @api private def load! if (fingerprint || subresource_integrity) && manifest_path.exist? @public_manifest = Config::Manifest.new( JSON.parse(manifest_path.read), manifest_path ) end @base_url = URI::Generic.build(scheme: scheme, host: host, port: url_port).to_s end protected # @since 0.3.0 # @api private attr_writer :subresource_integrity # @since 0.1.0 # @api private attr_writer :cdn # @since 0.1.0 # @api private attr_writer :compile # @since 0.1.0 # @api private attr_writer :scheme # @since 0.1.0 # @api private attr_writer :host # @since 0.1.0 # @api private attr_writer :port # @since 0.1.0 # @api private attr_writer :prefix # @since 0.1.0 # @api private attr_writer :root # @since 0.1.0 # @api private attr_writer :public_directory # @since 0.1.0 # @api private attr_writer :manifest # @since 0.1.0 # @api private attr_writer :sources # @since 0.1.0 # @api private attr_writer :javascript_compressor # @since 0.1.0 # @api private attr_writer :stylesheet_compressor private # @since 0.1.0 # @api private def compile_path(source) result = prefix.join(source) result = public_manifest.target(result) if fingerprint result.to_s end # @since 0.1.0 # @api private def url_port ((scheme == HTTP_SCHEME && port == HTTP_PORT) || # rubocop:disable Style/MultilineTernaryOperator (scheme == HTTPS_SCHEME && port == HTTPS_PORT)) ? nil : port.to_i end end end end