propshaft 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 963ec537db8637fe43844f1b46e290c600c04b5f8b170c79583d4370dbcefbbf
4
- data.tar.gz: c6f48c154b847a715317ba44d041287f82f009ee760b9b9d8e7a319c7f354ff2
3
+ metadata.gz: 8ea8c536e8ee4cd0229dd706a6689d8e52d13a63533fb1f2d5e19835f3a853f2
4
+ data.tar.gz: fdd5faee6f39306a3bd12f0d3b008b31554b2576637f14497640dd886d38d865
5
5
  SHA512:
6
- metadata.gz: 5e08613dd985ef975477b9a1886ffa5def8aef4889c6d4cbfd585d716c711e5806265d52370c819642ebcade755062c08358809823bbb4bc4ecece4892dbe3b5
7
- data.tar.gz: 16f7245a2a4c9aa69f066a566e9781a1cb1327141b6daf865fd6917f3b1d5eaec560f5ee1cf8600c484e43f60d3acea55fca4b8262dd8ec88ef714839a57ee61
6
+ metadata.gz: 684f7db0156395a7376f7f1788576a7f5bbfaef1ccbe44af0fb2bf2ce315c6acdc45e2f5e368df3d580e6e54c741a80d50bd7cb0db6976909662b40a65184f0c
7
+ data.tar.gz: ad626da00dd0f678abe3d5ea4ce26975c38e02906fdc14de6aa244cdb0902382dd25caf495e9d32e0da3d89ad9b260dce2bdf992a1f925a65fdaeea0494bf355
data/README.md CHANGED
@@ -16,7 +16,7 @@ With Rails 8, Propshaft is the default asset pipeline for new applications. With
16
16
 
17
17
  ## Usage
18
18
 
19
- 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.
19
+ 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.
20
20
 
21
21
  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")`).
22
22
 
