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 +4 -4
- data/README.md +48 -2
- data/lib/propshaft/assembly.rb +9 -2
- data/lib/propshaft/asset.rb +24 -1
- data/lib/propshaft/compiler/css_asset_urls.rb +1 -1
- data/lib/propshaft/helper.rb +133 -6
- data/lib/propshaft/load_path.rb +18 -8
- data/lib/propshaft/manifest.rb +170 -0
- data/lib/propshaft/processor.rb +2 -2
- data/lib/propshaft/railtie.rb +2 -0
- data/lib/propshaft/resolver/dynamic.rb +14 -1
- data/lib/propshaft/resolver/static.rb +16 -4
- data/lib/propshaft/server.rb +7 -2
- data/lib/propshaft/version.rb +1 -1
- data/lib/propshaft.rb +1 -1
- metadata +4 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ea8c536e8ee4cd0229dd706a6689d8e52d13a63533fb1f2d5e19835f3a853f2
|
4
|
+
data.tar.gz: fdd5faee6f39306a3bd12f0d3b008b31554b2576637f14497640dd886d38d865
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/propshaft/assembly.rb
CHANGED
@@ -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(
|
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
|
data/lib/propshaft/asset.rb
CHANGED
@@ -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
|
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 }
|
data/lib/propshaft/helper.rb
CHANGED
@@ -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
|
-
#
|
8
|
-
#
|
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
|
-
|
76
|
+
sources = all_stylesheets_paths
|
13
77
|
when :app
|
14
|
-
|
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
|
data/lib/propshaft/load_path.rb
CHANGED
@@ -1,10 +1,22 @@
|
|
1
|
+
require "propshaft/manifest"
|
1
2
|
require "propshaft/asset"
|
2
3
|
|
3
4
|
class Propshaft::LoadPath
|
4
|
-
|
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
|
-
|
34
|
-
assets.each
|
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
|
-
|
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
|
data/lib/propshaft/processor.rb
CHANGED
@@ -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
|
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
|
60
|
+
file.write asset.compiled_content.force_encoding("UTF-8")
|
61
61
|
end
|
62
62
|
end if compilers.compilable?(asset)
|
63
63
|
end
|
data/lib/propshaft/railtie.rb
CHANGED
@@ -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 =
|
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 =
|
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 =
|
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
|
23
|
-
@
|
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
|
data/lib/propshaft/server.rb
CHANGED
@@ -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 =
|
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
|
data/lib/propshaft/version.rb
CHANGED
data/lib/propshaft.rb
CHANGED
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.
|
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:
|
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.
|
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: []
|