assiette 0.2.0 → 0.3.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: 0a187177bddf32e79959d651085f938025b48fc19fdaaacadd45d40b123752b1
4
- data.tar.gz: 011cd619392df23a7a4f83d65b020615686390037cb3254dc026b5ba11f6918f
3
+ metadata.gz: 66f3dd34f305d6fbfa1ab42498d834bb2820d83ee2d5a1b84cec5cc54f686873
4
+ data.tar.gz: 4e824983daf15b8dad3eb6c4a9e9570b3fc9a2722575d21c31c8abd31e7fa70a
5
5
  SHA512:
6
- metadata.gz: c4c1634cd357063c7d38dcf3a9c12f74527a5c7b4fad59069c67c0fea8f14291c41e0c8e4a4a39f55bd399dc4e5d5c5af834768d2544296cf52c6ff4bd7565a6
7
- data.tar.gz: 127a615aac3b7edb7e5338dd433cccb1f1a3a6994d3d0406cb8955d210ab0a427513310d37f7d88b905cab69a57b1730ce07564641e0e206835ac8299c572619
6
+ metadata.gz: a123ca3f4632f04c15b4deba0e71f8bcce05123ea380a1a7c5877aab92d1994b10fb5a79168f86d795edebcdbad2a2056659c9729a42fa8608e0e5a49e10a7bf
7
+ data.tar.gz: 93273d9b24ee88b29d4fb7221e518a871e9931285c76c8bbc8c32af329d62a1a171e856068beb3fd0c337c8bf22bf6bc19ead2aac7ebb6a652d5c6671651d2bb
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "digest/sha2"
5
+ require "base64"
6
+ require "pathname"
7
+ require_relative "rewriter"
8
+ require_relative "version_tag"
9
+
10
+ module Assiette
11
+ class AssetHandler
12
+ CONTENT_TYPES = {
13
+ ".js" => "application/javascript",
14
+ ".mjs" => "application/javascript",
15
+ ".css" => "text/css",
16
+ ".svg" => "image/svg+xml",
17
+ ".png" => "image/png",
18
+ ".ico" => "image/x-icon"
19
+ }.freeze
20
+
21
+ JS_EXTENSIONS = %w[.js .mjs].to_set.freeze
22
+
23
+ def initialize(root:, additional_directory_mappings: {})
24
+ @mappings = build_mappings(root, additional_directory_mappings)
25
+ @integrity_cache = {}
26
+ @integrity_mutex = Mutex.new
27
+ @modules_cache = nil
28
+ @modules_mutex = Mutex.new
29
+ @modules_version = nil
30
+ end
31
+
32
+ def resolve_file(path)
33
+ clean = path.sub(%r{\A/}, "")
34
+ @mappings.each do |prefix, root|
35
+ if prefix.empty?
36
+ relative = clean
37
+ elsif clean.start_with?(prefix + "/")
38
+ relative = clean[(prefix.length + 1)..]
39
+ elsif clean == prefix
40
+ next
41
+ else
42
+ next
43
+ end
44
+
45
+ abs = root.join(relative).cleanpath
46
+ next unless abs.to_s.start_with?(root.to_s + "/")
47
+ return abs if abs.exist? && abs.file?
48
+ end
49
+ nil
50
+ end
51
+
52
+ def absolute_asset_url_path(path, script_name = "")
53
+ clean = path.sub(%r{\A/}, "")
54
+ return nil unless resolve_file(clean)
55
+ "#{script_name}/#{clean}?v=#{Assiette.version_tag}"
56
+ end
57
+
58
+ def asset_integrity(path)
59
+ version_tag = Assiette.version_tag
60
+ @integrity_mutex.synchronize do
61
+ if @integrity_version != version_tag
62
+ @integrity_cache = {}
63
+ @integrity_version = version_tag
64
+ end
65
+ return @integrity_cache[path] if @integrity_cache.key?(path)
66
+
67
+ clean = path.sub(%r{\A/}, "")
68
+ @integrity_cache[path] = compute_integrity(clean, version_tag)
69
+ end
70
+ end
71
+
72
+ def js_modules
73
+ version_tag = Assiette.version_tag
74
+ @modules_mutex.synchronize do
75
+ return @modules_cache if @modules_version == version_tag
76
+
77
+ @modules_cache = @mappings.flat_map { |prefix, root|
78
+ Dir[File.join(root, "**/*.{js,mjs}")].filter_map { |abs|
79
+ next unless File.foreach(abs).any? { |line| line.match?(/\A\s*(import|export)\s/) }
80
+ relative = Pathname.new(abs).relative_path_from(root).to_s
81
+ mod_path = "/#{"#{prefix}/" unless prefix.empty?}#{relative}".squeeze("/")
82
+ {path: mod_path, integrity: asset_integrity(mod_path)}
83
+ }
84
+ }.uniq { |m| m[:path] }.sort_by { |m| m[:path] }
85
+
86
+ @modules_version = version_tag
87
+ @modules_cache
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def build_mappings(root, additional_directory_mappings)
94
+ mappings = [["", Pathname.new(root).expand_path]]
95
+ additional_directory_mappings.each do |prefix, path|
96
+ clean_prefix = prefix.to_s.sub(%r{\A/}, "").chomp("/")
97
+ mappings << [clean_prefix, Pathname.new(path).expand_path]
98
+ end
99
+ mappings
100
+ end
101
+
102
+ def compute_integrity(clean, version_tag)
103
+ file_path = resolve_file(clean)
104
+ return nil unless file_path
105
+
106
+ raw = File.read(file_path)
107
+ ext = File.extname(clean)
108
+ served = case ext
109
+ when ".js", ".mjs" then Rewriter.rewrite_js_imports(raw, version_tag)
110
+ when ".css" then Rewriter.rewrite_css_urls(raw, version_tag)
111
+ else raw
112
+ end
113
+ "sha256-#{Base64.strict_encode64(Digest::SHA256.digest(served))}"
114
+ end
115
+ end
116
+ end
@@ -7,7 +7,7 @@ module Assiette
7
7
  def assiette_asset_path(path)