@@ -50,9 +50,55 @@ export default class extends Controller {
50
50
 
51
51
  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.
52
52
 
53
+ ## Subresource Integrity (SRI)
54
+
55
+ 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.
56
+
57
+ ### Enabling SRI
58
+
59
+ To enable SRI support, configure the hash algorithm in your Rails application:
60
+
61
+ ```ruby
62
+ config.assets.integrity_hash_algorithm = "sha384"
63
+ ```
64
+
65
+ Valid hash algorithms include:
66
+ - `"sha256"` - SHA-256 (most common)
67
+ - `"sha384"` - SHA-384 (recommended for enhanced security)
68
+ - `"sha512"` - SHA-512 (strongest)
69
+
70
+ ### Using SRI in your views
71
+
72
+ Once configured, you can enable SRI by passing the `integrity: true` option to asset helpers:
73
+
74
+ ```erb
75
+ <%= stylesheet_link_tag "application", integrity: true %>
76
+ <%= javascript_include_tag "application", integrity: true %>
77
+ ```
78
+
79
+ This generates HTML with integrity hashes:
80
+
81
+ ```html
82
+ <link rel="stylesheet" href="/assets/application-abc123.css"
83
+ integrity="sha384-xyz789...">
84
+ <script src="/assets/application-def456.js"
85
+ integrity="sha384-uvw012..."></script>
86
+ ```
87
+
88
+ **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.
89
+
90
+ ### Bulk stylesheet inclusion with SRI
91
+
92
+ Propshaft extends `stylesheet_link_tag` with special symbols for bulk inclusion:
93
+
94
+ ```erb
95
+ <%= stylesheet_link_tag :all, integrity: true %> <!-- All stylesheets -->
96
+ <%= stylesheet_link_tag :app, integrity: true %> <!-- Only app/assets stylesheets -->
97
+ ```
98
+
53
99
  ## Improving performance in development
54
100
 
55
- 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`.
101
+ 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`.
56
102
 
57
103
  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:
58
104
 
@@ -1,3 +1,4 @@
1
+ require "propshaft/manifest"
1
2
  require "propshaft/load_path"
2
3
  require "propshaft/resolver/dynamic"
3
4
  require "propshaft/resolver/static"
@@ -16,7 +17,13 @@ class Propshaft::Assembly
16
17
  end
17
18
 
18
19
  def load_path
19
- @load_path ||= Propshaft::LoadPath.new(config.paths, compilers: compilers, version: config.version)
20
+ @load_path ||= Propshaft::LoadPath.new(
21
+ config.paths,
22
+ compilers: compilers,
23
+ version: config.version,
24
+ file_watcher: config.file_watcher,
25
+ integrity_hash_algorithm: config.integrity_hash_algorithm
26
+ )
20
27
  end
21
28
 
22
29
  def resolver
@@ -47,7 +54,7 @@ class Propshaft::Assembly
47
54
 
48
55
  def reveal(path_type = :logical_path)
49
56
  path_type = path_type.presence_in(%i[ logical_path path ]) || raise(ArgumentError, "Unknown path_type: #{path_type}")
50
-
57
+
51
58
  load_path.assets.collect do |asset|
52
59
  asset.send(path_type)
53
60
  end
@@ -1,4 +1,5 @@
1
1
  require "digest/sha1"
2
+ require "digest/sha2"
2
3
  require "action_dispatch/http/mime_type"
3
4
 
4
5
  class Propshaft::Asset
@@ -17,8 +18,12 @@ class Propshaft::Asset
17
18
  @path, @logical_path, @load_path = path, Pathname.new(logical_path), load_path
18
19
  end
19
20
 
21
+ def compiled_content
22
+ @compiled_content ||= load_path.compilers.compile(self)
23
+ end
24
+
20
25
  def content(encoding: "ASCII-8BIT")
21
- File.read(path, encoding: encoding)
26
+ File.read(path, encoding: encoding, mode: "rb")
22
27
  end
23
28
 
24
29
  def content_type
@@ -33,6 +38,24 @@ class Propshaft::Asset
33
38
  @digest ||= Digest::SHA1.hexdigest("#{content_with_compile_references}#{load_path.version}").first(8)
34
39
  end
35
40
 
41
+ def integrity(hash_algorithm:)
42
+ # Following the Subresource Integrity spec draft
43
+ # https://w3c.github.io/webappsec-subresource-integrity/
44
+ # allowing only sha256, sha384, and sha512
45
+ bitlen = case hash_algorithm
46
+ when "sha256"
47
+ 256
48
+ when "sha384"
49
+ 384
50
+ when "sha512"
51
+ 512
52
+ else
53
+ raise(StandardError.new("Subresource Integrity hash algorithm must be one of SHA2 family (sha256, sha384, sha512)"))
54
+ end
55
+
56
+ [hash_algorithm, Digest::SHA2.new(bitlen).base64digest(compiled_content)].join("-")
57
+ end
58
+
36
59
  def digested_path
37
60
  if already_digested?
38
61
  logical_path
@@ -3,7 +3,7 @@
3
3
  require "propshaft/compiler"
4
4
 
5
5
  class Propshaft::Compiler::CssAssetUrls < Propshaft::Compiler
6
- ASSET_URL_PATTERN = /url\(\s*["']?(?!(?:\#|%23|data|http|\/\/))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)/
6
+ ASSET_URL_PATTERN = /url\(\s*["']?(?!(?:\#|%23|data:|http:|https:|\/\/))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)/
7
7
 
8
8
  def compile(asset, input)
9
9
  input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(asset.logical_path.dirname, $1), asset.logical_path, $2, $1 }
@@ -1,20 +1,102 @@
1
1
  module Propshaft
2
+ # Helper module that provides asset path resolution and integrity support for Rails applications.
3
+ #
4
+ # This module extends Rails' built-in asset helpers with additional functionality:
5
+ # - Subresource Integrity (SRI) support for enhanced security
6
+ # - Bulk stylesheet inclusion with :all and :app options
7
+ # - Asset path resolution with proper error handling
8
+ #
9
+ # == Subresource Integrity (SRI) Support
10
+ #
11
+ # SRI helps protect against malicious modifications of assets by ensuring that
12
+ # resources fetched from CDNs or other sources haven't been tampered with.
13
+ #
14
+ # SRI is automatically enabled in secure contexts (HTTPS or local development)
15
+ # when the 'integrity' option is set to true:
16
+ #
17
+ # <%= stylesheet_link_tag "application", integrity: true %>
18
+ # <%= javascript_include_tag "application", integrity: true %>
19
+ #
20
+ # This will generate integrity hashes and include them in the HTML:
21
+ #
22
+ # <link rel="stylesheet" href="/assets/application-abc123.css"
23
+ # integrity="sha256-xyz789...">
24
+ # <script src="/assets/application-def456.js"
25
+ # integrity="sha256-uvw012..."></script>
26
+ #
27
+ # == Bulk Stylesheet Inclusion
28
+ #
29
+ # The stylesheet_link_tag helper supports special symbols for bulk inclusion:
30
+ # - :all - includes all CSS files found in the load path
31
+ # - :app - includes only CSS files from app/assets/**/*.css
32
+ #
33
+ # <%= stylesheet_link_tag :all %> # All stylesheets
34
+ # <%= stylesheet_link_tag :app %> # Only app stylesheets
2
35
  module Helper
36
+ # Computes the Subresource Integrity (SRI) hash for the given asset path.
37
+ #
38
+ # This method generates a cryptographic hash of the asset content that can be used
39
+ # to verify the integrity of the resource when it's loaded by the browser.
40
+ #
41
+ # asset_integrity("application.css")
42
+ # # => "sha256-xyz789abcdef..."
43
+ def asset_integrity(path, options = {})
44
+ path = _path_with_extname(path, options)
45
+ Rails.application.assets.resolver.integrity(path)
46
+ end
47
+
48
+ # Resolves the full path for an asset, raising an error if not found.
3
49
  def compute_asset_path(path, options = {})
4
50
  Rails.application.assets.resolver.resolve(path) || raise(MissingAssetError.new(path))
5
51
  end
6
52
 
7
- # Add an option to call `stylesheet_link_tag` with `:all` to include every css file found on the load path
8
- # or `:app` to include css files found in `Rails.root("app/assets/**/*.css")`, which will exclude lib/ and plugins.
53
+ # Enhanced +stylesheet_link_tag+ with integrity support and bulk inclusion options.
54
+ #
55
+ # In addition to the standard Rails functionality, this method supports:
56
+ # * Automatic SRI (Subresource Integrity) hash generation in secure contexts
57
+ # * Add an option to call +stylesheet_link_tag+ with +:all+ to include every css
58
+ # file found on the load path or +:app+ to include css files found in
59
+ # <tt>Rails.root("app/assets/**/*.css")</tt>, which will exclude lib/ and plugins.
60
+ #
61
+ # ==== Options
62
+ #
63
+ # * <tt>:integrity</tt> - Enable SRI hash generation
64
+ #
65
+ # ==== Examples
66
+ #
67
+ # stylesheet_link_tag "application", integrity: true
68
+ # # => <link rel="stylesheet" href="/assets/application-abc123.css"
69
+ # # integrity="sha256-xyz789...">
70
+ #
71
+ # stylesheet_link_tag :all # All stylesheets in load path
72
+ # stylesheet_link_tag :app # Only app/assets stylesheets
9
73
  def stylesheet_link_tag(*sources, **options)
10
74
  case sources.first
11
75
  when :all
12
- super(*all_stylesheets_paths , **options)
76
+ sources = all_stylesheets_paths
13
77
  when :app
14
- super(*app_stylesheets_paths , **options)
15
- else
16
- super
78
+ sources = app_stylesheets_paths
17
79
  end
80
+
81
+ _build_asset_tags(sources, options, :stylesheet) { |source, opts| super(source, opts) }
82
+ end
83
+
84
+ # Enhanced +javascript_include_tag+ with automatic SRI (Subresource Integrity) support.
85
+ #
86
+ # This method extends Rails' built-in +javascript_include_tag+ to automatically
87
+ # generate and include integrity hashes when running in secure contexts.
88
+ #
89
+ # ==== Options
90
+ #
91
+ # * <tt>:integrity</tt> - Enable SRI hash generation
92
+ #
93
+ # ==== Examples
94
+ #
95
+ # javascript_include_tag "application", integrity: true
96
+ # # => <script src="/assets/application-abc123.js"
97
+ # # integrity="sha256-xyz789..."></script>
98
+ def javascript_include_tag(*sources, **options)
99
+ _build_asset_tags(sources, options, :javascript) { |source, opts| super(source, opts) }
18
100
  end
19
101
 
20
102
  # Returns a sorted and unique array of logical paths for all stylesheets in the load path.
@@ -26,5 +108,50 @@ module Propshaft
26
108
  def app_stylesheets_paths
27
109
  Rails.application.assets.load_path.asset_paths_by_glob("#{Rails.root.join("app/assets")}/**/*.css")
28
110
  end
111
+
112
+ private
113
+ # Core method that builds asset tags with optional integrity support.
114
+ #
115
+ # This method handles the common logic for both +stylesheet_link_tag+ and
116
+ # +javascript_include_tag+, including SRI hash generation and HTML tag creation.
117
+ def _build_asset_tags(sources, options, asset_type)
118
+ options = options.stringify_keys
119
+ integrity = _compute_integrity?(options)
120
+
121
+ sources.map { |source|
122
+ opts = integrity ? options.merge!('integrity' => asset_integrity(source, type: asset_type)) : options
123
+ yield(source, opts)
124
+ }.join("\n").html_safe
125
+ end
126
+
127
+ # Determines whether integrity hashes should be computed for assets.
128
+ #
129
+ # Integrity is only computed in secure contexts (HTTPS or local development)
130
+ # and when explicitly requested via the +integrity+ option.
131
+ def _compute_integrity?(options)
132
+ if _secure_subresource_integrity_context?
133
+ case options['integrity']
134
+ when nil, false, true
135
+ options.delete('integrity') == true
136
+ end
137
+ else
138
+ options.delete 'integrity'
139
+ false
140
+ end
141
+ end
142
+
143
+ # Checks if the current context is secure enough for Subresource Integrity.
144
+ #
145
+ # SRI is only beneficial in secure contexts. Returns true when:
146
+ # * The request is made over HTTPS (SSL), OR
147
+ # * The request is local (development environment)
148
+ def _secure_subresource_integrity_context?
149
+ respond_to?(:request) && self.request && (self.request.local? || self.request.ssl?)
150
+ end
151
+
152
+ # Ensures the asset path includes the appropriate file extension.
153
+ def _path_with_extname(path, options)
154
+ "#{path}#{compute_asset_extname(path, options)}"
155
+ end
29
156
  end
30
157
  end
@@ -1,10 +1,22 @@
1
+ require "propshaft/manifest"
1
2
  require "propshaft/asset"
2
3
 
3
4
  class Propshaft::LoadPath
4
- attr_reader :paths, :compilers, :version
5
+ class NullFileWatcher # :nodoc:
6
+ def initialize(paths, files_to_watch, &block)
7
+ @block = block
8
+ end
9
+
10
+ def execute_if_updated
11
+ @block.call
12
+ end
13
+ end
14
+
15
+ attr_reader :paths, :compilers, :version, :integrity_hash_algorithm
5
16
 
6
- def initialize(paths = [], compilers:, version: nil)
7
- @paths, @compilers, @version = dedup(paths), compilers, version
17
+ def initialize(paths = [], compilers:, version: nil, file_watcher: nil, integrity_hash_algorithm: nil)
18
+ @paths, @compilers, @version, @integrity_hash_algorithm = dedup(paths), compilers, version, integrity_hash_algorithm
19
+ @file_watcher = file_watcher || NullFileWatcher
8
20
  end
9
21
 
10
22
  def find(asset_name)
@@ -30,10 +42,8 @@ class Propshaft::LoadPath
30
42
  end
31
43
 
32
44
  def manifest
33
- Hash.new.tap do |manifest|
34
- assets.each do |asset|
35
- manifest[asset.logical_path.to_s] = asset.digested_path.to_s
36
- end
45
+ Propshaft::Manifest.new(integrity_hash_algorithm: integrity_hash_algorithm).tap do |manifest|
46
+ assets.each { |asset| manifest.push_asset(asset) }
37
47
  end
38
48
  end
39
49
 
@@ -46,7 +56,7 @@ class Propshaft::LoadPath
46
56
  files_to_watch = Array(paths).collect { |dir| [ dir.to_s, exts_to_watch ] }.to_h
47
57
  mutex = Mutex.new
48
58
 
49
- Rails.application.config.file_watcher.new([], files_to_watch) do
59
+ @file_watcher.new([], files_to_watch) do
50
60
  mutex.synchronize do
51
61
  clear_cache
52
62
  seed_cache
@@ -0,0 +1,170 @@
1
+ module Propshaft
2
+ # Manages the manifest file that maps logical asset paths to their digested counterparts.
3
+ #
4
+ # The manifest is used to track assets that have been processed and digested, storing
5
+ # their logical paths, digested paths, and optional integrity hashes.
6
+ class Manifest
7
+ # Represents a single entry in the asset manifest.
8
+ #
9
+ # Each entry contains information about an asset including its logical path
10
+ # (the original path), digested path (the path with content hash), and
11
+ # optional integrity hash for security verification.
12
+ class ManifestEntry
13
+ attr_reader :logical_path, :digested_path, :integrity
14
+
15
+ # Creates a new manifest entry.
16
+ #
17
+ # ==== Parameters
18
+ #
19
+ # * +logical_path+ - The logical path of the asset
20
+ # * +digested_path+ - The digested path of the asset
21
+ # * +integrity+ - The integrity hash of the asset (optional)
22
+ def initialize(logical_path:, digested_path:, integrity:) # :nodoc:
23
+ @logical_path = logical_path
24
+ @digested_path = digested_path
25
+ @integrity = integrity
26
+ end
27
+
28
+ # Converts the manifest entry to a hash representation.
29
+ #
30
+ # Returns a hash containing the +digested_path+ and +integrity+ keys.
31
+ def to_h
32
+ { digested_path: digested_path, integrity: integrity}
33
+ end
34
+ end
35
+
36
+ class << self
37
+ # Creates a new Manifest instance from a manifest file.
38
+ #
39
+ # Reads and parses a manifest file, supporting both the current format
40
+ # (with +digested_path+ and +integrity+ keys) and the legacy format
41
+ # (simple string values for backwards compatibility).
42
+ #
43
+ # ==== Parameters
44
+ #
45
+ # * +manifest_path+ - The path to the manifest file
46
+ #
47
+ # ==== Returns
48
+ #
49
+ # A new manifest instance populated with entries from the file.
50
+ def from_path(manifest_path)
51
+ manifest = Manifest.new
52
+
53
+ serialized_manifest = JSON.parse(manifest_path.read, symbolize_names: false)
54
+
55
+ serialized_manifest.each_pair do |key, value|
56
+ # Compatibility mode to be able to
57
+ # read the old "simple manifest" format
58
+ digested_path, integrity = if value.is_a?(String)
59
+ [value, nil]
60
+ else
61
+ [value["digested_path"], value["integrity"]]
62
+ end
63
+
64
+ entry = ManifestEntry.new(
65
+ logical_path: key, digested_path: digested_path, integrity: integrity
66
+ )
67
+
68
+ manifest.push(entry)
69
+ end
70
+
71
+ manifest
72
+ end
73
+ end
74
+
75
+ # Creates a new Manifest instance.
76
+ #
77
+ # ==== Parameters
78
+ #
79
+ # * +integrity_hash_algorithm+ - The algorithm to use for generating
80
+ # integrity hashes (e.g., 'sha256', 'sha384', 'sha512'). If +nil+, integrity hashes
81
+ # will not be generated.
82
+ def initialize(integrity_hash_algorithm: nil)
83
+ @integrity_hash_algorithm = integrity_hash_algorithm
84
+ @entries = {}
85
+ end
86
+
87
+ # Adds an asset to the manifest.
88
+ #
89
+ # Creates a manifest entry from the given asset and adds it to the manifest.
90
+ # The entry will include the asset's logical path, digested path, and optionally
91
+ # an integrity hash if an integrity hash algorithm is configured.
92
+ #
93
+ # ==== Parameters
94
+ #
95
+ # * +asset+ - The asset to add to the manifest
96
+ #
97
+ # ==== Returns
98
+ #
99
+ # The manifest entry that was added.
100
+ def push_asset(asset)
101
+ entry = ManifestEntry.new(
102
+ logical_path: asset.logical_path.to_s,
103
+ digested_path: asset.digested_path.to_s,
104
+ integrity: integrity_hash_algorithm && asset.integrity(hash_algorithm: integrity_hash_algorithm)
105
+ )
106
+
107
+ push(entry)
108
+ end
109
+
110
+ # Adds a manifest entry to the manifest.
111
+ #
112
+ # ==== Parameters
113
+ #
114
+ # * +entry+ - The manifest entry to add
115
+ #
116
+ # ==== Returns
117
+ #
118
+ # The entry that was added.
119
+ def push(entry)
120
+ @entries[entry.logical_path] = entry
121
+ end
122
+ alias_method :<<, :push
123
+
124
+ # Retrieves a manifest entry by its logical path.
125
+ #
126
+ # ==== Parameters
127
+ #
128
+ # * +logical_path+ - The logical path of the asset to retrieve
129
+ #
130
+ # ==== Returns
131
+ #
132
+ # The manifest entry, or +nil+ if not found.
133
+ def [](logical_path)
134
+ @entries[logical_path]
135
+ end
136
+
137
+ # Converts the manifest to JSON format.
138
+ #
139
+ # The JSON representation maps logical paths to hash representations of
140
+ # manifest entries, containing +digested_path+ and +integrity+ information.
141
+ #
142
+ # ==== Returns
143
+ #
144
+ # The JSON representation of the manifest.
145
+ def to_json
146
+ @entries.transform_values do |manifest_entry|
147
+ manifest_entry.to_h
148
+ end.to_json
149
+ end
150
+
151
+ # Transforms the values of all manifest entries using the given block.
152
+ #
153
+ # This method is useful for applying transformations to all manifest entries
154
+ # while preserving the logical path keys.
155
+ #
156
+ # ==== Parameters
157
+ #
158
+ # * +block+ - A block that will receive each manifest entry
159
+ #
160
+ # ==== Returns
161
+ #
162
+ # A new hash with the same keys but transformed values.
163
+ def transform_values(&block)
164
+ @entries.transform_values(&block)
165
+ end
166
+
167
+ private
168
+ attr_reader :integrity_hash_algorithm
169
+ end
170
+ end
@@ -54,10 +54,10 @@ class Propshaft::Processor
54
54
  def compile_asset(asset)
55
55
  File.open(output_path.join(asset.digested_path), "w+") do |file|
56
56
  begin
57
- file.write compilers.compile(asset)
57
+ file.write asset.compiled_content
58
58
  rescue Encoding::UndefinedConversionError
59
59
  # FIXME: Not sure if there's a better way here?
60
- file.write compilers.compile(asset).force_encoding("UTF-8")
60
+ file.write asset.compiled_content.force_encoding("UTF-8")
61
61
  end
62
62
  end if compilers.compilable?(asset)
63
63
  end
@@ -35,6 +35,8 @@ module Propshaft
35
35
  # Prioritize assets from within the application over assets of the same path from engines/gems.
36
36
  config.assets.paths.sort_by!.with_index { |path, i| [path.to_s.start_with?(Rails.root.to_s) ? 0 : 1, i] }
37
37
 
38
+ config.assets.file_watcher ||= app.config.file_watcher
39
+
38
40
  config.assets.relative_url_root ||= app.config.relative_url_root
39
41
  config.assets.output_path ||=
40
42
  Pathname.new(File.join(app.config.paths["public"].first, app.config.assets.prefix))
@@ -7,15 +7,28 @@ module Propshaft::Resolver
7
7
  end
8
8
 
9
9
  def resolve(logical_path)
10
- if asset = load_path.find(logical_path)
10
+ if asset = find_asset(logical_path)
11
11
  File.join prefix, asset.digested_path
12
12
  end
13
13
  end
14
14
 
15
+ def integrity(logical_path)
16
+ hash_algorithm = load_path.integrity_hash_algorithm
17
+
18
+ if hash_algorithm && (asset = find_asset(logical_path))
19
+ asset.integrity(hash_algorithm: hash_algorithm)
20
+ end
21
+ end
22
+
15
23
  def read(logical_path, options = {})
16
24
  if asset = load_path.find(logical_path)
17
25
  asset.content(**options)
18
26
  end
19
27
  end
28
+
29
+ private
30
+ def find_asset(logical_path)
31
+ load_path.find(logical_path)
32
+ end
20
33
  end
21
34
  end
@@ -7,20 +7,32 @@ module Propshaft::Resolver
7
7
  end
8
8
 
9
9
  def resolve(logical_path)
10
- if asset_path = parsed_manifest[logical_path]
10
+ if asset_path = digested_path(logical_path)
11
11
  File.join prefix, asset_path
12
12
  end
13
13
  end
14
14
 
15
+ def integrity(logical_path)
16
+ entry = manifest[logical_path]
17
+
18
+ entry&.integrity
19
+ end
20
+
15
21
  def read(logical_path, encoding: "ASCII-8BIT")
16
- if asset_path = parsed_manifest[logical_path]
22
+ if asset_path = digested_path(logical_path)
17
23
  File.read(manifest_path.dirname.join(asset_path), encoding: encoding)
18
24
  end
19
25
  end
20
26
 
21
27
  private
22
- def parsed_manifest
23
- @parsed_manifest ||= JSON.parse(manifest_path.read, symbolize_names: false)
28
+ def manifest
29
+ @manifest ||= Propshaft::Manifest.from_path(manifest_path)
30
+ end
31
+
32
+ def digested_path(logical_path)
33
+ entry = manifest[logical_path]
34
+
35
+ entry&.digested_path
24
36
  end
25
37
  end
26
38
  end
@@ -7,10 +7,11 @@ class Propshaft::Server
7
7
  end
8
8
 
9
9
  def call(env)
10
+ execute_cache_sweeper_if_updated
10
11
  path, digest = extract_path_and_digest(env)
11
12
 
12
13
  if (asset = @assembly.load_path.find(path)) && asset.fresh?(digest)
13
- compiled_content = @assembly.compilers.compile(asset)
14
+ compiled_content = asset.compiled_content
14
15
 
15
16
  [
16
17
  200,
@@ -18,7 +19,7 @@ class Propshaft::Server
18
19
  Rack::CONTENT_LENGTH => compiled_content.length.to_s,
19
20
  Rack::CONTENT_TYPE => asset.content_type.to_s,
20
21
  VARY => "Accept-Encoding",
21
- Rack::ETAG => asset.digest,
22
+ Rack::ETAG => "\"#{asset.digest}\"",
22
23
  Rack::CACHE_CONTROL => "public, max-age=31536000, immutable"
23
24
  },
24
25
  [ compiled_content ]
@@ -44,4 +45,8 @@ class Propshaft::Server
44
45
  else
45
46
  VARY = "vary"
46
47
  end
48
+
49
+ def execute_cache_sweeper_if_updated
50
+ @assembly.load_path.cache_sweeper.execute_if_updated
51
+ end
47
52
  end
@@ -1,3 +1,3 @@
1
1
  module Propshaft
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/propshaft.rb CHANGED
@@ -10,4 +10,4 @@ end
10
10
  require "propshaft/assembly"
11
11
  require "propshaft/errors"
12
12
  require "propshaft/helper"
13
- require "propshaft/railtie"
13
+ require "propshaft/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: propshaft
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actionpack
@@ -38,20 +37,6 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: 7.0.0
41
- - !ruby/object:Gem::Dependency
42
- name: railties
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: 7.0.0
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: 7.0.0
55
40
  - !ruby/object:Gem::Dependency
56
41
  name: rack
57
42
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +51,6 @@ dependencies:
66
51
  - - ">="
67
52
  - !ruby/object:Gem::Version
68
53
  version: '0'
69
- description:
70
54
  email: dhh@hey.com
71
55
  executables: []
72
56
  extensions: []
@@ -86,6 +70,7 @@ files:
86
70
  - lib/propshaft/errors.rb
87
71
  - lib/propshaft/helper.rb
88
72
  - lib/propshaft/load_path.rb
73
+ - lib/propshaft/manifest.rb
89
74
  - lib/propshaft/output_path.rb
90
75
  - lib/propshaft/processor.rb
91
76
  - lib/propshaft/quiet_assets.rb
@@ -100,7 +85,6 @@ licenses:
100
85
  - MIT
101
86
  metadata:
102
87
  rubygems_mfa_required: 'true'
103
- post_install_message:
104
88
  rdoc_options: []
105
89
  require_paths:
106
90
  - lib
@@ -115,8 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
99
  - !ruby/object:Gem::Version
116
100
  version: '0'
117
101
  requirements: []
118
- rubygems_version: 3.5.16
119
- signing_key:
102
+ rubygems_version: 3.6.7
120
103
  specification_version: 4
121
104
  summary: Deliver assets for Rails.
122
105
  test_files: []