isolate_assets 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: 3ac311515e556ca2199cbfdb72559a19bffed148c18184c1ac364e742b31d2d6
4
- data.tar.gz: ff9aeb49469e913c4b48c20ee1b82790778410f553606b62f10702a37373623a
3
+ metadata.gz: dc92b30bb9f88b0d8f12eb3ed349f6a6f1f9decbe87a675b7200e70c8c9bd611
4
+ data.tar.gz: 5aa6ea46f8f8b348f913b1656a6187c3e0875536edb066929f453d997abf7186
5
5
  SHA512:
6
- metadata.gz: 0f25291c4ff1393aa72c764d52492e9baaba70f61880ce31403292b15796120a3f1e751cc53db01a8a9fe2e3eb4524ef20767a60bd739cbe3dc2dd96e0b915e5
7
- data.tar.gz: c4aac80bac4d4fc9cf59367afa765024e6f2f3574da76637159875c617a3ddae5b0bbbfd175cd4afad1eed32aae5bb0fa316c53a753045ea738383dcb4401b1c
6
+ metadata.gz: 3bb014e542b5067a9b5543f0ae6c6a1c14e8578b1b6f8dbe2d717c304f4798ff11e7293baab555bcdcddba0162067ee172b6ccd6ddf5b92d25dfeb0bc18c2c94
7
+ data.tar.gz: 96bedf91563d0e5cbea9ac08075e3b5611bdd61a8275fa366058420a7dacbeed2c04845f7c7cce7b424453a2e0638ba0879f403f168cdce2309d38ba67228e59
@@ -4,6 +4,7 @@ jobs:
4
4
  test:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
+ fail-fast: false
7
8
  matrix:
8
9
  ruby: ["3.2", "3.3", "3.4", "4.0"]
9
10
  gemfile: [rails_7_2, rails_8_0, rails_8_1]
data/README.md CHANGED
@@ -54,13 +54,44 @@ my_engine/
54
54
  stylesheets/
55
55
  application.css
56
56
  theme.css
57
+ images/
58
+ logo.png
59
+ icons/
60
+ menu.svg
61
+ fonts/
62
+ custom.woff2
57
63
  ```
58
64
 
59
65
  IsolateAssets automatically excludes your engine's `app/assets/` directory from the host app's asset pipeline (Sprockets/Propshaft), so your assets won't conflict with or be processed by the host application.
60
66
 
61
- ### 3. Include the helper
67
+ ### 3. Use in your views
62
68
 
63
- In your engine's application helper:
69
+ Call helper methods directly on your engine's namespace:
70
+
71
+ ```erb
72
+ <%# Stylesheets %>
73
+ <%= MyEngine.stylesheet_link_tag "application" %>
74
+
75
+ <%# JavaScript %>
76
+ <%= MyEngine.javascript_include_tag "application" %>
77
+
78
+ <%# Images %>
79
+ <%= MyEngine.image_tag "logo.png", alt: "Logo" %>
80
+ <%= MyEngine.image_path "icon.svg" %>
81
+
82
+ <%# Other assets %>
83
+ <%= MyEngine.font_path "custom.woff2" %>
84
+ <%= MyEngine.asset_path "data.json" %>
85
+
86
+ <%# ES6 import maps with CDN dependencies %>
87
+ <%= MyEngine.javascript_importmap_tags "application", {
88
+ "jquery" => "https://cdn.jsdelivr.net/npm/jquery@3.7.1/+esm",
89
+ } %>
90
+ ```
91
+
92
+ ### Alternative: Include helper for unprefixed access
93
+
94
+ If you prefer `stylesheet_link_tag` over `MyEngine.stylesheet_link_tag`, include the helper in your engine's ApplicationHelper:
64
95
 
65
96
  ```ruby
66
97
  # app/helpers/my_engine/application_helper.rb
@@ -71,22 +102,32 @@ module MyEngine
71
102
  end
72
103
  ```
73
104
 
74
- ### 4. Use in your views
105
+ Then in views:
75
106
 
76
107
  ```erb
