propshaft-1.2.1/0000755000004100000410000000000015044375704013562 5ustar www-datawww-datapropshaft-1.2.1/propshaft.gemspec0000644000004100000410000000437515044375704017146 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: propshaft 1.2.1 ruby lib Gem::Specification.new do |s| s.name = "propshaft".freeze s.version = "1.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "rubygems_mfa_required" => "true" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["David Heinemeier Hansson".freeze] s.date = "1980-01-02" s.email = "dhh@hey.com".freeze s.files = ["MIT-LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "lib/propshaft.rb".freeze, "lib/propshaft/assembly.rb".freeze, "lib/propshaft/asset.rb".freeze, "lib/propshaft/compiler.rb".freeze, "lib/propshaft/compiler/css_asset_urls.rb".freeze, "lib/propshaft/compiler/js_asset_urls.rb".freeze, "lib/propshaft/compiler/source_mapping_urls.rb".freeze, "lib/propshaft/compilers.rb".freeze, "lib/propshaft/errors.rb".freeze, "lib/propshaft/helper.rb".freeze, "lib/propshaft/load_path.rb".freeze, "lib/propshaft/manifest.rb".freeze, "lib/propshaft/output_path.rb".freeze, "lib/propshaft/processor.rb".freeze, "lib/propshaft/quiet_assets.rb".freeze, "lib/propshaft/railtie.rb".freeze, "lib/propshaft/railties/assets.rake".freeze, "lib/propshaft/resolver/dynamic.rb".freeze, "lib/propshaft/resolver/static.rb".freeze, "lib/propshaft/server.rb".freeze, "lib/propshaft/version.rb".freeze] s.homepage = "https://github.com/rails/propshaft".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.7.0".freeze) s.rubygems_version = "3.3.15".freeze s.summary = "Deliver assets for Rails.".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_runtime_dependency(%q.freeze, [">= 7.0.0"]) s.add_runtime_dependency(%q.freeze, [">= 7.0.0"]) s.add_runtime_dependency(%q.freeze, [">= 0"]) else s.add_dependency(%q.freeze, [">= 7.0.0"]) s.add_dependency(%q.freeze, [">= 7.0.0"]) s.add_dependency(%q.freeze, [">= 0"]) end end propshaft-1.2.1/lib/0000755000004100000410000000000015044375704014330 5ustar www-datawww-datapropshaft-1.2.1/lib/propshaft.rb0000644000004100000410000000056315044375704016667 0ustar www-datawww-datarequire "active_support" require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/delegation" require "logger" module Propshaft mattr_accessor :logger, default: Logger.new(STDOUT) end require "propshaft/assembly" require "propshaft/errors" require "propshaft/helper" require "propshaft/railtie" if defined?(Rails::Railtie) propshaft-1.2.1/lib/propshaft/0000755000004100000410000000000015044375704016336 5ustar www-datawww-datapropshaft-1.2.1/lib/propshaft/railties/0000755000004100000410000000000015044375704020152 5ustar www-datawww-datapropshaft-1.2.1/lib/propshaft/railties/assets.rake0000644000004100000410000000213415044375704022320 0ustar www-datawww-datanamespace :assets do desc "Compile all the assets from config.assets.paths" task precompile: :environment do Rails.application.assets.processor.process if Rails.env.development? puts "Warning: You are precompiling assets in development. Rails will not " \ "serve any changed assets until you delete public#{Rails.application.config.assets.prefix}/.manifest.json" end end desc "Remove config.assets.output_path" task clobber: :environment do Rails.application.assets.processor.clobber end desc "Removes old files in config.assets.output_path" task :clean, [:count] => [:environment] do |_, args| count = args.fetch(:count, 2) Rails.application.assets.processor.clean(count.to_i) end desc "Print all the assets available in config.assets.paths" task reveal: :environment do puts Rails.application.assets.reveal(:logical_path).join("\n") end namespace :reveal do desc "Print the full path of assets available in config.assets.paths" task full: :environment do puts Rails.application.assets.reveal(:path).join("\n") end end end propshaft-1.2.1/lib/propshaft/server.rb0000644000004100000410000000254715044375704020201 0ustar www-datawww-datarequire "rack/utils" require "rack/version" class Propshaft::Server def initialize(assembly) @assembly = assembly end def call(env) execute_cache_sweeper_if_updated path, digest = extract_path_and_digest(env) if (asset = @assembly.load_path.find(path)) && asset.fresh?(digest) compiled_content = asset.compiled_content [ 200, { Rack::CONTENT_LENGTH => compiled_content.length.to_s, Rack::CONTENT_TYPE => asset.content_type.to_s, VARY => "Accept-Encoding", Rack::ETAG => "\"#{asset.digest}\"", Rack::CACHE_CONTROL => "public, max-age=31536000, immutable" }, [ compiled_content ] ] else [ 404, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "9" }, [ "Not found" ] ] end end def inspect self.class.inspect end private def extract_path_and_digest(env) full_path = Rack::Utils.unescape(env["PATH_INFO"].to_s.sub(/^\//, "")) Propshaft::Asset.extract_path_and_digest(full_path) end if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3") VARY = "Vary" else VARY = "vary" end def execute_cache_sweeper_if_updated if @assembly.config.sweep_cache @assembly.load_path.cache_sweeper.execute_if_updated end end end propshaft-1.2.1/lib/propshaft/manifest.rb0000644000004100000410000001262015044375704020472 0ustar www-datawww-datamodule Propshaft # Manages the manifest file that maps logical asset paths to their digested counterparts. # # The manifest is used to track assets that have been processed and digested, storing # their logical paths, digested paths, and optional integrity hashes. class Manifest # Represents a single entry in the asset manifest. # # Each entry contains information about an asset including its logical path # (the original path), digested path (the path with content hash), and # optional integrity hash for security verification. class ManifestEntry attr_reader :logical_path, :digested_path, :integrity # Creates a new manifest entry. # # ==== Parameters # # * +logical_path+ - The logical path of the asset # * +digested_path+ - The digested path of the asset # * +integrity+ - The integrity hash of the asset (optional) def initialize(logical_path:, digested_path:, integrity:) # :nodoc: @logical_path = logical_path @digested_path = digested_path @integrity = integrity end # Converts the manifest entry to a hash representation. # # Returns a hash containing the +digested_path+ and +integrity+ keys. def to_h { digested_path: digested_path, integrity: integrity} end end class << self # Creates a new Manifest instance from a manifest file. # # Reads and parses a manifest file, supporting both the current format # (with +digested_path+ and +integrity+ keys) and the legacy format # (simple string values for backwards compatibility). # # ==== Parameters # # * +manifest_path+ - The path to the manifest file # # ==== Returns # # A new manifest instance populated with entries from the file. def from_path(manifest_path) manifest = Manifest.new serialized_manifest = JSON.parse(manifest_path.read, symbolize_names: false) serialized_manifest.each_pair do |key, value| # Compatibility mode to be able to # read the old "simple manifest" format digested_path, integrity = if value.is_a?(String) [value, nil] else [value["digested_path"], value["integrity"]] end entry = ManifestEntry.new( logical_path: key, digested_path: digested_path, integrity: integrity ) manifest.push(entry) end manifest end end # Creates a new Manifest instance. # # ==== Parameters # # * +integrity_hash_algorithm+ - The algorithm to use for generating # integrity hashes (e.g., 'sha256', 'sha384', 'sha512'). If +nil+, integrity hashes # will not be generated. def initialize(integrity_hash_algorithm: nil) @integrity_hash_algorithm = integrity_hash_algorithm @entries = {} end # Adds an asset to the manifest. # # Creates a manifest entry from the given asset and adds it to the manifest. # The entry will include the asset's logical path, digested path, and optionally # an integrity hash if an integrity hash algorithm is configured. # # ==== Parameters # # * +asset+ - The asset to add to the manifest # # ==== Returns # # The manifest entry that was added. def push_asset(asset) entry = ManifestEntry.new( logical_path: asset.logical_path.to_s, digested_path: asset.digested_path.to_s, integrity: integrity_hash_algorithm && asset.integrity(hash_algorithm: integrity_hash_algorithm) ) push(entry) end # Adds a manifest entry to the manifest. # # ==== Parameters # # * +entry+ - The manifest entry to add # # ==== Returns # # The entry that was added. def push(entry) @entries[entry.logical_path] = entry end alias_method :<<, :push # Retrieves a manifest entry by its logical path. # # ==== Parameters # # * +logical_path+ - The logical path of the asset to retrieve # # ==== Returns # # The manifest entry, or +nil+ if not found. def [](logical_path) @entries[logical_path] end # Removes a manifest entry by its logical path. # # ==== Parameters # # * +logical_path+ - The logical path of the asset to remove # # ==== Returns # # The removed manifest entry, or +nil+ if not found. def delete(logical_path) @entries.delete(logical_path) end # Converts the manifest to JSON format. # # The JSON representation maps logical paths to hash representations of # manifest entries, containing +digested_path+ and +integrity+ information. # # ==== Returns # # The JSON representation of the manifest. def to_json @entries.transform_values do |manifest_entry| manifest_entry.to_h end.to_json end # Transforms the values of all manifest entries using the given block. # # This method is useful for applying transformations to all manifest entries # while preserving the logical path keys. # # ==== Parameters # # * +block+ - A block that will receive each manifest entry # # ==== Returns # # A new hash with the same keys but transformed values. def transform_values(&block) @entries.transform_values(&block) end private attr_reader :integrity_hash_algorithm end end propshaft-1.2.1/lib/propshaft/helper.rb0000644000004100000410000001453415044375704020151 0ustar www-datawww-datamodule Propshaft # Helper module that provides asset path resolution and integrity support for Rails applications. # # This module extends Rails' built-in asset helpers with additional functionality: # - Subresource Integrity (SRI) support for enhanced security # - Bulk stylesheet inclusion with :all and :app options # - Asset path resolution with proper error handling # # == Subresource Integrity (SRI) Support # # SRI helps protect against malicious modifications of assets by ensuring that # resources fetched from CDNs or other sources haven't been tampered with. # # SRI is automatically enabled in secure contexts (HTTPS or local development) # when the 'integrity' option is set to true: # # <%= stylesheet_link_tag "application", integrity: true %> # <%= javascript_include_tag "application", integrity: true %> # # This will generate integrity hashes and include them in the HTML: # # # # # == Bulk Stylesheet Inclusion # # The stylesheet_link_tag helper supports special symbols for bulk inclusion: # - :all - includes all CSS files found in the load path # - :app - includes only CSS files from app/assets/**/*.css # # <%= stylesheet_link_tag :all %> # All stylesheets # <%= stylesheet_link_tag :app %> # Only app stylesheets module Helper # Computes the Subresource Integrity (SRI) hash for the given asset path. # # This method generates a cryptographic hash of the asset content that can be used # to verify the integrity of the resource when it's loaded by the browser. # # asset_integrity("application.css") # # => "sha256-xyz789abcdef..." def asset_integrity(path, options = {}) path = _path_with_extname(path, options) Rails.application.assets.resolver.integrity(path) end # Resolves the full path for an asset, raising an error if not found. def compute_asset_path(path, options = {}) Rails.application.assets.resolver.resolve(path) || raise(MissingAssetError.new(path)) end # Enhanced +stylesheet_link_tag+ with integrity support and bulk inclusion options. # # In addition to the standard Rails functionality, this method supports: # * Automatic SRI (Subresource Integrity) hash generation in secure contexts # * Add an option to call +stylesheet_link_tag+ with +:all+ to include every css # file found on the load path or +:app+ to include css files found in # Rails.root("app/assets/**/*.css"), which will exclude lib/ and plugins. # # ==== Options # # * :integrity - Enable SRI hash generation # # ==== Examples # # stylesheet_link_tag "application", integrity: true # # => # # stylesheet_link_tag :all # All stylesheets in load path # stylesheet_link_tag :app # Only app/assets stylesheets def stylesheet_link_tag(*sources) options = sources.extract_options! case sources.first when :all sources = all_stylesheets_paths when :app sources = app_stylesheets_paths end _build_asset_tags(sources, options, :stylesheet) { |source, opts| super(source, opts) } end # Enhanced +javascript_include_tag+ with automatic SRI (Subresource Integrity) support. # # This method extends Rails' built-in +javascript_include_tag+ to automatically # generate and include integrity hashes when running in secure contexts. # # ==== Options # # * :integrity - Enable SRI hash generation # # ==== Examples # # javascript_include_tag "application", integrity: true # # => def javascript_include_tag(*sources) options = sources.extract_options! _build_asset_tags(sources, options, :javascript) { |source, opts| super(source, opts) } end # Returns a sorted and unique array of logical paths for all stylesheets in the load path. def all_stylesheets_paths Rails.application.assets.load_path.asset_paths_by_type("css") end # Returns a sorted and unique array of logical paths for all stylesheets in app/assets/**/*.css. def app_stylesheets_paths Rails.application.assets.load_path.asset_paths_by_glob("#{Rails.root.join("app/assets")}/**/*.css") end private # Core method that builds asset tags with optional integrity support. # # This method handles the common logic for both +stylesheet_link_tag+ and # +javascript_include_tag+, including SRI hash generation and HTML tag creation. def _build_asset_tags(sources, options, asset_type) options = options.stringify_keys integrity = _compute_integrity?(options) sources.map { |source| opts = integrity ? options.merge!('integrity' => asset_integrity(source, type: asset_type)) : options yield(source, opts) }.join("\n").html_safe end # Determines whether integrity hashes should be computed for assets. # # Integrity is only computed in secure contexts (HTTPS or local development) # and when explicitly requested via the +integrity+ option. def _compute_integrity?(options) if _secure_subresource_integrity_context? case options['integrity'] when nil, false, true options.delete('integrity') == true end else options.delete 'integrity' false end end # Checks if the current context is secure enough for Subresource Integrity. # # SRI is only beneficial in secure contexts. Returns true when: # * The request is made over HTTPS (SSL), OR # * The request is local (development environment) def _secure_subresource_integrity_context? respond_to?(:request) && self.request && (self.request.local? || self.request.ssl?) end # Ensures the asset path includes the appropriate file extension. def _path_with_extname(path, options) "#{path}#{compute_asset_extname(path, options)}" end end end propshaft-1.2.1/lib/propshaft/compiler/0000755000004100000410000000000015044375704020150 5ustar www-datawww-datapropshaft-1.2.1/lib/propshaft/compiler/css_asset_urls.rb0000644000004100000410000000300515044375704023527 0ustar www-datawww-data# frozen_string_literal: true require "propshaft/compiler" class Propshaft::Compiler::CssAssetUrls < Propshaft::Compiler ASSET_URL_PATTERN = /url\(\s*["']?(?!(?:\#|%23|data:|http:|https:|\/\/))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)/ def compile(asset, input) input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(asset.logical_path.dirname, $1), asset.logical_path, $2, $1 } end def referenced_by(asset, references: Set.new) asset.content.scan(ASSET_URL_PATTERN).each do |referenced_asset_url, _| referenced_asset = load_path.find(resolve_path(asset.logical_path.dirname, referenced_asset_url)) if referenced_asset && references.exclude?(referenced_asset) references << referenced_asset references.merge referenced_by(referenced_asset, references: references) end end references end private def resolve_path(directory, filename) if filename.start_with?("../") Pathname.new(directory + filename).relative_path_from("").to_s elsif filename.start_with?("/") filename.delete_prefix("/").to_s else (directory + filename.delete_prefix("./")).to_s end end def asset_url(resolved_path, logical_path, fingerprint, pattern) if asset = load_path.find(resolved_path) %[url("#{url_prefix}/#{asset.digested_path}#{fingerprint}")] else Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" %[url("#{pattern}")] end end end propshaft-1.2.1/lib/propshaft/compiler/js_asset_urls.rb0000644000004100000410000000301315044375704023352 0ustar www-datawww-data# frozen_string_literal: true require "propshaft/compiler" class Propshaft::Compiler::JsAssetUrls < Propshaft::Compiler ASSET_URL_PATTERN = %r{RAILS_ASSET_URL\(\s*["']?(?!(?:\#|%23|data|http|//))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)} def compile(asset, input) input.gsub(ASSET_URL_PATTERN) { asset_url(resolve_path(asset.logical_path.dirname, $1), asset.logical_path, $2, $1) } end def referenced_by(asset, references: Set.new) asset.content.scan(ASSET_URL_PATTERN).each do |referenced_asset_url, _| referenced_asset = load_path.find(resolve_path(asset.logical_path.dirname, referenced_asset_url)) if referenced_asset && references.exclude?(referenced_asset) references << referenced_asset references.merge referenced_by(referenced_asset, references: references) end end references end private def resolve_path(directory, filename) if filename.start_with?("../") Pathname.new(directory + filename).relative_path_from("").to_s elsif filename.start_with?("/") filename.delete_prefix("/").to_s else (directory + filename.delete_prefix("./")).to_s end end def asset_url(resolved_path, logical_path, fingerprint, pattern) asset = load_path.find(resolved_path) if asset %["#{url_prefix}/#{asset.digested_path}#{fingerprint}"] else Propshaft.logger.warn("Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}") %["#{pattern}"] end end end propshaft-1.2.1/lib/propshaft/compiler/source_mapping_urls.rb0000644000004100000410000000207315044375704024557 0ustar www-datawww-data# frozen_string_literal: true require "propshaft/compiler" class Propshaft::Compiler::SourceMappingUrls < Propshaft::Compiler SOURCE_MAPPING_PATTERN = %r{(//|/\*)# sourceMappingURL=(.+\.map)(\s*?\*\/)?\s*?\Z} def compile(asset, input) input.gsub(SOURCE_MAPPING_PATTERN) { source_mapping_url(asset.logical_path, asset_path($2, asset.logical_path), $1, $3) } end private def asset_path(source_mapping_url, logical_path) source_mapping_url.gsub!(/^(.+\/)?#{url_prefix}\//, "") if logical_path.dirname.to_s == "." source_mapping_url else logical_path.dirname.join(source_mapping_url).to_s end end def source_mapping_url(logical_path, resolved_path, comment_start, comment_end) if asset = load_path.find(resolved_path) "#{comment_start}# sourceMappingURL=#{url_prefix}/#{asset.digested_path}#{comment_end}" else Propshaft.logger.warn "Removed sourceMappingURL comment for missing asset '#{resolved_path}' from #{logical_path}" "#{comment_start}#{comment_end}" end end end propshaft-1.2.1/lib/propshaft/quiet_assets.rb0000644000004100000410000000045615044375704021401 0ustar www-datawww-dataclass Propshaft::QuietAssets def initialize(app) @app = app @assets_regex = %r(\A/{0,2}#{::Rails.application.config.assets.prefix}) end def call(env) if env['PATH_INFO'] =~ @assets_regex ::Rails.logger.silence { @app.call(env) } else @app.call(env) end end end propshaft-1.2.1/lib/propshaft/assembly.rb0000644000004100000410000000322415044375704020503 0ustar www-datawww-datarequire "propshaft/manifest" require "propshaft/load_path" require "propshaft/resolver/dynamic" require "propshaft/resolver/static" require "propshaft/server" require "propshaft/processor" require "propshaft/compilers" require "propshaft/compiler/css_asset_urls" require "propshaft/compiler/js_asset_urls" require "propshaft/compiler/source_mapping_urls" class Propshaft::Assembly attr_reader :config def initialize(config) @config = config end def load_path @load_path ||= Propshaft::LoadPath.new( config.paths, compilers: compilers, version: config.version, file_watcher: config.file_watcher, integrity_hash_algorithm: config.integrity_hash_algorithm ) end def resolver @resolver ||= if config.manifest_path.exist? Propshaft::Resolver::Static.new manifest_path: config.manifest_path, prefix: config.prefix else Propshaft::Resolver::Dynamic.new load_path: load_path, prefix: config.prefix end end def server Propshaft::Server.new(self) end def processor Propshaft::Processor.new \ load_path: load_path, output_path: config.output_path, compilers: compilers, manifest_path: config.manifest_path end def compilers @compilers ||= Propshaft::Compilers.new(self).tap do |compilers| Array(config.compilers).each do |(mime_type, klass)| compilers.register mime_type, klass end end end def reveal(path_type = :logical_path) path_type = path_type.presence_in(%i[ logical_path path ]) || raise(ArgumentError, "Unknown path_type: #{path_type}") load_path.assets.collect do |asset| asset.send(path_type) end end end propshaft-1.2.1/lib/propshaft/asset.rb0000644000004100000410000000410615044375704020003 0ustar www-datawww-datarequire "digest/sha1" require "digest/sha2" require "action_dispatch/http/mime_type" class Propshaft::Asset attr_reader :path, :logical_path, :load_path class << self def extract_path_and_digest(digested_path) digest = digested_path[/-([0-9a-zA-Z]{7,128})\.(?!digested)([^.]|.map)+\z/, 1] path = digest ? digested_path.sub("-#{digest}", "") : digested_path [path, digest] end end def initialize(path, logical_path:, load_path:) @path, @logical_path, @load_path = path, Pathname.new(logical_path), load_path end def compiled_content @compiled_content ||= load_path.compilers.compile(self) end def content(encoding: "ASCII-8BIT") File.read(path, encoding: encoding, mode: "rb") end def content_type Mime::Type.lookup_by_extension(logical_path.extname.from(1)) end def length content.size end def digest @digest ||= Digest::SHA1.hexdigest("#{content_with_compile_references}#{load_path.version}").first(8) end def integrity(hash_algorithm:) # Following the Subresource Integrity spec draft # https://w3c.github.io/webappsec-subresource-integrity/ # allowing only sha256, sha384, and sha512 bitlen = case hash_algorithm when "sha256" 256 when "sha384" 384 when "sha512" 512 else raise(StandardError.new("Subresource Integrity hash algorithm must be one of SHA2 family (sha256, sha384, sha512)")) end [hash_algorithm, Digest::SHA2.new(bitlen).base64digest(compiled_content)].join("-") end def digested_path if already_digested? logical_path else logical_path.sub(/\.(\w+(\.map)?)$/) { |ext| "-#{digest}#{ext}" } end end def fresh?(digest) self.digest == digest || already_digested? end def ==(other_asset) logical_path.hash == other_asset.logical_path.hash end private def content_with_compile_references content + load_path.find_referenced_by(self).collect(&:content).join end def already_digested? logical_path.to_s =~ /-([0-9a-zA-Z_-]{7,128})\.digested/ end end propshaft-1.2.1/lib/propshaft/compiler.rb0000644000004100000410000000102115044375704020467 0ustar www-datawww-data# frozen_string_literal: true # Base compiler from which other compilers can inherit class Propshaft::Compiler attr_reader :assembly delegate :config, :load_path, to: :assembly def initialize(assembly) @assembly = assembly end # Override this in a specific compiler def compile(asset, input) raise NotImplementedError end def referenced_by(asset) Set.new end private def url_prefix @url_prefix ||= File.join(config.relative_url_root.to_s, config.prefix.to_s).chomp("/") end end propshaft-1.2.1/lib/propshaft/resolver/0000755000004100000410000000000015044375704020177 5ustar www-datawww-datapropshaft-1.2.1/lib/propshaft/resolver/static.rb0000644000004100000410000000153615044375704022020 0ustar www-datawww-datamodule Propshaft::Resolver class Static attr_reader :manifest_path, :prefix def initialize(manifest_path:, prefix:) @manifest_path, @prefix = manifest_path, prefix end def resolve(logical_path) if asset_path = digested_path(logical_path) File.join prefix, asset_path end end def integrity(logical_path) entry = manifest[logical_path] entry&.integrity end def read(logical_path, encoding: "ASCII-8BIT") if asset_path = digested_path(logical_path) File.read(manifest_path.dirname.join(asset_path), encoding: encoding) end end private def manifest @manifest ||= Propshaft::Manifest.from_path(manifest_path) end def digested_path(logical_path) entry = manifest[logical_path] entry&.digested_path end end end propshaft-1.2.1/lib/propshaft/resolver/dynamic.rb0000644000004100000410000000141715044375704022153 0ustar www-datawww-datamodule Propshaft::Resolver class Dynamic attr_reader :load_path, :prefix def initialize(load_path:, prefix:) @load_path, @prefix = load_path, prefix end def resolve(logical_path) if asset = find_asset(logical_path) File.join prefix, asset.digested_path end end def integrity(logical_path) hash_algorithm = load_path.integrity_hash_algorithm if hash_algorithm && (asset = find_asset(logical_path)) asset.integrity(hash_algorithm: hash_algorithm) end end def read(logical_path, options = {}) if asset = load_path.find(logical_path) asset.content(**options) end end private def find_asset(logical_path) load_path.find(logical_path) end end end propshaft-1.2.1/lib/propshaft/version.rb0000644000004100000410000000005115044375704020344 0ustar www-datawww-datamodule Propshaft VERSION = "1.2.1" end propshaft-1.2.1/lib/propshaft/compilers.rb0000644000004100000410000000176215044375704020666 0ustar www-datawww-dataclass Propshaft::Compilers attr_reader :registrations, :assembly def initialize(assembly) @assembly = assembly @registrations = Hash.new end def register(mime_type, klass) registrations[mime_type] ||= [] registrations[mime_type] << klass end def any? registrations.any? end def compilable?(asset) registrations[asset.content_type.to_s].present? end def compile(asset) if relevant_registrations = registrations[asset.content_type.to_s] asset.content.dup.tap do |input| relevant_registrations.each do |compiler| input.replace compiler.new(assembly).compile(asset, input) end end else asset.content end end def referenced_by(asset) Set.new.tap do |references| if relevant_registrations = registrations[asset.content_type.to_s] relevant_registrations.each do |compiler| references.merge compiler.new(assembly).referenced_by(asset) end end end end end propshaft-1.2.1/lib/propshaft/load_path.rb0000644000004100000410000000632015044375704020617 0ustar www-datawww-datarequire "propshaft/manifest" require "propshaft/asset" class Propshaft::LoadPath class NullFileWatcher # :nodoc: def initialize(paths, files_to_watch, &block) @block = block end def execute_if_updated @block.call end end attr_reader :paths, :compilers, :version, :integrity_hash_algorithm def initialize(paths = [], compilers:, version: nil, file_watcher: nil, integrity_hash_algorithm: nil) @paths, @compilers, @version, @integrity_hash_algorithm = dedup(paths), compilers, version, integrity_hash_algorithm @file_watcher = file_watcher || NullFileWatcher end def find(asset_name) assets_by_path[asset_name] end def find_referenced_by(asset) compilers.referenced_by(asset).delete(self) end def assets assets_by_path.values end def asset_paths_by_type(content_type) (@cached_asset_paths_by_type ||= Hash.new)[content_type] ||= extract_logical_paths_from(assets.select { |a| a.content_type == Mime::EXTENSION_LOOKUP[content_type] }) end def asset_paths_by_glob(glob) (@cached_asset_paths_by_glob ||= Hash.new)[glob] ||= extract_logical_paths_from(assets.select { |a| a.path.fnmatch?(glob) }) end def manifest Propshaft::Manifest.new(integrity_hash_algorithm: integrity_hash_algorithm).tap do |manifest| assets.each { |asset| manifest.push_asset(asset) } end end # Returns a file watcher object configured to clear the cache of the load_path # when the directories passed during its initialization have changes. This is used in development # and test to ensure the map caches are reset when javascript files are changed. def cache_sweeper @cache_sweeper ||= begin exts_to_watch = Mime::EXTENSION_LOOKUP.map(&:first) files_to_watch = Array(paths).collect { |dir| [ dir.to_s, exts_to_watch ] }.to_h mutex = Mutex.new @file_watcher.new([], files_to_watch) do mutex.synchronize do clear_cache seed_cache end end end end private def assets_by_path @cached_assets_by_path ||= Hash.new.tap do |mapped| paths.each do |path| without_dotfiles(all_files_from_tree(path)).each do |file| logical_path = file.relative_path_from(path) mapped[logical_path.to_s] ||= Propshaft::Asset.new(file, logical_path: logical_path, load_path: self) end if path.exist? end end end def all_files_from_tree(path) path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child } end def extract_logical_paths_from(assets) assets.collect { |asset| asset.logical_path.to_s }.sort end def without_dotfiles(files) files.reject { |file| file.basename.to_s.starts_with?(".") } end def clear_cache @cached_assets_by_path = nil @cached_asset_paths_by_type = nil @cached_asset_paths_by_glob = nil end def seed_cache assets_by_path end def dedup(paths) paths = Array(paths).map { |path| Pathname.new(path) } deduped = [].tap do |deduped| paths.sort.each { |path| deduped << path if deduped.blank? || !path.to_s.start_with?(deduped.last.to_s) } end paths & deduped end end propshaft-1.2.1/lib/propshaft/errors.rb0000644000004100000410000000060415044375704020177 0ustar www-datawww-data# frozen_string_literal: true module Propshaft # Generic base class for all Propshaft exceptions. class Error < StandardError; end # Raised when LoadPath cannot find the requested asset class MissingAssetError < Error def initialize(path) super @path = path end def message "The asset '#{@path}' was not found in the load path." end end end propshaft-1.2.1/lib/propshaft/output_path.rb0000644000004100000410000000301315044375704021234 0ustar www-datawww-datarequire "propshaft/asset" class Propshaft::OutputPath attr_reader :path, :manifest def initialize(path, manifest) @path, @manifest = path, manifest end def clean(count, age) asset_versions = files.group_by { |_, attrs| attrs[:logical_path] } asset_versions.each do |logical_path, versions| current = manifest[logical_path] versions .reject { |path, _| current && path == current } .sort_by { |_, attrs| attrs[:mtime] } .reverse .each_with_index .drop_while { |(_, attrs), index| fresh_version_within_limit(attrs[:mtime], count, expires_at: age, limit: index) } .each { |(path, _), _| remove(path) } end end def files Hash.new.tap do |files| all_files_from_tree(path).each do |file| digested_path = file.relative_path_from(path) logical_path, digest = Propshaft::Asset.extract_path_and_digest(digested_path.to_s) files[digested_path.to_s] = { logical_path: logical_path.to_s, digest: digest, mtime: File.mtime(file) } end end end private def fresh_version_within_limit(mtime, count, expires_at:, limit:) modified_at = [ 0, Time.now - mtime ].max modified_at < expires_at || limit < count end def remove(path) FileUtils.rm(@path.join(path)) Propshaft.logger.info "Removed #{path}" end def all_files_from_tree(path) path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child } end end propshaft-1.2.1/lib/propshaft/railtie.rb0000644000004100000410000000635115044375704020321 0ustar www-datawww-datarequire "rails" require "active_support/ordered_options" require "propshaft/quiet_assets" module Propshaft class Railtie < ::Rails::Railtie config.assets = ActiveSupport::OrderedOptions.new config.assets.paths = [] config.assets.excluded_paths = [] config.assets.version = "1" config.assets.prefix = "/assets" config.assets.quiet = false config.assets.compilers = [ [ "text/css", Propshaft::Compiler::CssAssetUrls ], [ "text/css", Propshaft::Compiler::SourceMappingUrls ], [ "text/javascript", Propshaft::Compiler::JsAssetUrls ], [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ], ] config.assets.sweep_cache = Rails.env.development? config.assets.server = Rails.env.development? || Rails.env.test? config.assets.relative_url_root = nil # Register propshaft initializer to copy the assets path in all the Rails Engines. # This makes possible for us to keep all `assets` config in this Railtie, but still # allow engines to automatically register their own paths. Rails::Engine.initializer "propshaft.append_assets_path", group: :all do |app| app.config.assets.paths.unshift(*paths["vendor/assets"].existent_directories) app.config.assets.paths.unshift(*paths["lib/assets"].existent_directories) app.config.assets.paths.unshift(*paths["app/assets"].existent_directories) app.config.assets.paths = app.config.assets.paths.without(Array(app.config.assets.excluded_paths).collect(&:to_s)) end config.after_initialize do |app| # Prioritize assets from within the application over assets of the same path from engines/gems. config.assets.paths.sort_by!.with_index { |path, i| [path.to_s.start_with?(Rails.root.to_s) ? 0 : 1, i] } config.assets.file_watcher ||= app.config.file_watcher config.assets.relative_url_root ||= app.config.relative_url_root config.assets.output_path ||= Pathname.new(File.join(app.config.paths["public"].first, app.config.assets.prefix)) config.assets.manifest_path ||= config.assets.output_path.join(".manifest.json") app.assets = Propshaft::Assembly.new(app.config.assets) if config.assets.server app.routes.prepend do mount app.assets.server, at: app.assets.config.prefix end end ActiveSupport.on_load(:action_view) do include Propshaft::Helper end if config.assets.sweep_cache ActiveSupport.on_load(:action_controller_base) do before_action { Rails.application.assets.load_path.cache_sweeper.execute_if_updated } end end end initializer "propshaft.logger" do Propshaft.logger = config.assets.logger || Rails.logger end initializer :quiet_assets do |app| if app.config.assets.quiet app.middleware.insert_before ::Rails::Rack::Logger, Propshaft::QuietAssets end end rake_tasks do load "propshaft/railties/assets.rake" end # Compatibility shiming (need to provide log warnings when used) config.assets.precompile = [] config.assets.debug = nil config.assets.compile = nil config.assets.css_compressor = nil config.assets.js_compressor = nil end end propshaft-1.2.1/lib/propshaft/processor.rb0000644000004100000410000000341015044375704020700 0ustar www-datawww-datarequire "propshaft/output_path" class Propshaft::Processor attr_reader :load_path, :output_path, :compilers, :manifest_path def initialize(load_path:, output_path:, compilers:, manifest_path:) @load_path, @output_path = load_path, output_path @manifest_path = manifest_path @compilers = compilers end def process ensure_output_path_exists write_manifest output_assets end def clobber FileUtils.rm_r(output_path) if File.exist?(output_path) end def clean(count) Propshaft::OutputPath.new(output_path, load_path.manifest).clean(count, 1.hour) end private def ensure_output_path_exists FileUtils.mkdir_p output_path end def write_manifest FileUtils.mkdir_p(File.dirname(manifest_path)) File.open(manifest_path, "wb+") do |manifest| manifest.write load_path.manifest.to_json end end def output_assets load_path.assets.each do |asset| unless output_path.join(asset.digested_path).exist? Propshaft.logger.info "Writing #{asset.digested_path}" FileUtils.mkdir_p output_path.join(asset.digested_path.parent) output_asset(asset) end end end def output_asset(asset) compile_asset(asset) || copy_asset(asset) end def compile_asset(asset) File.open(output_path.join(asset.digested_path), "w+") do |file| begin file.write asset.compiled_content rescue Encoding::UndefinedConversionError # FIXME: Not sure if there's a better way here? file.write asset.compiled_content.force_encoding("UTF-8") end end if compilers.compilable?(asset) end def copy_asset(asset) FileUtils.copy asset.path, output_path.join(asset.digested_path) end end propshaft-1.2.1/Rakefile0000644000004100000410000000034115044375704015225 0ustar www-datawww-datarequire "bundler/setup" require "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new do |test| test.libs << "test" test.test_files = FileList["test/**/*_test.rb"] test.warning = true end task default: :test propshaft-1.2.1/MIT-LICENSE0000644000004100000410000000203415044375704015215 0ustar www-datawww-dataCopyright (c) 2021 Basecamp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. propshaft-1.2.1/README.md0000644000004100000410000001622615044375704015050 0ustar www-datawww-data# Propshaft Propshaft is an asset pipeline library for Rails. It's built for an era where bundling assets to save on HTTP connections is no longer urgent, where JavaScript and CSS are either compiled by dedicated Node.js bundlers or served directly to the browsers, and where increases in bandwidth have made the need for minification less pressing. These factors allow for a dramatically simpler and faster asset pipeline compared to previous options, like [Sprockets](https://github.com/rails/sprockets-rails). So that's what Propshaft doesn't do. Here's what it does provide: 1. **Configurable load path**: You can register directories from multiple places in your app and gems, and reference assets from all of these paths as though they were one. 1. **Digest stamping**: All assets in the load path will be copied (or compiled) in a precompilation step for production that also stamps all of them with a digest hash, so you can use long-expiry cache headers for better performance. The digested assets can be referred to through their logical path because the processing leaves a manifest file that provides a way to translate. 1. **Development server**: There's no need to precompile the assets in development. You can refer to them via the same asset_path helpers and they'll be served by a development server. 1. **Basic compilers**: Propshaft was explicitly not designed to provide full transpiler capabilities. You can get that better elsewhere. But it does offer a simple input->output compiler setup that by default is used to translate `url(asset)` function calls in CSS to `url(digested-asset)` instead and source mapping comments likewise. ## Installation With Rails 8, Propshaft is the default asset pipeline for new applications. With Rails 7, you can start a new application with propshaft using `rails new myapp -a propshaft`. For existing applications, check the [upgrade guide](https://github.com/rails/propshaft/blob/main/UPGRADING.md) which contains step-by-step instructions. ## Usage Propshaft makes all the assets from all the paths it's been configured with through `config.assets.paths` available for serving and will copy all of them into `public/assets` when precompiling. This is unlike Sprockets, which did not copy over assets that hadn't been explicitly included in one of the bundled assets. You can however exempt directories that have been added through the `config.assets.excluded_paths`. This is useful if you're for example using `app/assets/stylesheets` exclusively as a set of inputs to a compiler like Dart Sass for Rails, and you don't want these input files to be part of the load path. (Remember you need to add full paths, like `Rails.root.join("app/assets/stylesheets")`). These assets can be referenced through their logical path using the normal helpers like `asset_path`, `image_tag`, `javascript_include_tag`, and all the other asset helper tags. These logical references are automatically converted into digest-aware paths in production when `assets:precompile` has been run (through a JSON mapping file found in `public/assets/.manifest.json`). ## Referencing digested assets in CSS and JavaScript Propshaft will automatically convert asset references in CSS to use the digested file names. So `background: url("/bg/pattern.svg")` is converted to `background: url("/assets/bg/pattern-2169cbef.svg")` before the stylesheet is served. For JavaScript, you'll have to manually trigger this transformation by using the `RAILS_ASSET_URL` pseudo-method. It's used like this: ```javascript export default class extends Controller { init() { this.img = RAILS_ASSET_URL("/icons/trash.svg") } } ``` That'll turn into: ```javascript export default class extends Controller { init() { this.img = "/assets/icons/trash-54g9cbef.svg" } } ``` ## Bypassing the digest step If you need to put multiple files that refer to each other through Propshaft, like a JavaScript file and its source map, you have to digest these files in advance to retain stable file names. Propshaft looks for the specific pattern of `-[digest].digested.js` as the postfix to any asset file as an indication that the file has already been digested. ## Subresource Integrity (SRI) Propshaft supports Subresource Integrity (SRI) to help protect against malicious modifications of assets. SRI allows browsers to verify that resources fetched from CDNs or other sources haven't been tampered with by checking cryptographic hashes. ### Enabling SRI To enable SRI support, configure the hash algorithm in your Rails application: ```ruby config.assets.integrity_hash_algorithm = "sha384" ``` Valid hash algorithms include: - `"sha256"` - SHA-256 (most common) - `"sha384"` - SHA-384 (recommended for enhanced security) - `"sha512"` - SHA-512 (strongest) ### Using SRI in your views Once configured, you can enable SRI by passing the `integrity: true` option to asset helpers: ```erb <%= stylesheet_link_tag "application", integrity: true %> <%= javascript_include_tag "application", integrity: true %> ``` This generates HTML with integrity hashes: ```html ``` **Important**: SRI only works in secure contexts (HTTPS) or during local development. The integrity hashes are automatically omitted when serving over HTTP in production for security reasons. ### Bulk stylesheet inclusion with SRI Propshaft extends `stylesheet_link_tag` with special symbols for bulk inclusion: ```erb <%= stylesheet_link_tag :all, integrity: true %> <%= stylesheet_link_tag :app, integrity: true %> ``` ## Improving performance in development Before every request Propshaft checks if any asset was updated to decide if a cache sweep is needed. This verification is done using the application's configured file watcher which, by default, is `ActiveSupport::FileUpdateChecker`. If you have a lot of assets in your project, you can improve performance by adding the `listen` gem to the development group in your Gemfile, and this line to the `development.rb` environment file: ```ruby config.file_watcher = ActiveSupport::EventedFileUpdateChecker ``` ## Migrating from Sprockets Propshaft does a lot less than Sprockets, by design, so it might well be a fair bit of work to migrate if it's even desirable. This is particularly true if you rely on Sprockets to provide any form of transpiling, like CoffeeScript or Sass, or if you rely on any gems that do. You'll need to either stop transpiling or use a Node-based transpiler, like those in [`jsbundling-rails`](https://github.com/rails/jsbundling-rails) and [`cssbundling-rails`](https://github.com/rails/cssbundling-rails). On the other hand, if you're already bundling JavaScript and CSS through a Node-based setup, then Propshaft is going to slot in easily. Since you don't need another tool to bundle or transpile. Just to digest and serve. But for greenfield apps using the default import-map approach, Propshaft can also work well, if you're able to deal with vanilla CSS. ## License Propshaft is released under the [MIT License](https://opensource.org/licenses/MIT).