isolate_assets 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 +7 -0
- data/.github/workflows/ci.yml +24 -0
- data/Appraisals +11 -0
- data/LICENSE +21 -0
- data/README.md +111 -0
- data/Rakefile +10 -0
- data/cucumber.yml +1 -0
- data/features/asset_serving.feature +28 -0
- data/features/step_definitions/asset_steps.rb +27 -0
- data/features/step_definitions/view_helper_steps.rb +24 -0
- data/features/support/env.rb +10 -0
- data/features/view_helpers.feature +14 -0
- data/gemfiles/rails_7_2.gemfile +15 -0
- data/gemfiles/rails_8_0.gemfile +15 -0
- data/gemfiles/rails_8_1.gemfile +15 -0
- data/lib/isolate_assets/assets.rb +72 -0
- data/lib/isolate_assets/controller.rb +37 -0
- data/lib/isolate_assets/engine_extension.rb +37 -0
- data/lib/isolate_assets/helper.rb +50 -0
- data/lib/isolate_assets/version.rb +5 -0
- data/lib/isolate_assets.rb +15 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b42e51e51aa619c08d02e6d2e7c02aca58aab0cbab94b7d629449940bb2d32df
|
|
4
|
+
data.tar.gz: a5872849a66dd7b213a3341e6e905af3403b1d66bf2d00c911e137b8b9411fd5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 03bce00b9fb6117c4c0e2b202e38a99dd37b89dd60d4f767f94d3a7ca253d4b48dbd5da62d83852f054ded4466151cdc6c4aa65e20ad96564e65e806f7414dea
|
|
7
|
+
data.tar.gz: af6aae32f9f05cac171c7e9f761f877ae07afa7419656bf6996424e2c474ab082cb431794e7b339b7e43d9c9744e6341e2183d36b0654638e134088586746113
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
strategy:
|
|
7
|
+
matrix:
|
|
8
|
+
ruby: ["3.2", "3.3", "3.4", "4.0"]
|
|
9
|
+
gemfile: [rails_7_2, rails_8_0, rails_8_1]
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Ruby
|
|
18
|
+
uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Run tests
|
|
24
|
+
run: bundle exec rake
|
data/Appraisals
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Micah Geisel
|
|
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 to 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,111 @@
|
|
|
1
|
+
# IsolateAssets
|
|
2
|
+
|
|
3
|
+
Self-contained asset serving for Rails engines. Serve JavaScript, CSS, and other assets from your engine without depending on Sprockets, Propshaft, or the host application's asset pipeline.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
Rails engines that include UI components need to serve assets, but integrating with the host app's asset pipeline is problematic:
|
|
8
|
+
|
|
9
|
+
- **Sprockets/Propshaft conflicts** - Different versions, configurations, or the host might not use them at all
|
|
10
|
+
- **Webpacker/esbuild/Vite** - Modern setups don't expect engine assets
|
|
11
|
+
- **Configuration burden** - Users must manually configure asset paths
|
|
12
|
+
- **Version compatibility** - Asset pipeline APIs change between Rails versions
|
|
13
|
+
|
|
14
|
+
IsolateAssets solves this by letting your engine serve its own assets through a simple controller, with fingerprinting and caching handled automatically.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add to your engine's gemspec:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
spec.add_dependency "isolate_assets"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### 1. Set up your engine
|
|
27
|
+
|
|
28
|
+
In your engine file, add `isolate_assets` alongside `isolate_namespace`:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# lib/my_engine/engine.rb
|
|
32
|
+
require "isolate_assets"
|
|
33
|
+
|
|
34
|
+
module MyEngine
|
|
35
|
+
class Engine < ::Rails::Engine
|
|
36
|
+
isolate_namespace MyEngine
|
|
37
|
+
isolate_assets
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Add your assets
|
|
43
|
+
|
|
44
|
+
Place assets in `app/engine_assets/`:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
my_engine/
|
|
48
|
+
app/
|
|
49
|
+
engine_assets/
|
|
50
|
+
javascripts/
|
|
51
|
+
application.js
|
|
52
|
+
components/
|
|
53
|
+
widget.js
|
|
54
|
+
stylesheets/
|
|
55
|
+
application.css
|
|
56
|
+
theme.css
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Include the helper
|
|
60
|
+
|
|
61
|
+
In your engine's application helper:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# app/helpers/my_engine/application_helper.rb
|
|
65
|
+
module MyEngine
|
|
66
|
+
module ApplicationHelper
|
|
67
|
+
include MyEngine.isolated_assets_helper
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. Use in your views
|
|
73
|
+
|
|
74
|
+
```erb
|
|
75
|
+
<%# Basic stylesheet %>
|
|
76
|
+
<%= engine_stylesheet_link_tag "application" %>
|
|
77
|
+
|
|
78
|
+
<%# Basic script tag %>
|
|
79
|
+
<%= engine_javascript_include_tag "application" %>
|
|
80
|
+
|
|
81
|
+
<%# ES6 import maps with CDN dependencies %>
|
|
82
|
+
<%= engine_javascript_importmap_tags "application", {
|
|
83
|
+
"jquery" => "https://cdn.jsdelivr.net/npm/jquery@3.7.1/+esm",
|
|
84
|
+
} %>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Example output:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<script type="importmap">
|
|
91
|
+
{
|
|
92
|
+
"imports": {
|
|
93
|
+
"jquery": "https://cdn.jsdelivr.net/npm/jquery@3.7.1/+esm",
|
|
94
|
+
"my_engine/application": "/my_engine/assets/application.js?v=a1b2c3d4",
|
|
95
|
+
"my_engine/components/widget": "/my_engine/assets/components/widget.js?v=e5f6g7h8"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
<script type="module">
|
|
100
|
+
import "my_engine/application"
|
|
101
|
+
</script>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
- Ruby 3.2+
|
|
107
|
+
- Rails 7.2+
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
data/Rakefile
ADDED
data/cucumber.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default: --publish-quiet
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Feature: Asset Serving
|
|
2
|
+
As a Rails engine developer
|
|
3
|
+
I want to serve JavaScript and CSS assets from my engine
|
|
4
|
+
So that my engine's UI works without depending on the host app's asset pipeline
|
|
5
|
+
|
|
6
|
+
Scenario: Serving a JavaScript file
|
|
7
|
+
When I request "/dummy/assets/application.js"
|
|
8
|
+
Then I should receive a successful response
|
|
9
|
+
And the content type should be "application/javascript"
|
|
10
|
+
And the response should contain "Dummy engine loaded"
|
|
11
|
+
|
|
12
|
+
Scenario: Serving a CSS file
|
|
13
|
+
When I request "/dummy/assets/application.css"
|
|
14
|
+
Then I should receive a successful response
|
|
15
|
+
And the content type should be "text/css"
|
|
16
|
+
And the response should contain "font-family: sans-serif"
|
|
17
|
+
|
|
18
|
+
Scenario: Asset fingerprinting
|
|
19
|
+
When I request "/dummy/assets/application.js"
|
|
20
|
+
Then the response should have caching headers
|
|
21
|
+
|
|
22
|
+
Scenario: Missing asset returns 404
|
|
23
|
+
When I request "/dummy/assets/nonexistent.js"
|
|
24
|
+
Then I should receive a not found response
|
|
25
|
+
|
|
26
|
+
Scenario: Path traversal is blocked
|
|
27
|
+
When I request "/dummy/assets/../../lib/dummy.rb"
|
|
28
|
+
Then I should receive a not found response
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
When("I request {string}") do |path|
|
|
4
|
+
visit path
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Then("I should receive a successful response") do
|
|
8
|
+
expect(page.status_code).to eq(200)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Then("I should receive a not found response") do
|
|
12
|
+
expect(page.status_code).to eq(404)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Then("the content type should be {string}") do |content_type|
|
|
16
|
+
expect(page.response_headers["Content-Type"]).to include(content_type)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Then("the response should contain {string}") do |text|
|
|
20
|
+
expect(page.body).to include(text)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Then("the response should have caching headers") do
|
|
24
|
+
expect(page.response_headers["Cache-Control"]).to match(/max-age=\d+/)
|
|
25
|
+
expect(page.response_headers["Cache-Control"]).to include("public")
|
|
26
|
+
expect(page.response_headers["ETag"]).to be_present
|
|
27
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
When("I visit the dummy engine root") do
|
|
4
|
+
visit "/dummy"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Then("the page should have a stylesheet link to {string}") do |path|
|
|
8
|
+
expect(page).to have_css("link[rel='stylesheet'][href^='#{path}']", visible: false)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Then("the stylesheet link should include a fingerprint parameter") do
|
|
12
|
+
link = page.find("link[rel='stylesheet']", visible: false)
|
|
13
|
+
expect(link[:href]).to match(/\?v=[a-f0-9]{8}/)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Then("the page should have an import map") do
|
|
17
|
+
expect(page).to have_css("script[type='importmap']", visible: false)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Then("the import map should include {string}") do |key|
|
|
21
|
+
script = page.find("script[type='importmap']", visible: false)
|
|
22
|
+
import_map = JSON.parse(script.text(:all))
|
|
23
|
+
expect(import_map["imports"]).to have_key(key)
|
|
24
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Feature: View Helpers
|
|
2
|
+
As a Rails engine developer
|
|
3
|
+
I want view helpers to generate asset tags
|
|
4
|
+
So that I can easily include my engine's assets in views
|
|
5
|
+
|
|
6
|
+
Scenario: Stylesheet link tag includes fingerprint
|
|
7
|
+
When I visit the dummy engine root
|
|
8
|
+
Then the page should have a stylesheet link to "/dummy/assets/application.css"
|
|
9
|
+
And the stylesheet link should include a fingerprint parameter
|
|
10
|
+
|
|
11
|
+
Scenario: Import map includes engine JavaScript files
|
|
12
|
+
When I visit the dummy engine root
|
|
13
|
+
Then the page should have an import map
|
|
14
|
+
And the import map should include "dummy/application"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "appraisal"
|
|
7
|
+
gem "rails", "~> 7.2.0"
|
|
8
|
+
|
|
9
|
+
group :test do
|
|
10
|
+
gem "rspec", "~> 3.0"
|
|
11
|
+
gem "cucumber", "~> 9.0"
|
|
12
|
+
gem "capybara", "~> 3.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../", name: "isolate_assets"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "appraisal"
|
|
7
|
+
gem "rails", "~> 8.0.0"
|
|
8
|
+
|
|
9
|
+
group :test do
|
|
10
|
+
gem "rspec", "~> 3.0"
|
|
11
|
+
gem "cucumber", "~> 9.0"
|
|
12
|
+
gem "capybara", "~> 3.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../", name: "isolate_assets"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "appraisal"
|
|
7
|
+
gem "rails", "~> 8.1.0"
|
|
8
|
+
|
|
9
|
+
group :test do
|
|
10
|
+
gem "rspec", "~> 3.0"
|
|
11
|
+
gem "cucumber", "~> 9.0"
|
|
12
|
+
gem "capybara", "~> 3.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../", name: "isolate_assets"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IsolateAssets
|
|
4
|
+
class Assets
|
|
5
|
+
attr_reader :engine, :assets_subdir
|
|
6
|
+
|
|
7
|
+
def initialize(engine:, assets_subdir: "engine_assets")
|
|
8
|
+
@engine = engine
|
|
9
|
+
@assets_subdir = assets_subdir
|
|
10
|
+
@fingerprints = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
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
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def asset_url(source, type)
|
|
23
|
+
fingerprint_value = fingerprint(source, type)
|
|
24
|
+
engine.routes.url_helpers.isolated_asset_path("#{source}.#{normalize_type(type)}", v: fingerprint_value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def fingerprint(source, type)
|
|
28
|
+
cache_key = "#{source}.#{type}"
|
|
29
|
+
|
|
30
|
+
if ::Rails.env.production?
|
|
31
|
+
@fingerprints[cache_key] ||= calculate_fingerprint(source, type)
|
|
32
|
+
else
|
|
33
|
+
calculate_fingerprint(source, type)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
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
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def javascript_files
|
|
49
|
+
engine.root.glob("app/#{assets_subdir}/javascripts/**/*.js")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def normalize_type(type)
|
|
55
|
+
case type.to_s
|
|
56
|
+
when "javascript" then "js"
|
|
57
|
+
when "stylesheet" then "css"
|
|
58
|
+
else type.to_s
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def calculate_fingerprint(source, type)
|
|
63
|
+
file_path = asset_path(source, type)
|
|
64
|
+
|
|
65
|
+
if file_path && File.exist?(file_path)
|
|
66
|
+
Digest::SHA256.file(file_path).hexdigest[0...8]
|
|
67
|
+
else
|
|
68
|
+
"missing"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IsolateAssets
|
|
4
|
+
class Controller < ActionController::API
|
|
5
|
+
include ActionController::MimeResponds
|
|
6
|
+
|
|
7
|
+
class_attribute :isolated_assets
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
file_path = safe_file_path
|
|
11
|
+
|
|
12
|
+
if file_path && File.exist?(file_path)
|
|
13
|
+
expires_in 1.year, public: true
|
|
14
|
+
fresh_when(etag: File.mtime(file_path), public: true)
|
|
15
|
+
|
|
16
|
+
send_file file_path,
|
|
17
|
+
type: content_type,
|
|
18
|
+
disposition: "inline"
|
|
19
|
+
else
|
|
20
|
+
head :not_found
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def safe_file_path
|
|
27
|
+
requested = params[:file].gsub("..", "")
|
|
28
|
+
format = params[:format] || request.format.symbol.to_s
|
|
29
|
+
isolated_assets.asset_path(requested, format)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def content_type
|
|
33
|
+
format = params[:format] || request.format.symbol.to_s
|
|
34
|
+
isolated_assets.content_type(format)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IsolateAssets
|
|
4
|
+
module EngineExtension
|
|
5
|
+
def isolate_assets(assets_subdir: "engine_assets")
|
|
6
|
+
engine_class = self
|
|
7
|
+
|
|
8
|
+
# Create a unique controller class for this engine
|
|
9
|
+
controller_class = Class.new(IsolateAssets::Controller)
|
|
10
|
+
|
|
11
|
+
# Register an initializer to set up assets when Rails boots
|
|
12
|
+
initializer "#{engine_name}.isolate_assets", before: :set_routes_reloader do
|
|
13
|
+
assets = IsolateAssets::Assets.new(engine: engine_class, assets_subdir: assets_subdir)
|
|
14
|
+
|
|
15
|
+
# Create a unique helper module for this engine with the assets baked in
|
|
16
|
+
helper_module = Module.new do
|
|
17
|
+
define_method(:isolated_assets) { assets }
|
|
18
|
+
include IsolateAssets::Helper
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Wire up the controller
|
|
22
|
+
controller_class.isolated_assets = assets
|
|
23
|
+
|
|
24
|
+
# Store on the engine's namespace module
|
|
25
|
+
if engine_class.respond_to?(:railtie_namespace) && engine_class.railtie_namespace
|
|
26
|
+
engine_class.railtie_namespace.singleton_class.define_method(:isolated_assets) { assets }
|
|
27
|
+
engine_class.railtie_namespace.singleton_class.define_method(:isolated_assets_helper) { helper_module }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Draw routes
|
|
31
|
+
engine_class.routes.prepend do
|
|
32
|
+
get "/assets/*file", to: controller_class.action(:show), as: :isolated_asset
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IsolateAssets
|
|
4
|
+
module Helper
|
|
5
|
+
# Note: isolated_assets method is defined by the including module,
|
|
6
|
+
# created dynamically in EngineExtension#isolate_assets
|
|
7
|
+
|
|
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)
|
|
19
|
+
tag.link(
|
|
20
|
+
rel: "stylesheet",
|
|
21
|
+
href: engine_asset_url(source, "css"),
|
|
22
|
+
**options
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def engine_javascript_include_tag(source, **options)
|
|
27
|
+
tag.script(
|
|
28
|
+
src: engine_asset_url(source, "js"),
|
|
29
|
+
**options
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def engine_javascript_importmap_tags(entry_point = "application", imports = {})
|
|
34
|
+
assets_root = isolated_assets.engine.root.join("app/#{isolated_assets.assets_subdir}/javascripts")
|
|
35
|
+
engine_imports = isolated_assets.javascript_files.each_with_object({}) do |path, hash|
|
|
36
|
+
relative_path = path.relative_path_from(assets_root).to_s
|
|
37
|
+
key = "#{isolated_assets.engine.engine_name}/#{relative_path.sub(/\.js\z/, "")}"
|
|
38
|
+
hash[key] = engine_asset_url(relative_path.sub(/\.js\z/, ""), "js")
|
|
39
|
+
end
|
|
40
|
+
[
|
|
41
|
+
tag.script(type: "importmap") do
|
|
42
|
+
JSON.pretty_generate({"imports" => imports.merge(engine_imports)}).html_safe
|
|
43
|
+
end,
|
|
44
|
+
tag.script(<<~JS.html_safe, type: "module")
|
|
45
|
+
import "#{isolated_assets.engine.engine_name}/#{entry_point}"
|
|
46
|
+
JS
|
|
47
|
+
].join("\n").html_safe
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha2"
|
|
4
|
+
require "active_support/core_ext/class/attribute"
|
|
5
|
+
require_relative "isolate_assets/version"
|
|
6
|
+
|
|
7
|
+
module IsolateAssets
|
|
8
|
+
autoload :Assets, "isolate_assets/assets"
|
|
9
|
+
autoload :Controller, "isolate_assets/controller"
|
|
10
|
+
autoload :Helper, "isolate_assets/helper"
|
|
11
|
+
autoload :EngineExtension, "isolate_assets/engine_extension"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Extend Rails::Engine with isolate_assets
|
|
15
|
+
Rails::Engine.extend(IsolateAssets::EngineExtension)
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: isolate_assets
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Micah Geisel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.2'
|
|
27
|
+
description: Serve JavaScript, CSS, and other assets from your Rails engine without
|
|
28
|
+
depending on Sprockets, Propshaft, or the host application's asset pipeline.
|
|
29
|
+
email:
|
|
30
|
+
- micah@botandrose.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".github/workflows/ci.yml"
|
|
36
|
+
- Appraisals
|
|
37
|
+
- LICENSE
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- cucumber.yml
|
|
41
|
+
- features/asset_serving.feature
|
|
42
|
+
- features/step_definitions/asset_steps.rb
|
|
43
|
+
- features/step_definitions/view_helper_steps.rb
|
|
44
|
+
- features/support/env.rb
|
|
45
|
+
- features/view_helpers.feature
|
|
46
|
+
- gemfiles/rails_7_2.gemfile
|
|
47
|
+
- gemfiles/rails_8_0.gemfile
|
|
48
|
+
- gemfiles/rails_8_1.gemfile
|
|
49
|
+
- lib/isolate_assets.rb
|
|
50
|
+
- lib/isolate_assets/assets.rb
|
|
51
|
+
- lib/isolate_assets/controller.rb
|
|
52
|
+
- lib/isolate_assets/engine_extension.rb
|
|
53
|
+
- lib/isolate_assets/helper.rb
|
|
54
|
+
- lib/isolate_assets/version.rb
|
|
55
|
+
homepage: https://github.com/botandrose/isolate_assets
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
homepage_uri: https://github.com/botandrose/isolate_assets
|
|
60
|
+
source_code_uri: https://github.com/botandrose/isolate_assets
|
|
61
|
+
post_install_message:
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 3.2.0
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.4.19
|
|
77
|
+
signing_key:
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Self-contained asset serving for Rails engines
|
|
80
|
+
test_files: []
|