77
- <%# Basic stylesheet %>
78
- <%= engine_stylesheet_link_tag "application" %>
79
-
80
- <%# Basic script tag %>
81
- <%= engine_javascript_include_tag "application" %>
82
-
83
- <%# ES6 import maps with CDN dependencies %>
84
- <%= engine_javascript_importmap_tags "application", {
85
- "jquery" => "https://cdn.jsdelivr.net/npm/jquery@3.7.1/+esm",
86
- } %>
108
+ <%= stylesheet_link_tag "application" %>
109
+ <%= image_tag "logo.png", alt: "Logo" %>
87
110
  ```
88
111
 
89
- Example output:
112
+ Note: This shadows Rails' built-in asset helpers within your engine's views.
113
+
114
+ ### Available helpers
115
+
116
+ | Helper | Description |
117
+ |--------|-------------|
118
+ | `stylesheet_link_tag(source, **options)` | `<link>` tag for CSS |
119
+ | `javascript_include_tag(source, **options)` | `<script>` tag for JS |
120
+ | `javascript_importmap_tags(entry_point, imports)` | ES6 import map |
121
+ | `image_tag(source, **options)` | `<img>` tag |
122
+ | `image_path(source)` | URL path for images |
123
+ | `asset_path(source)` | URL path for any asset (infers type from extension) |
124
+ | `font_path(source)` | URL path for fonts |
125
+ | `audio_tag(source, **options)` | `<audio>` tag |
126
+ | `audio_path(source)` | URL path for audio |
127
+ | `video_tag(source, **options)` | `<video>` tag |
128
+ | `video_path(source)` | URL path for video |
129
+
130
+ ### Example output
90
131
 
91
132
  ```html
92
133
  <script type="importmap">
@@ -15,6 +15,11 @@ Feature: Asset Serving
15
15
  And the content type should be "text/css"
16
16
  And the response should contain "font-family: sans-serif"
17
17
 
18
+ Scenario: Serving an image file
19
+ When I request "/dummy/assets/logo.png"
20
+ Then I should receive a successful response
21
+ And the content type should be "image/png"
22
+
18
23
  Scenario: Asset fingerprinting
19
24
  When I request "/dummy/assets/application.js"
20
25
  Then the response should have caching headers
@@ -22,3 +22,12 @@ Then("the import map should include {string}") do |key|
22
22
  import_map = JSON.parse(script.text(:all))
23
23
  expect(import_map["imports"]).to have_key(key)
24
24
  end
25
+
26
+ Then("the page should have an image tag with src starting with {string}") do |path|
27
+ expect(page).to have_css("img[src^='#{path}']", visible: false)
28
+ end
29
+
30
+ Then("the image tag should include a fingerprint parameter") do
31
+ img = page.find("img", visible: false)
32
+ expect(img[:src]).to match(/\?v=[a-f0-9]{8}/)
33
+ end
@@ -12,3 +12,8 @@ Feature: View Helpers
12
12
  When I visit the dummy engine root
13
13
  Then the page should have an import map
14
14
  And the import map should include "dummy/application"
15
+
16
+ Scenario: Image tag includes fingerprint
17
+ When I visit the dummy engine root
18
+ Then the page should have an image tag with src starting with "/dummy/assets/logo.png"
19
+ And the image tag should include a fingerprint parameter
@@ -10,6 +10,7 @@ group :test do
10
10
  gem "rspec", "~> 3.0"
11
11
  gem "cucumber", "~> 9.0"
12
12
  gem "capybara", "~> 3.0"
13
+ gem "sprockets-rails"
13
14
  end
14
15
 
15
16
  gemspec path: "../", name: "isolate_assets"
@@ -10,6 +10,7 @@ group :test do
10
10
  gem "rspec", "~> 3.0"
11
11
  gem "cucumber", "~> 9.0"
12
12
  gem "capybara", "~> 3.0"
13
+ gem "sprockets-rails"
13
14
  end
14
15
 
15
16
  gemspec path: "../", name: "isolate_assets"
@@ -10,6 +10,7 @@ group :test do
10
10
  gem "rspec", "~> 3.0"
11
11
  gem "cucumber", "~> 9.0"
12
12
  gem "capybara", "~> 3.0"
13
+ gem "sprockets-rails"
13
14
  end
14
15
 
15
16
  gemspec path: "../", name: "isolate_assets"
@@ -4,6 +4,56 @@ module IsolateAssets
4
4
  class Assets
5
5
  attr_reader :engine, :assets_subdir
6
6
 
