hanami-sprockets 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c506dbf07d8b95e741306b6511182bc1428913b4f081f09a5727143de9f27ba3
4
+ data.tar.gz: d307f243f6ea655eeeafbddf4b326cd75307ea25321b75215a933563543a456a
5
+ SHA512:
6
+ metadata.gz: cff9adcbec8df4c221ab3df289b7f33d8065dc12e98f6cb959c0e7828ea1cc01e9208ebba5c7f18d14338244b55160b04fde1e6fabd183b5a2b3beae26163815
7
+ data.tar.gz: cddf52b5cd2a4c5c968c3d11504f19bae75249239685f4a92664ea93264ed410f2ac625ce1f0954c69759f302bec0d21cfa9b03a055b521c3568b9fe40609f5d
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2025-09-29
8
+ ### Added
9
+ - Initial release
10
+ - Full Sprockets integration with Rails-like asset pipeline
11
+ - Asset fingerprinting and cache busting
12
+ - Development middleware for on-the-fly asset serving
13
+ - Template helpers (stylesheet_tag, javascript_tag, image_tag)
14
+ - Asset precompilation for production
15
+ - Subresource Integrity (SRI) support
16
+ - Cross-origin request detection
17
+ - Compatible with existing Sprockets ecosystem gems
18
+ - No npm/Node.js dependencies required
19
+ - Comprehensive test suite
20
+ - Hanami-compatible API matching original hanami-assets
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrew Nesbitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject so the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # Hanami::Sprockets
2
+
3
+ Drop-in replacement for hanami-assets that uses Sprockets (like Rails) instead of npm/Node.js.
4
+
5
+ If you want the Rails asset pipeline in your Hanami app without dealing with npm, this is for you.
6
+
7
+ ## What you get
8
+
9
+ - Rails-style Sprockets asset pipeline
10
+ - No npm/package.json required
11
+ - Works with existing Sprockets gems (sassc-rails, coffee-rails, etc.)
12
+ - Same API as hanami-assets
13
+ - Asset fingerprinting and precompilation
14
+ - Development middleware for serving assets
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'hanami-sprockets'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ ## Basic Usage
31
+
32
+ ### Configuration
33
+
34
+ ```ruby
35
+ require 'hanami-sprockets'
36
+
37
+ # Basic configuration
38
+ config = Hanami::Assets::Config.new(
39
+ path_prefix: "/assets",
40
+ digest: true, # Enable fingerprinting for production
41
+ subresource_integrity: [:sha256] # Enable SRI
42
+ )
43
+
44
+ # Create assets instance
45
+ assets = Hanami::Assets.new(
46
+ config: config,
47
+ root: "/path/to/your/app"
48
+ )
49
+ ```
50
+
51
+ ### Asset Structure
52
+
53
+ Follow Rails conventions:
54
+
55
+ ```
56
+ app/
57
+ ├── assets/
58
+ │ ├── stylesheets/
59
+ │ │ └── app.css
60
+ │ ├── javascripts/
61
+ │ │ └── app.js
62
+ │ └── images/
63
+ │ └── logo.png
64
+ ├── lib/
65
+ │ └── assets/
66
+ └── vendor/
67
+ └── assets/
68
+ ```
69
+
70
+ ### CSS Image References
71
+
72
+ Reference images in CSS using ERB and the `asset_path` helper:
73
+
74
+ ```css
75
+ /* app.css.erb */
76
+ .header {
77
+ background-image: url('<%= asset_path("logo.png") %>');
78
+ }
79
+ ```
80
+
81
+ This generates fingerprinted URLs automatically in production. Plain CSS with relative paths also works:
82
+
83
+ ```css
84
+ /* Plain CSS */
85
+ .simple {
86
+ background-image: url('../images/logo.png');
87
+ }
88
+ ```
89
+
90
+ ### Development Server
91
+
92
+ Add the middleware to your Rack stack:
93
+
94
+ ```ruby
95
+ use Hanami::Assets::Middleware, assets
96
+ ```
97
+
98
+ ### Using Assets
99
+
100
+ ```ruby
101
+ # Get an asset
102
+ asset = assets["app.css"]
103
+ puts asset.url # => "/assets/app-abc123def.css"
104
+ puts asset.path # => "/assets/app-abc123def.css"
105
+ puts asset.sri # => "sha256-..."
106
+
107
+ # Check if asset exists
108
+ begin
109
+ asset = assets["missing.css"]
110
+ rescue Hanami::Assets::AssetMissingError
111
+ puts "Asset not found"
112
+ end
113
+ ```
114
+
115
+ ### Template Helpers
116
+
117
+ Include the helpers in your templates:
118
+
119
+ ```ruby
120
+ class MyView
121
+ include Hanami::Assets::Helpers
122
+
123
+ private
124
+
125
+ def hanami_assets
126
+ @hanami_assets # Inject your assets instance
127
+ end
128
+ end
129
+ ```
130
+
131
+ Then use them in templates:
132
+
133
+ ```erb
134
+ <!DOCTYPE html>
135
+ <html>
136
+ <head>
137
+ <%= stylesheet_tag "reset", "app" %>
138
+ </head>
139
+ <body>
140
+ <%= image_tag "logo", alt: "Logo" %>
141
+ <%= javascript_tag "app", async: true %>
142
+ </body>
143
+ </html>
144
+ ```
145
+
146
+ ### Precompilation
147
+
148
+ For production:
149
+
150
+ ```ruby
151
+ # Precompile assets
152
+ manifest = assets.precompile("/path/to/public/assets")
153
+ puts "Compiled assets:", manifest.assets.keys
154
+ ```
155
+
156
+ ## Advanced Configuration
157
+
158
+ ```ruby
159
+ config = Hanami::Assets::Config.new do |config|
160
+ config.path_prefix = "/assets"
161
+ config.digest = Rails.env.production?
162
+ config.compress = Rails.env.production?
163
+ config.subresource_integrity = [:sha256, :sha512]
164
+ config.base_url = ENV['CDN_URL'] # For CDN support
165
+ config.asset_paths = [
166
+ "vendor/assets/custom",
167
+ "lib/special_assets"
168
+ ]
169
+ config.precompile = %w[
170
+ app.js
171
+ app.css
172
+ *.png
173
+ *.jpg
174
+ *.svg
175
+ ]
176
+ end
177
+ ```
178
+
179
+ ## Adding processors
180
+
181
+ Just add the gems you want:
182
+
183
+ ```ruby
184
+ gem 'sassc-rails' # SCSS/Sass
185
+ gem 'coffee-rails' # CoffeeScript
186
+ gem 'uglifier' # JS compression
187
+ ```
188
+
189
+ ## Development vs Production
190
+
191
+ - **Development**: Assets served on-demand via middleware
192
+ - **Production**: Precompile assets with `assets.precompile("/path/to/public/assets")`
193
+
194
+ ## API Reference
195
+
196
+ ### Hanami::Assets
197
+
198
+ Main class for asset management.
199
+
200
+ #### Methods
201
+
202
+ - `#[](path)` - Find and return an asset
203
+ - `#precompile(target_dir)` - Precompile assets for production
204
+ - `#logical_paths` - Get all available asset paths
205
+ - `#subresource_integrity?` - Check if SRI is enabled
206
+ - `#crossorigin?(url)` - Check if URL is cross-origin
207
+
208
+ ### Hanami::Assets::Asset
209
+
210
+ Represents a single asset.
211
+
212
+ #### Methods
213
+
214
+ - `#url` - Full URL to asset
215
+ - `#path` - Path to asset (without base URL)
216
+ - `#sri` - Subresource integrity hash
217
+ - `#logical_path` - Original path without fingerprint
218
+ - `#digest_path` - Fingerprinted path
219
+
220
+ ### Hanami::Assets::Helpers
221
+
222
+ Template helpers for generating asset HTML tags.
223
+
224
+ #### Methods
225
+
226
+ - `stylesheet_tag(*sources, **options)` - Generate `<link>` tags
227
+ - `javascript_tag(*sources, **options)` - Generate `<script>` tags
228
+ - `image_tag(source, **options)` - Generate `<img>` tags
229
+ - `asset_url(source)` - Get URL for asset
230
+ - `asset_path(source)` - Get path for asset
231
+
232
+ ## License
233
+
234
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "hanami/sprockets/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "hanami-sprockets"
9
+ spec.version = Hanami::Assets::VERSION
10
+ spec.authors = ["Andrew Nesbitt"]
11
+ spec.email = ["andrewnez@gmail.com"]
12
+ spec.summary = "Sprockets-based assets management for Hanami"
13
+ spec.description = "Alternative to hanami-assets that uses Sprockets for asset compilation and management, compatible with existing Sprockets gems"
14
+ spec.homepage = "https://github.com/andrew/hanami-sprockets"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -- lib/* bin/* CHANGELOG.md LICENSE.md README.md hanami-sprockets.gemspec`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+ spec.metadata["rubygems_mfa_required"] = "true"
21
+ spec.required_ruby_version = ">= 3.1"
22
+
23
+ spec.add_dependency "sprockets", "~> 4.0"
24
+ spec.add_dependency "zeitwerk", "~> 2.6"
25
+ spec.add_dependency "base64", "~> 0.1"
26
+ spec.add_dependency "hanami-view", "~> 2.1"
27
+
28
+ spec.add_development_dependency "bundler", ">= 1.6", "< 3"
29
+ spec.add_development_dependency "rake", "~> 13"
30
+ spec.add_development_dependency "rspec", "~> 3.9"
31
+ spec.add_development_dependency "rubocop", "~> 1.0"
32
+ spec.add_development_dependency "rack", "~> 2.2"
33
+ spec.add_development_dependency "rack-test", "~> 1.1"
34
+ spec.add_development_dependency "dry-configurable", "~> 1.1"
35
+ spec.add_development_dependency "dry-inflector", "~> 1.0"
36
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Assets
5
+ # Represents a single front end asset.
6
+ #
7
+ # @api public
8
+ # @since 0.1.0
9
+ class Asset
10
+ # @api private
11
+ # @since 0.1.0
12
+ attr_reader :config
13
+ private :config
14
+
15
+ # Returns the asset's absolute URL path.
16
+ #
17
+ # @example Asset from local dev server
18
+ # asset.path # => "/assets/app.js"
19
+ #
20
+ # @example Deployed asset with fingerprinted name
21
+ # asset.path # => "/assets/app-28a6b886de2372ee3922fcaf3f78f2d8.js"
22
+ #
23
+ # @return [String]
24
+ #
25
+ # @api public
26
+ # @since 0.1.0
27
+ attr_reader :path
28
+
29
+ # @api private
30
+ # @since 0.1.0
31
+ attr_reader :base_url
32
+ private :base_url
33
+
34
+ # Returns the asset's subresource integrity value, or nil if none is available.
35
+ #
36
+ # @return [String, nil]
37
+ #
38
+ # @api public
39
+ # @since 0.1.0
40
+ attr_reader :sri
41
+
42
+ # Returns the asset's logical path (original path without fingerprinting)
43
+ #
44
+ # @return [String]
45
+ #
46
+ # @api public
47
+ # @since 0.1.0
48
+ attr_reader :logical_path
49
+
50
+ # Returns the asset's digest path (fingerprinted path)
51
+ #
52
+ # @return [String]
53
+ #
54
+ # @api public
55
+ # @since 0.1.0
56
+ attr_reader :digest_path
57
+
58
+ # Returns the asset's content type
59
+ #
60
+ # @return [String]
61
+ #
62
+ # @api public
63
+ # @since 0.1.0
64
+ attr_reader :content_type
65
+
66
+ # Returns the asset's source content
67
+ #
68
+ # @return [String]
69
+ #
70
+ # @api public
71
+ # @since 0.1.0
72
+ attr_reader :source
73
+
74
+ # @api private
75
+ # @since 0.1.0
76
+ def initialize(path:, base_url:, sri: nil, logical_path: nil, digest_path: nil, content_type: nil, source: nil)
77
+ @path = path
78
+ @base_url = base_url
79
+ @sri = sri
80
+ @logical_path = logical_path
81
+ @digest_path = digest_path
82
+ @content_type = content_type
83
+ @source = source
84
+ end
85
+
86
+ # @api public
87
+ # @since 0.1.0
88
+ alias_method :subresource_integrity_value, :sri
89
+
90
+ # Returns the asset's full URL.
91
+ #
92
+ # @example Asset from local dev server
93
+ # asset.url # => "https://example.com/assets/app.js"
94
+ #
95
+ # @example Deployed asset with fingerprinted name
96
+ # asset.url # => "https://example.com/assets/app-28a6b886de2372ee3922fcaf3f78f2d8.js"
97
+ #
98
+ # @return [String]
99
+ #
100
+ # @api public
101
+ # @since 0.1.0
102
+ def url
103
+ base_url.join(path)
104
+ end
105
+
106
+ # Returns the asset's full URL
107
+ #
108
+ # @return [String]
109
+ #
110
+ # @see #url
111
+ #
112
+ # @api public
113
+ # @since 0.1.0
114
+ def to_s
115
+ url
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Hanami
6
+ class Assets
7
+ # Base URL for assets
8
+ #
9
+ # @api private
10
+ # @since 0.1.0
11
+ class BaseUrl
12
+ # @api private
13
+ # @since 0.1.0
14
+ attr_reader :url
15
+
16
+ # @api private
17
+ # @since 0.1.0
18
+ def initialize(url = "")
19
+ @url = url.to_s
20
+ end
21
+
22
+ # Join the base URL with a path
23
+ #
24
+ # @param path [String] the path to join
25
+ #
26
+ # @return [String] the full URL
27
+ #
28
+ # @api private
29
+ # @since 0.1.0
30
+ def join(path)
31
+ return path if url.empty?
32
+
33
+ if url.end_with?("/")
34
+ url + path.sub(%r{^/}, "")
35
+ else
36
+ url + path
37
+ end
38
+ end
39
+
40
+ # Returns true if the given source is linked via Cross-Origin policy
41
+ #
42
+ # @param source [String] the source URL
43
+ #
44
+ # @return [Boolean]
45
+ #
46
+ # @api private
47
+ # @since 0.1.0
48
+ def crossorigin?(source)
49
+ return false if url.empty?
50
+
51
+ begin
52
+ base_uri = URI.parse(url)
53
+ source_uri = URI.parse(source)
54
+
55
+ base_uri.host != source_uri.host || base_uri.port != source_uri.port || base_uri.scheme != source_uri.scheme
56
+ rescue URI::InvalidURIError
57
+ false
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require_relative "base_url"
5
+
6
+ module Hanami
7
+ class Assets
8
+ # Hanami sprockets configuration.
9
+ #
10
+ # @api public
11
+ # @since 0.1.0
12
+ class Config
13
+ include Dry::Configurable
14
+
15
+ # @api public
16
+ # @since 0.1.0
17
+ BASE_URL = ""
18
+ private_constant :BASE_URL
19
+
20
+ # @!attribute [rw] path_prefix
21
+ # @return [String]
22
+ #
23
+ # @api public
24
+ # @since 0.1.0
25
+ setting :path_prefix, default: "/assets"
26
+
27
+ # @!attribute [rw] subresource_integrity
28
+ # @return [Array<Symbol>]
29
+ #
30
+ # @example
31
+ # config.subresource_integrity # => [:sha256, :sha512]
32
+ #
33
+ # @api public
34
+ # @since 0.1.0
35
+ setting :subresource_integrity, default: []
36
+
37
+ # @!attribute [rw] base_url
38
+ # @return [BaseUrl]
39
+ #
40
+ # @example
41
+ # config.base_url = "http://some-cdn.com/assets"
42
+ #
43
+ # @api public
44
+ # @since 0.1.0
45
+ setting :base_url, constructor: -> url { BaseUrl.new(url.to_s) }
46
+
47
+ # @!attribute [rw] asset_paths
48
+ # @return [Array<String>]
49
+ #
50
+ # @api public
51
+ # @since 0.1.0
52
+ setting :asset_paths, default: []
53
+
54
+ # @!attribute [rw] precompile
55
+ # @return [Array<String>]
56
+ #
57
+ # @api public
58
+ # @since 0.1.0
59
+ setting :precompile, default: %w[*.js *.css *.png *.jpg *.gif *.svg]
60
+
61
+ # @!attribute [rw] digest
62
+ # @return [Boolean] Whether to use fingerprinted asset names
63
+ #
64
+ # @api public
65
+ # @since 0.1.0
66
+ setting :digest, default: true
67
+
68
+ # @!attribute [rw] compress
69
+ # @return [Boolean] Whether to compress assets
70
+ #
71
+ # @api public
72
+ # @since 0.1.0
73
+ setting :compress, default: true
74
+
75
+ # @!attribute [rw] cache
76
+ # @return [String, nil] Cache directory path
77
+ #
78
+ # @api public
79
+ # @since 0.1.0
80
+ setting :cache, default: nil
81
+
82
+ # @api public
83
+ # @since 0.1.0
84
+ def initialize(**values)
85
+ super()
86
+
87
+ config.update(values.select { |k| _settings.key?(k) })
88
+
89
+ yield(config) if block_given?
90
+ end
91
+
92
+ # Returns true if the given source is linked via Cross-Origin policy (or in other words, if
93
+ # the given source does not satisfy the Same-Origin policy).
94
+ #
95
+ # @param source [String]
96
+ #
97
+ # @return [Boolean]
98
+ #
99
+ # @see https://en.wikipedia.org/wiki/Same-origin_policy#Origin_determination_rules
100
+ # @see https://en.wikipedia.org/wiki/Same-origin_policy#document.domain_property
101
+ #
102
+ # @api private
103
+ # @since 0.1.0
104
+ def crossorigin?(source)
105
+ base_url.crossorigin?(source)
106
+ end
107
+
108
+ private
109
+
110
+ def method_missing(name, ...)
111
+ if config.respond_to?(name)
112
+ config.public_send(name, ...)
113
+ else
114
+ super
115
+ end
116
+ end
117
+
118
+ def respond_to_missing?(name, _incude_all = false)
119
+ config.respond_to?(name) || super
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Assets
5
+ # Base error for Hanami::Assets
6
+ #
7
+ # @since 0.1.0
8
+ class Error < StandardError
9
+ end
10
+
11
+ # Error raised when a requested asset cannot be found
12
+ #
13
+ # @since 0.1.0
14
+ class AssetMissingError < Error
15
+ def initialize(path)
16
+ super("Missing asset: #{path}")
17
+ end
18
+ end
19
+
20
+ # Error raised when the asset manifest cannot be found
21
+ #
22
+ # @since 0.1.0
23
+ class ManifestMissingError < Error
24
+ def initialize(path)
25
+ super("Missing manifest: #{path}")
26
+ end
27
+ end
28
+ end
29
+ end