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 +4 -4
- data/.github/workflows/ci.yml +1 -0
- data/README.md +55 -14
- data/features/asset_serving.feature +5 -0
- data/features/step_definitions/view_helper_steps.rb +9 -0
- data/features/view_helpers.feature +5 -0
- data/gemfiles/rails_7_2.gemfile +1 -0
- data/gemfiles/rails_8_0.gemfile +1 -0
- data/gemfiles/rails_8_1.gemfile +1 -0
- data/lib/isolate_assets/assets.rb +56 -14
- data/lib/isolate_assets/engine_extension.rb +33 -9
- data/lib/isolate_assets/helper.rb +32 -16
- data/lib/isolate_assets/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc92b30bb9f88b0d8f12eb3ed349f6a6f1f9decbe87a675b7200e70c8c9bd611
|
|
4
|
+
data.tar.gz: 5aa6ea46f8f8b348f913b1656a6187c3e0875536edb066929f453d997abf7186
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bb014e542b5067a9b5543f0ae6c6a1c14e8578b1b6f8dbe2d717c304f4798ff11e7293baab555bcdcddba0162067ee172b6ccd6ddf5b92d25dfeb0bc18c2c94
|
|
7
|
+
data.tar.gz: 96bedf91563d0e5cbea9ac08075e3b5611bdd61a8275fa366058420a7dacbeed2c04845f7c7cce7b424453a2e0638ba0879f403f168cdce2309d38ba67228e59
|
data/.github/workflows/ci.yml
CHANGED
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.
|
|
67
|
+
### 3. Use in your views
|
|
62
68
|
|
|
63
|
-
|
|
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
|
-
|
|
105
|
+
Then in views:
|
|
75
106
|
|
|
76
107
|
```erb
|
|
77
|
-
|
|
78
|
-
<%=
|
|
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
|
-
|
|
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
|
data/gemfiles/rails_7_2.gemfile
CHANGED
data/gemfiles/rails_8_0.gemfile
CHANGED
data/gemfiles/rails_8_1.gemfile
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
#
|
|
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
|
|
43
|
-
|
|
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
|
|
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:
|
|
14
|
+
href: isolated_assets.asset_url(source, "css"),
|
|
22
15
|
**options
|
|
23
16
|
)
|
|
24
17
|
end
|
|
25
18
|
|
|
26
|
-
def
|
|
19
|
+
def javascript_include_tag(source, **options)
|
|
27
20
|
tag.script(
|
|
28
|
-
src:
|
|
21
|
+
src: isolated_assets.asset_url(source, "js"),
|
|
29
22
|
**options
|
|
30
23
|
)
|
|
31
24
|
end
|
|
32
25
|
|
|
33
|
-
def
|
|
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] =
|
|
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
|