7
+ ASSET_DIRECTORIES = {
8
+ "js" => "javascripts",
9
+ "javascript" => "javascripts",
10
+ "css" => "stylesheets",
11
+ "stylesheet" => "stylesheets",
12
+ "png" => "images",
13
+ "jpg" => "images",
14
+ "jpeg" => "images",
15
+ "gif" => "images",
16
+ "svg" => "images",
17
+ "webp" => "images",
18
+ "ico" => "images",
19
+ "woff" => "fonts",
20
+ "woff2" => "fonts",
21
+ "ttf" => "fonts",
22
+ "otf" => "fonts",
23
+ "eot" => "fonts",
24
+ "mp3" => "audio",
25
+ "ogg" => "audio",
26
+ "wav" => "audio",
27
+ "mp4" => "video",
28
+ "webm" => "video",
29
+ "ogv" => "video"
30
+ }.freeze
31
+
32
+ CONTENT_TYPES = {
33
+ "js" => "application/javascript",
34
+ "javascript" => "application/javascript",
35
+ "css" => "text/css",
36
+ "stylesheet" => "text/css",
37
+ "png" => "image/png",
38
+ "jpg" => "image/jpeg",
39
+ "jpeg" => "image/jpeg",
40
+ "gif" => "image/gif",
41
+ "svg" => "image/svg+xml",
42
+ "webp" => "image/webp",
43
+ "ico" => "image/x-icon",
44
+ "woff" => "font/woff",
45
+ "woff2" => "font/woff2",
46
+ "ttf" => "font/ttf",
47
+ "otf" => "font/otf",
48
+ "eot" => "application/vnd.ms-fontobject",
49
+ "mp3" => "audio/mpeg",
50
+ "ogg" => "audio/ogg",
51
+ "wav" => "audio/wav",
52
+ "mp4" => "video/mp4",
53
+ "webm" => "video/webm",
54
+ "ogv" => "video/ogg"
55
+ }.freeze
56
+
7
57
  def initialize(engine:, assets_subdir: "assets")
8
58
  @engine = engine
9
59
  @assets_subdir = assets_subdir
@@ -11,12 +61,11 @@ module IsolateAssets
11
61
  end
12
62
 
13
63
  def asset_path(source, type)
14
- case type.to_s
15
- when "js", "javascript"
16
- engine.root.join("app/#{assets_subdir}/javascripts", "#{source}.js")
17
- when "css", "stylesheet"
18
- engine.root.join("app/#{assets_subdir}/stylesheets", "#{source}.css")
19
- end
64
+ directory = ASSET_DIRECTORIES[type.to_s]
65
+ return nil unless directory
66
+
67
+ normalized = normalize_type(type)
68
+ engine.root.join("app/#{assets_subdir}/#{directory}", "#{source}.#{normalized}")
20
69
  end
21
70
 
22
71
  def asset_url(source, type)
@@ -35,14 +84,7 @@ module IsolateAssets
35
84
  end
36
85
 
37
86
  def content_type(type)
38
- case type.to_s
39
- when "js", "javascript"
40
- "application/javascript"
41
- when "css", "stylesheet"
42
- "text/css"
43
- else
44
- "application/octet-stream"
45
- end
87
+ CONTENT_TYPES[type.to_s] || "application/octet-stream"
46
88
  end
47
89
 
48
90
  def javascript_files
@@ -2,11 +2,18 @@
2
2
 
3
3
  module IsolateAssets
4
4
  module EngineExtension
5
+ HELPER_METHODS = %i[
6
+ stylesheet_link_tag javascript_include_tag javascript_importmap_tags
7
+ asset_path image_path image_tag font_path audio_path audio_tag video_path video_tag
8
+ ].freeze
9
+
5
10
  def isolate_assets(assets_subdir: "assets")
6
11
  engine_class = self
7
12
 
8
- # Exclude engine assets from host's asset pipeline
13
+ # Exclude engine assets from host's asset pipeline (if one exists)
9
14
  initializer "#{engine_name}.isolate_assets.exclude_from_pipeline", before: :load_config_initializers do |app|
15
+ next unless app.config.respond_to?(:assets) && app.config.assets
16
+
10
17
  asset_base = engine_class.root.join("app", assets_subdir)
11
18
  app.config.assets.excluded_paths ||= []
12
19
  if asset_base.exist?
@@ -18,11 +25,12 @@ module IsolateAssets
18
25
 
19
26
  # Sprockets doesn't respect excluded_paths, so filter manually
20
27
  initializer "#{engine_name}.isolate_assets.filter_asset_paths", after: :load_config_initializers do |app|
21
- if app.config.assets.excluded_paths.present?
22
- excluded = app.config.assets.excluded_paths.map(&:to_s)
23
- app.config.assets.paths = app.config.assets.paths.reject do |path|
24
- excluded.include?(path.to_s)
25
- end
28
+ next unless app.config.respond_to?(:assets) && app.config.assets
29
+ next unless app.config.assets.excluded_paths.present?
30
+
31
+ excluded = app.config.assets.excluded_paths.map(&:to_s)
32
+ app.config.assets.paths = app.config.assets.paths.reject do |path|
33
+ excluded.include?(path.to_s)
26
34
  end
27
35
  end
28
36
 
@@ -33,14 +41,30 @@ module IsolateAssets
33
41
  controller_class = Class.new(IsolateAssets::Controller)
34
42
  controller_class.isolated_assets = assets
35
43
 
36
- # Hack in the helpers. There's gotta be a better way than this...
44
+ # Create helper module for inclusion in engine's ApplicationHelper
37
45
  helper_module = Module.new do
