propshaft-1.1.0/0000755000004100000410000000000014751202523013550 5ustar www-datawww-datapropshaft-1.1.0/propshaft.gemspec0000644000004100000410000000452114751202523017125 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: propshaft 1.1.0 ruby lib Gem::Specification.new do |s| s.name = "propshaft".freeze s.version = "1.1.0" 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 = "2024-09-30" 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/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"]) s.add_runtime_dependency(%q.freeze, [">= 7.0.0"]) else s.add_dependency(%q.freeze, [">= 7.0.0"]) s.add_dependency(%q.freeze, [">= 7.0.0"]) s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, [">= 7.0.0"]) end end propshaft-1.1.0/lib/0000755000004100000410000000000014751202523014316 5ustar www-datawww-datapropshaft-1.1.0/lib/propshaft.rb0000644000004100000410000000052714751202523016655 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" propshaft-1.1.0/lib/propshaft/0000755000004100000410000000000014751202523016324 5ustar www-datawww-datapropshaft-1.1.0/lib/propshaft/railties/0000755000004100000410000000000014751202523020140 5ustar www-datawww-datapropshaft-1.1.0/lib/propshaft/railties/assets.rake0000644000004100000410000000213414751202523022306 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.1.0/lib/propshaft/server.rb0000644000004100000410000000224614751202523020163 0ustar www-datawww-datarequire "rack/utils" require "rack/version" class Propshaft::Server def initialize(assembly) @assembly = assembly end def call(env) path, digest = extract_path_and_digest(env) if (asset = @assembly.load_path.find(path)) && asset.fresh?(digest) compiled_content = @assembly.compilers.compile(asset) [ 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 end propshaft-1.1.0/lib/propshaft/helper.rb0000644000004100000410000000213714751202523020133 0ustar www-datawww-datamodule Propshaft module Helper def compute_asset_path(path, options = {}) Rails.application.assets.resolver.resolve(path) || raise(MissingAssetError.new(path)) end # 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. def stylesheet_link_tag(*sources, **options) case sources.first when :all super(*all_stylesheets_paths , **options) when :app super(*app_stylesheets_paths , **options) else super end 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 end end propshaft-1.1.0/lib/propshaft/compiler/0000755000004100000410000000000014751202523020136 5ustar www-datawww-datapropshaft-1.1.0/lib/propshaft/compiler/css_asset_urls.rb0000644000004100000410000000277414751202523023531 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|\/\/))([^"'\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.1.0/lib/propshaft/compiler/js_asset_urls.rb0000644000004100000410000000301314751202523023340 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.1.0/lib/propshaft/compiler/source_mapping_urls.rb0000644000004100000410000000207314751202523024545 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.1.0/lib/propshaft/quiet_assets.rb0000644000004100000410000000045614751202523021367 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.1.0/lib/propshaft/assembly.rb0000644000004100000410000000277114751202523020477 0ustar www-datawww-datarequire "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) 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.1.0/lib/propshaft/asset.rb0000644000004100000410000000262514751202523017775 0ustar www-datawww-datarequire "digest/sha1" 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 content(encoding: "ASCII-8BIT") File.read(path, encoding: encoding) 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 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.1.0/lib/propshaft/compiler.rb0000644000004100000410000000102114751202523020455 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.1.0/lib/propshaft/resolver/0000755000004100000410000000000014751202523020165 5ustar www-datawww-datapropshaft-1.1.0/lib/propshaft/resolver/static.rb0000644000004100000410000000123614751202523022003 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 = parsed_manifest[logical_path] File.join prefix, asset_path end end def read(logical_path, encoding: "ASCII-8BIT") if asset_path = parsed_manifest[logical_path] File.read(manifest_path.dirname.join(asset_path), encoding: encoding) end end private def parsed_manifest @parsed_manifest ||= JSON.parse(manifest_path.read, symbolize_names: false) end end end propshaft-1.1.0/lib/propshaft/resolver/dynamic.rb0000644000004100000410000000072014751202523022135 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 = load_path.find(logical_path) File.join prefix, asset.digested_path end end def read(logical_path, options = {}) if asset = load_path.find(logical_path) asset.content(**options) end end end end propshaft-1.1.0/lib/propshaft/version.rb0000644000004100000410000000005114751202523020332 0ustar www-datawww-datamodule Propshaft VERSION = "1.1.0" end propshaft-1.1.0/lib/propshaft/compilers.rb0000644000004100000410000000176214751202523020654 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.1.0/lib/propshaft/load_path.rb0000644000004100000410000000552714751202523020615 0ustar www-datawww-datarequire "propshaft/asset" class Propshaft::LoadPath attr_reader :paths, :compilers, :version def initialize(paths = [], compilers:, version: nil) @paths, @compilers, @version = dedup(paths), compilers, version 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 Hash.new.tap do |manifest| assets.each do |asset| manifest[asset.logical_path.to_s] = asset.digested_path.to_s end 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 Rails.application.config.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.1.0/lib/propshaft/errors.rb0000644000004100000410000000060414751202523020165 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.1.0/lib/propshaft/output_path.rb0000644000004100000410000000301314751202523021222 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.1.0/lib/propshaft/railtie.rb0000644000004100000410000000625314751202523020310 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.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.1.0/lib/propshaft/processor.rb0000644000004100000410000000341414751202523020672 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 compilers.compile(asset) rescue Encoding::UndefinedConversionError # FIXME: Not sure if there's a better way here? file.write compilers.compile(asset).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.1.0/Rakefile0000644000004100000410000000034114751202523015213 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.1.0/MIT-LICENSE0000644000004100000410000000203414751202523015203 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.1.0/README.md0000644000004100000410000001313714751202523015034 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. ## 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).