8
8
  entry = request.env["assiette.stack"]&.last
9
9
  raise "No Assiette::Server in middleware stack" unless entry
10
- entry[:server].absolute_asset_url_path(path, entry[:script_name])
10
+ entry[:handler].absolute_asset_url_path(path, entry[:script_name])
11
11
  end
12
12
 
13
13
  # Returns the SRI integrity hash for an asset, computed over the served
@@ -15,7 +15,7 @@ module Assiette
15
15
  def assiette_asset_integrity(path)
16
16
  entry = request.env["assiette.stack"]&.last
17
17
  raise "No Assiette::Server in middleware stack" unless entry
18
- entry[:server].asset_integrity(path)
18
+ entry[:handler].asset_integrity(path)
19
19
  end
20
20
 
21
21
  # Generates a <link rel="stylesheet"> tag with SRI integrity.
@@ -30,7 +30,7 @@ module Assiette
30
30
  def assiette_modulepreload_tags
31
31
  entry = request.env["assiette.stack"]&.last
32
32
  raise "No Assiette::Server in middleware stack" unless entry
33
- modules = entry[:server].js_modules
33
+ modules = entry[:handler].js_modules
34
34
  safe_join(modules.map { |mod|
35
35
  tag.link(rel: "modulepreload", href: assiette_asset_path(mod[:path]),
36
36
  integrity: mod[:integrity], crossorigin: "anonymous")
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assiette
4
+ # Include this module into ActionView::Base to make standard Rails asset
5
+ # helpers (image_tag, stylesheet_link_tag, etc.) resolve paths through
6
+ # an Assiette::AssetHandler assigned to Rails.application.assets.
7
+ #
8
+ # ActiveSupport.on_load(:action_view) do
9
+ # include Assiette::RailsAssetUrlHelper
10
+ # end
11
+ module RailsAssetUrlHelper
12
+ def compute_asset_path(source, options = {})
13
+ resolver = Rails.application.assets
14
+ if resolver.is_a?(Assiette::AssetHandler)
15
+ resolved = resolver.absolute_asset_url_path("/#{source}")
16
+ return resolved if resolved
17
+ end
18
+ super
19
+ end
20
+ end
21
+ end
@@ -7,5 +7,11 @@ module Assiette
7
7
  generators do
8
8
  require_relative "../generators/assiette/install/install_generator"
9
9
  end
10
+
11
+ initializer "assiette.rails_asset_url_helper" do
12
+ ActiveSupport.on_load(:action_view) do
13
+ include Assiette::RailsAssetUrlHelper
14
+ end
15
+ end
10
16
  end
11
17
  end
@@ -1,40 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest/sha1"
4
- require "digest/sha2"
5
- require "base64"
6
- require "pathname"
7
- require_relative "rewriter"
8
- require_relative "version_tag"
3
+ require_relative "asset_handler"
9
4
 
10
5
  module Assiette
11
6
  class Server
12
- CONTENT_TYPES = {
13
- ".js" => "application/javascript",
14
- ".mjs" => "application/javascript",
15
- ".css" => "text/css",
16
- ".svg" => "image/svg+xml",
17
- ".png" => "image/png",
18
- ".ico" => "image/x-icon"
19
- }.freeze
20
-
21
- JS_EXTENSIONS = %w[.js .mjs].to_set.freeze
22
-
23
7
  CACHE_CONTROL = "public, max-age=432000, must-revalidate"
24
8
 
25
- def initialize(app, root:, additional_directory_mappings: {})
9
+ # Accepts either a pre-built handler or keyword args:
10
+ # Server.new(app, handler)
11
+ # Server.new(app, root: "...", additional_directory_mappings: {})
12
+ def initialize(app, handler = nil, root: nil, additional_directory_mappings: {})
26
13
  @app = app
27
- @mappings = build_mappings(root, additional_directory_mappings)
28
- @integrity_cache = {}
29
- @integrity_mutex = Mutex.new
30
- @modules_cache = nil
31
- @modules_mutex = Mutex.new
32
- @modules_version = nil
14
+ @handler = handler || AssetHandler.new(root: root, additional_directory_mappings: additional_directory_mappings)
33
15
  end
34
16
 
35
17
  def call(env)
36
18
  stack = (env["assiette.stack"] ||= [])
37
- stack << {server: self, script_name: env["SCRIPT_NAME"].to_s}
19
+ stack << {handler: @handler, script_name: env["SCRIPT_NAME"].to_s}
38
20
 
39
21
  result = serve(env)
40
22
  return result if result
@@ -42,57 +24,8 @@ module Assiette
42
24
  @app.call(env)
43
25
  end
44
26
 
45
- # Public API for helpers
46
- def absolute_asset_url_path(path, script_name = "")
47
- clean = path.sub(%r{\A/}, "")
48
- return nil unless resolve_file(clean)
49
- "#{script_name}/#{clean}?v=#{Assiette.version_tag}"
50
- end
51
-
52
- def asset_integrity(path)
53
- version_tag = Assiette.version_tag
54
- @integrity_mutex.synchronize do
55
- if @integrity_version != version_tag
56
- @integrity_cache = {}
57
- @integrity_version = version_tag
58
- end
59
- return @integrity_cache[path] if @integrity_cache.key?(path)
60
-
61
- clean = path.sub(%r{\A/}, "")
62
- @integrity_cache[path] = compute_integrity(clean, version_tag)
63
- end
64
- end
65
-
66
- def js_modules
67
- version_tag = Assiette.version_tag
68
- @modules_mutex.synchronize do
69
- return @modules_cache if @modules_version == version_tag
70
-
71
- @modules_cache = @mappings.flat_map { |prefix, root|
72
- Dir[File.join(root, "**/*.{js,mjs}")].filter_map { |abs|
73
- next unless File.foreach(abs).any? { |line| line.match?(/\A\s*(import|export)\s/) }
74
- relative = Pathname.new(abs).relative_path_from(root).to_s
75
- mod_path = "/#{"#{prefix}/" unless prefix.empty?}#{relative}".squeeze("/")
76
- {path: mod_path, integrity: asset_integrity(mod_path)}
77
- }
78
- }.uniq { |m| m[:path] }.sort_by { |m| m[:path] }
79
-
80
- @modules_version = version_tag
81
- @modules_cache
82
- end
83
- end
84
-
85
27
  private
86
28
 
87
- def build_mappings(root, additional_directory_mappings)
88
- mappings = [["", Pathname.new(root).expand_path]]
89
- additional_directory_mappings.each do |prefix, path|
90
- clean_prefix = prefix.to_s.sub(%r{\A/}, "").chomp("/")
91
- mappings << [clean_prefix, Pathname.new(path).expand_path]
92
- end
93
- mappings
94
- end
95
-
96
29
  def serve(env)
97
30
  return unless env["REQUEST_METHOD"] == "GET" || env["REQUEST_METHOD"] == "HEAD"
98
31
 
@@ -100,10 +33,10 @@ module Assiette
100
33
  path_info = path_info.sub(%r{\A/}, "")
101
34
 
102
35
  extension = File.extname(path_info)
103
- content_type = CONTENT_TYPES[extension]
36
+ content_type = AssetHandler::CONTENT_TYPES[extension]
104
37
  return unless content_type
105
38
 
106
- file_path = resolve_file(path_info)
39
+ file_path = @handler.resolve_file(path_info)
107
40
  return unless file_path
108
41
 
109
42
  raw_bytes = File.binread(file_path)
@@ -117,7 +50,7 @@ module Assiette
117
50
  query = Rack::Utils.parse_query(env["QUERY_STRING"])
118
51
  tag = query["v"].to_s.empty? ? Assiette.version_tag : query["v"]
119
52
 
120
- body = if JS_EXTENSIONS.include?(extension)
53
+ body = if AssetHandler::JS_EXTENSIONS.include?(extension)
121
54
  Rewriter.rewrite_js_imports(raw_bytes, tag)
122
55
  elsif extension == ".css"
123
56
  Rewriter.rewrite_css_urls(raw_bytes, tag)
@@ -134,39 +67,5 @@ module Assiette
134
67
 
135
68
  [200, headers, [body]]
136
69
  end
137
-
138
- def resolve_file(path)
139
- clean = path.sub(%r{\A/}, "")
140
- @mappings.each do |prefix, root|
141
- if prefix.empty?
142
- relative = clean
143
- elsif clean.start_with?(prefix + "/")
144
- relative = clean[(prefix.length + 1)..]
145
- elsif clean == prefix
146
- next
147
- else
148
- next
149
- end
150
-
151
- abs = root.join(relative).cleanpath
152
- next unless abs.to_s.start_with?(root.to_s + "/")
153
- return abs if abs.exist? && abs.file?
154
- end
155
- nil
156
- end
157
-
158
- def compute_integrity(clean, version_tag)
159
- file_path = resolve_file(clean)
160
- return nil unless file_path
161
-
162
- raw = File.read(file_path)
163
- ext = File.extname(clean)
164
- served = case ext
165
- when ".js", ".mjs" then Rewriter.rewrite_js_imports(raw, version_tag)
166
- when ".css" then Rewriter.rewrite_css_urls(raw, version_tag)
167
- else raw
168
- end
169
- "sha256-#{Base64.strict_encode64(Digest::SHA256.digest(served))}"
170
- end
171
70
  end
172
71
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Assiette
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/assiette.rb CHANGED
@@ -3,8 +3,10 @@
3
3
  require_relative "assiette/version"
4
4
  require_relative "assiette/version_tag"
5
5
  require_relative "assiette/rewriter"
6
+ require_relative "assiette/asset_handler"
6
7
  require_relative "assiette/server"
7
8
  require_relative "assiette/helpers"
9
+ require_relative "assiette/rails_asset_url_helper"
8
10
  require_relative "assiette/railtie" if defined?(Rails::Railtie)
9
11
 
10
12
  module Assiette
@@ -6,20 +6,28 @@
6
6
  # This file runs during :load_config_initializers, which is before the
7
7
  # middleware stack is frozen (:build_middleware_stack) in Rails 8.1+.
8
8
 
9
- # Serve files from app/assets (JS modules, CSS, SVGs, etc.)
10
- Rails.application.config.middleware.use(
11
- Assiette::Server,
12
- root: Rails.root.join("app/assets")
9
+ # Build a single handler that knows about app/assets and public/, and share
10
+ # it between the Rack middleware (which serves the files) and the Rails
11
+ # asset helpers (which generate URLs).
12
+ handler = Assiette::AssetHandler.new(
13
+ root: Rails.root.join("app/assets"),
14
+ additional_directory_mappings: {
15
+ "/" => Rails.root.join("public")
16
+ }
13
17
  )
14
18
 
15
- # Serve files from public/ (favicons, static images, etc.)
16
- Rails.application.config.middleware.use(
17
- Assiette::Server,
18
- root: Rails.root.join("public")
19
- )
19
+ # Serve files through the shared handler.
20
+ Rails.application.config.middleware.use Assiette::Server, handler
21
+
22
+ # Make the standard Rails asset helpers (asset_path, image_tag,
23
+ # stylesheet_link_tag, javascript_include_tag, ...) resolve paths through
24
+ # Assiette and pick up its ?v= cache-busting tag. Unknown assets fall back
25
+ # to Rails' default behavior. Remove this line if you'd rather use only
26
+ # the assiette_* helpers below.
27
+ Rails.application.assets = handler
20
28
 
21
- # Make assiette_asset_path, assiette_stylesheet_tag, and
22
- # assiette_modulepreload_tags available in all views.
29
+ # Also expose assiette_asset_path, assiette_stylesheet_tag, and
30
+ # assiette_modulepreload_tags for templates that want them directly.
23
31
  ActiveSupport.on_load(:action_controller_base) do
24
32
  helper Assiette::Helpers
25
33
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: assiette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2026-05-20 00:00:00.000000000 Z
11
+ date: 2026-05-25 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: actionpack
@@ -34,7 +35,9 @@ files:
34
35
  - LICENSE.txt
35
36
  - Rakefile
36
37
  - lib/assiette.rb
38
+ - lib/assiette/asset_handler.rb
37
39
  - lib/assiette/helpers.rb
40
+ - lib/assiette/rails_asset_url_helper.rb
38
41
  - lib/assiette/railtie.rb
39
42
  - lib/assiette/rewriter.rb
40
43
  - lib/assiette/server.rb
@@ -50,6 +53,7 @@ metadata:
50
53
  homepage_uri: https://github.com/julik/assiette
51
54
  source_code_uri: https://github.com/julik/assiette
52
55
  changelog_uri: https://github.com/julik/assiette/blob/main/CHANGELOG.md
56
+ post_install_message:
53
57
  rdoc_options: []
54
58
  require_paths:
55
59
  - lib
@@ -64,7 +68,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
68
  - !ruby/object:Gem::Version
65
69
  version: '0'
66
70
  requirements: []
67
- rubygems_version: 3.6.6
71
+ rubygems_version: 3.4.10
72
+ signing_key:
68
73
  specification_version: 4
69
74
  summary: Zero-build asset serving for Rails engines
70
75
  test_files: []