38
46
  define_method(:isolated_assets) { assets }
39
47
  include IsolateAssets::Helper
40
48
  end
49
+
41
50
  if engine_class.respond_to?(:railtie_namespace) && engine_class.railtie_namespace
42
- engine_class.railtie_namespace.singleton_class.define_method(:isolated_assets) { assets }
43
- engine_class.railtie_namespace.singleton_class.define_method(:isolated_assets_helper) { helper_module }
51
+ namespace = engine_class.railtie_namespace
52
+
53
+ # Expose isolated_assets and helper module
54
+ namespace.singleton_class.define_method(:isolated_assets) { assets }
55
+ namespace.singleton_class.define_method(:isolated_assets_helper) { helper_module }
56
+
57
+ # Define helper methods directly on namespace (e.g., Dummy.stylesheet_link_tag)
58
+ helper_context = Class.new do
59
+ include IsolateAssets::Helper
60
+ define_method(:isolated_assets) { assets }
61
+ end.new
62
+
63
+ HELPER_METHODS.each do |method_name|
64
+ namespace.singleton_class.define_method(method_name) do |*args, **kwargs, &block|
65
+ helper_context.send(method_name, *args, **kwargs, &block)
66
+ end
67
+ end
44
68
  end
45
69
 
46
70
  engine_class.routes.prepend do
@@ -2,40 +2,33 @@
2
2
 
3
3
  module IsolateAssets
4
4
  module Helper
5
+ include ActionView::Helpers::TagHelper
6
+ include ActionView::Helpers::AssetTagHelper
7
+
5
8
  # Note: isolated_assets method is defined by the including module,
6
9
  # created dynamically in EngineExtension#isolate_assets
7
10
 
8
- def engine_asset_url(source, type)
9
- fingerprint_value = isolated_assets.fingerprint(source, type)
10
- normalized_type = case type.to_s
11
- when "javascript" then "js"
12
- when "stylesheet" then "css"
13
- else type.to_s
14
- end
15
- isolated_asset_path("#{source}.#{normalized_type}", v: fingerprint_value)
16
- end
17
-
18
- def engine_stylesheet_link_tag(source, **options)
11
+ def stylesheet_link_tag(source, **options)
19
12
  tag.link(
20
13
  rel: "stylesheet",
21
- href: engine_asset_url(source, "css"),
14
+ href: isolated_assets.asset_url(source, "css"),
22
15
  **options
23
16
  )
24
17
  end
25
18
 
26
- def engine_javascript_include_tag(source, **options)
19
+ def javascript_include_tag(source, **options)
27
20
  tag.script(
28
- src: engine_asset_url(source, "js"),
21
+ src: isolated_assets.asset_url(source, "js"),
29
22
  **options
30
23
  )
31
24
  end
32
25
 
33
- def engine_javascript_importmap_tags(entry_point = "application", imports = {})
26
+ def javascript_importmap_tags(entry_point = "application", imports = {})
34
27
  assets_root = isolated_assets.engine.root.join("app/#{isolated_assets.assets_subdir}/javascripts")
35
28
  engine_imports = isolated_assets.javascript_files.each_with_object({}) do |path, hash|
36
29
  relative_path = path.relative_path_from(assets_root).to_s
37
30
  key = "#{isolated_assets.engine.engine_name}/#{relative_path.sub(/\.js\z/, "")}"
38
- hash[key] = engine_asset_url(relative_path.sub(/\.js\z/, ""), "js")
31
+ hash[key] = isolated_assets.asset_url(relative_path.sub(/\.js\z/, ""), "js")
39
32
  end
40
33
  [
41
34
  tag.script(type: "importmap") do
@@ -46,5 +39,28 @@ module IsolateAssets
46
39
  JS
47
40
  ].join("\n").html_safe
48
41
  end
42
+
43
+ def asset_path(source)
44
+ ext = File.extname(source).delete_prefix(".")
45
+ source_without_ext = source.sub(/\.#{Regexp.escape(ext)}\z/, "")
46
+ isolated_assets.asset_url(source_without_ext, ext)
47
+ end
48
+
49
+ alias_method :image_path, :asset_path
50
+ alias_method :font_path, :asset_path
51
+ alias_method :audio_path, :asset_path
52
+ alias_method :video_path, :asset_path
53
+
54
+ def image_tag(source, **options)
55
+ tag.img(src: image_path(source), **options)
56
+ end
57
+
58
+ def audio_tag(source, **options)
59
+ tag.audio(src: audio_path(source), **options)
60
+ end
61
+
62
+ def video_tag(source, **options)
63
+ tag.video(src: video_path(source), **options)
64
+ end
49
65
  end
50
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IsolateAssets
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isolate_assets
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
  - Micah Geisel