Skip to content

Commit 8f4c88a

Browse files
authoredMar 2, 2025··
Add Propshaft support (#1402)
* Add propshaft support
1 parent ae28afc commit 8f4c88a

27 files changed

+828
-736
lines changed
 

‎Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
33

44
gemspec
55

6+
# Assets for the dummy app (can also be sprockets, but propshaft is the new default)
7+
gem "propshaft"
8+
69
group :development do
710
gem "letter_opener"
811
end

‎Gemfile.lock

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ PATH
1616
pg
1717
rack-rewrite (>= 1.5.0)
1818
rails (>= 6.0, < 8.0)
19-
sprockets-rails
2019
stimulus-rails (>= 0.7.0)
2120
tailwindcss-ruby
2221
turbo-rails (>= 0.9, < 3.0)
@@ -228,6 +227,11 @@ GEM
228227
nokogiri (1.16.7-x86_64-linux)
229228
racc (~> 1.4)
230229
pg (1.5.9)
230+
propshaft (1.1.0)
231+
actionpack (>= 7.0.0)
232+
activesupport (>= 7.0.0)
233+
rack
234+
railties (>= 7.0.0)
231235
pry (0.15.0)
232236
coderay (~> 1.1)
233237
method_source (~> 1.0)
@@ -305,13 +309,6 @@ GEM
305309
json (>= 1.8, < 3)
306310
simplecov-html (~> 0.10.0)
307311
simplecov-html (0.10.2)
308-
sprockets (4.2.1)
309-
concurrent-ruby (~> 1.0)
310-
rack (>= 2.2.4, < 4)
311-
sprockets-rails (3.5.2)
312-
actionpack (>= 6.1)
313-
activesupport (>= 6.1)
314-
sprockets (>= 3.0.0)
315312
stimulus-rails (1.3.4)
316313
railties (>= 6.0.0)
317314
stringio (3.1.2)
@@ -364,6 +361,7 @@ DEPENDENCIES
364361
letter_opener
365362
minitest-reporters
366363
mocha
364+
propshaft
367365
pry-rails
368366
puma
369367
rails-controller-testing

‎app/assets/config/spina/manifest.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//= link_directory ../../javascripts/spina/libraries
66

77
//= link spina/animate.css
8-
//= link spina/fonts.css
8+
//= link spina/fonts-sprockets.css
99
//= link spina/tailwind.css
1010

11-
//= link spina/application.js
11+
//= link spina/application.js
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import "@hotwired/turbo-rails"
22
import "libraries/trix"
3-
import "controllers"
3+
import "controllers"

‎app/assets/javascripts/spina/controllers/confetti_controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ export default class extends Controller {
4040
return document.querySelector("#confetti")
4141
}
4242

43-
}
43+
}
Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,234 @@
1-
//= require ../libraries/stimulus-data-bindings@1.3.2.js
1+
import { Controller } from '@hotwired/stimulus'
2+
3+
/**
4+
* One way data and visibility bindings for inputs
5+
* @extends Controller
6+
*/
7+
export default class DataBindingController extends Controller {
8+
/**
9+
* Initialize bindings on connection to the DOM
10+
*/
11+
connect() {
12+
if (this.element.dataset.bindingDebug === "true") {
13+
this.debugMode = true
14+
}
15+
16+
this._debug("stimulus-data-binding: connecting to wrapper:", this.element)
17+
18+
const sourceElements = Array.from(this.element.querySelectorAll('[data-binding-target]'))
19+
if (this.element.dataset.bindingTarget) sourceElements.unshift(this.element)
20+
21+
if (sourceElements.length === 0) this._debug("No source elements found. Did you set data-binding-target on your source elements?")
22+
23+
for (const sourceElement of sourceElements) {
24+
if (this.debugMode) console.group("stimulus-data-binding: Source element")
25+
this._debug("Source element found", sourceElement)
26+
27+
if (sourceElement.dataset.bindingInitial !== 'false') {
28+
this._debug("Running initial binding on source element")
29+
this._runBindings(sourceElement)
30+
} else {
31+
this._debug("%cNot running initial binding on source element as binding-initial is set to false", "color: rgba(150,150,150,0.8);")
32+
}
33+
34+
if (this.debugMode) console.groupEnd()
35+
}
36+
}
37+
38+
/**
39+
* Updates bindings for the current element.
40+
* @param {Event} e - an event with a currentTarget DOMElement
41+
*/
42+
update(e) {
43+
this._runBindings(e.currentTarget)
44+
}
45+
46+
/**
47+
* @private
48+
* @param {DOMElement} source
49+
*/
50+
_runBindings(source) {
51+
this._debug("Searching for targets for source: ", source)
52+
for (const targetRef of source.dataset.bindingTarget.split(' ')) {
53+
const targetElements = this._bindingElements(targetRef)
54+
55+
if (targetElements.length === 0) this._debug(`Could not find any target elements for ref ${targetRef}. Have you set data-target-ref="${targetRef}" on your target elements?`)
56+
57+
for (const target of targetElements) {
58+
if (this.debugMode) console.group("stimulus-data-binding: Target Element")
59+
this._debug("Target found. Running bindings for target: ", target)
60+
61+
const bindingCondition = this._getDatum('bindingCondition', source, target)
62+
63+
if (bindingCondition) {
64+
this._debug(`Evaluating binding condition: '${bindingCondition}'`)
65+
} else {
66+
this._debug(`%cNo binding condition set. Evaluating as true. To add a condition set 'data-binding-condition="..."'`, "color: rgba(150,150,150,0.8);")
67+
}
68+
69+
const conditionPassed = this._evaluate(
70+
bindingCondition,
71+
{
72+
source,
73+
target
74+
}
75+
)
76+
77+
if (conditionPassed) {
78+
this._debug(`Condition evaluated to: `, conditionPassed)
79+
} else {
80+
this._debug(`Condition evaluated to: `, conditionPassed)
81+
}
82+
83+
const bindingValue = this._getDatum('bindingValue', source, target)
84+
85+
if (bindingValue) {
86+
this._debug(`Evaluating binding value: '${bindingValue}'`)
87+
} else {
88+
this._debug(`%cNo binding value set, evaluating as true. to set a value for the attribute / property on your target elements set 'data-binding-value="..."'`, "color: rgba(150,150,150,0.8);")
89+
}
90+
91+
const value = this._evaluate(
92+
bindingValue,
93+
{
94+
source,
95+
target
96+
}
97+
)
98+
99+
this._debug(`Value evaluated to: '${value}'`)
100+
101+
const bindingAttribute = this._getDatum(
102+
'bindingAttribute',
103+
source,
104+
target
105+
)
106+
107+
if (!bindingAttribute) {
108+
this._debug(`%cNo binding attribute set. To add attributes to your target element set 'data-binding-attribute="..."'`, "color: rgba(150,150,150,0.8);")
109+
}
110+
111+
if (bindingAttribute) {
112+
for (const attribute of bindingAttribute.split(' ')) {
113+
if (conditionPassed) {
114+
this._debug(`Condition passed so setting attribute '${attribute}' to '${value}'`)
115+
target.setAttribute(attribute, value)
116+
} else {
117+
this._debug(`Condition failed so removing attribute '${attribute}'`)
118+
target.removeAttribute(attribute)
119+
}
120+
}
121+
}
122+
123+
const bindingProperty = this._getDatum(
124+
'bindingProperty',
125+
source,
126+
target
127+
)
128+
129+
if (!bindingProperty) {
130+
this._debug(`%cNo binding property set. To add properties to your target element set 'data-binding-property="..."'`, "color: rgba(150,150,150,0.8);")
131+
}
132+
133+
if (bindingProperty) {
134+
for (const prop of bindingProperty.split(' ')) {
135+
const propertyValue = conditionPassed ? value : ''
136+
if (target[prop] != propertyValue) { target.dataset.hasChanged = true }
137+
138+
if (conditionPassed) {
139+
this._debug(`Condition passed so setting property '${prop}' from ${target[prop]} to '${value}'`)
140+
} else {
141+
this._debug(`Condition failed so setting property '${prop}' from ${target[prop]} to '' (empty string)`)
142+
}
143+
144+
target[prop] = propertyValue
145+
}
146+
}
147+
148+
const bindingClass = this._getDatum(
149+
'bindingClass',
150+
source,
151+
target
152+
)
153+
154+
if (!bindingClass) {
155+
this._debug(`%cNo binding class set. To add classes to your target element set 'data-binding-class="..."'`, "color: rgba(150,150,150,0.8);")
156+
}
157+
158+
if (bindingClass) {
159+
for (const klass of bindingClass.split(' ')) {
160+
if (conditionPassed) {
161+
this._debug(`Condition passed so adding class '${klass}'`)
162+
target.classList.add(klass)
163+
} else {
164+
this._debug(`Condition failed so removing class '${klass}'`)
165+
target.classList.remove(klass)
166+
}
167+
}
168+
}
169+
170+
const bindingEvent = this._getDatum('bindingEvent', source, target)
171+
172+
if (!bindingEvent) {
173+
this._debug(`%cNo binding event set. To dispatch events on property change to your target element set 'data-binding-event="..."'`, "color: rgba(150,150,150,0.8);")
174+
}
175+
176+
if (bindingEvent) {
177+
for (const event of bindingEvent.split(' ')) {
178+
if (target.dataset.hasChanged) {
179+
this._debug(`Target has changed so dispatching event ${event}`)
180+
target.dispatchEvent(new Event(event, { cancelable: true, bubbles: true }))
181+
delete target.dataset.hasChanged
182+
} else {
183+
this._debug(`No changes to target properties so not dispatching '${event}'`)
184+
}
185+
}
186+
}
187+
188+
if (this.debugMode) console.groupEnd()
189+
}
190+
}
191+
}
192+
193+
/**
194+
* @private
195+
* @param {String} name - the name of the binding reference
196+
*/
197+
_bindingElements(name) {
198+
return this.element.querySelectorAll(`[data-binding-ref="${name}"]`)
199+
}
200+
201+
/**
202+
* @private
203+
* @param {String} attribute - the attribute to fetch from the source / target dataaset
204+
* @param {String} source - The source element to get the attribute from, only loads if target doesnt have it
205+
* @param {String} target - The target element to get the attribute from, has precedence over the source
206+
*/
207+
_getDatum(attribute, source, target) {
208+
return target.dataset[attribute] || source.dataset[attribute]
209+
}
210+
211+
/**
212+
* @private
213+
* @param {String} expression - expression to safe eval
214+
* @param {Object} variables - variables to be present when evaluating the given expression
215+
*/
216+
_evaluate(expression, variables = {}) {
217+
if (!expression) return true
218+
return new Function(
219+
Object.keys(variables).map((v) => `$${v}`),
220+
`return ${expression.trim()}`
221+
)(...Object.values(variables))
222+
}
223+
224+
/**
225+
* @private
226+
* @param {String} expression - expression to safe eval
227+
* @param {Object} variables - variables to be present when evaluating the given expression
228+
*/
229+
_debug(...args) {
230+
if (this.debugMode) {
231+
console.log(...args)
232+
}
233+
}
234+
}

‎app/assets/javascripts/spina/controllers/form_controller.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Controller } from "@hotwired/stimulus"
22
import debounce from "libraries/debounce"
3-
import formRequestSubmitPolyfill from "libraries/form-request-submit-polyfill"
43

54
export default class extends Controller {
65

@@ -20,4 +19,4 @@ export default class extends Controller {
2019
return this.element.dataset.debounceTime || 0
2120
}
2221

23-
}
22+
}

‎app/assets/javascripts/spina/controllers/hotkeys_controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ export default class extends Controller {
1818
}
1919
}
2020

21-
}
21+
}

‎app/assets/javascripts/spina/controllers/reveal_controller.js

Lines changed: 424 additions & 1 deletion
Large diffs are not rendered by default.

‎app/assets/javascripts/spina/controllers/sortable_controller.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Controller } from "@hotwired/stimulus"
22
import Sortable from "libraries/sortablejs"
3-
import formRequestSubmitPolyfill from "libraries/form-request-submit-polyfill"
43

54
export default class extends Controller {
65
static get targets() {
@@ -36,4 +35,4 @@ export default class extends Controller {
3635
return this.sortable.toArray()
3736
}
3837

39-
}
38+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
//= require ./canvas-confetti@1.3.2.js
1+
export { default } from "libraries/canvas-confetti@1.3.2"

‎app/assets/javascripts/spina/libraries/form-request-submit-polyfill.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎app/assets/javascripts/spina/libraries/form-request-submit-polyfill@2.0.0.js

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
//= require ./hotkeys@3.8.7.js
1+
export { default } from "libraries/hotkeys@3.8.7"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
//= require ./sortablejs@1.13.0.js
1+
export { default } from "libraries/sortablejs@1.13.0"

‎app/assets/javascripts/spina/libraries/stimulus-data-bindings@1.3.2.js

Lines changed: 0 additions & 234 deletions
This file was deleted.

‎app/assets/javascripts/spina/libraries/stimulus-reveal@1.4.2.js

Lines changed: 0 additions & 424 deletions
This file was deleted.

‎app/assets/javascripts/spina/libraries/trix.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//= require ./trix@1.3.1.esm.js
1+
import "libraries/trix@1.3.1.esm"
22

33
// Extra headings
44
Trix.config.blockAttributes.heading2 = {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
@font-face {
2+
font-family: "Metropolis";
3+
font-weight: 400;
4+
src: url('Metropolis-Regular.woff2') format("woff2");
5+
}
6+
7+
@font-face {
8+
font-family: "Metropolis";
9+
font-style: italic;
10+
src: url('Metropolis-RegularItalic.woff2') format("woff2");
11+
}
12+
13+
@font-face {
14+
font-family: "Metropolis";
15+
font-weight: 100;
16+
src: url('Metropolis-Thin.woff2') format("woff2");
17+
}
18+
19+
@font-face {
20+
font-family: "Metropolis";
21+
font-weight: 100;
22+
font-style: italic;
23+
src: url('Metropolis-ThinItalic.woff2') format("woff2");
24+
}
25+
26+
@font-face {
27+
font-family: "Metropolis";
28+
font-weight: 200;
29+
src: url('Metropolis-ExtraLight.woff2') format("woff2");
30+
}
31+
32+
@font-face {
33+
font-family: "Metropolis";
34+
font-weight: 200;
35+
src: url('Metropolis-ExtraLightItalic.woff2') format("woff2");
36+
}
37+
38+
@font-face {
39+
font-family: "Metropolis";
40+
font-weight: 300;
41+
src: url('Metropolis-Light.woff2') format("woff2");
42+
}
43+
44+
@font-face {
45+
font-family: "Metropolis";
46+
font-weight: 300;
47+
font-style: italic;
48+
src: url('Metropolis-LightItalic.woff2') format("woff2");
49+
}
50+
51+
@font-face {
52+
font-family: "Metropolis";
53+
font-weight: 500;
54+
src: url('Metropolis-Medium.woff2') format("woff2");
55+
}
56+
57+
@font-face {
58+
font-family: "Metropolis";
59+
font-weight: 500;
60+
font-style: italic;
61+
src: url('Metropolis-MediumItalic.woff2') format("woff2");
62+
}
63+
64+
@font-face {
65+
font-family: "Metropolis";
66+
font-weight: 600;
67+
src: url('Metropolis-SemiBold.woff2') format("woff2");
68+
}
69+
70+
@font-face {
71+
font-family: "Metropolis";
72+
font-weight: 600;
73+
font-style: italic;
74+
src: url('Metropolis-SemiBoldItalic.woff2') format("woff2");
75+
}
76+
77+
@font-face {
78+
font-family: "Metropolis";
79+
font-weight: 700;
80+
src: url('Metropolis-Bold.woff2') format("woff2");
81+
}
82+
83+
@font-face {
84+
font-family: "Metropolis";
85+
font-weight: 700;
86+
font-style: italic;
87+
src: url('Metropolis-BoldItalic.woff2') format("woff2");
88+
}
89+
90+
@font-face {
91+
font-family: "Metropolis";
92+
font-weight: 800;
93+
src: url('Metropolis-ExtraBold.woff2') format("woff2");
94+
}
95+
96+
@font-face {
97+
font-family: "Metropolis";
98+
font-weight: 800;
99+
font-style: italic;
100+
src: url('Metropolis-ExtraBoldItalic.woff2') format("woff2");
101+
}
102+
103+
@font-face {
104+
font-family: "Metropolis";
105+
font-weight: 900;
106+
src: url('Metropolis-Black.woff2') format("woff2");
107+
}
108+
109+
@font-face {
110+
font-family: "Metropolis";
111+
font-weight: 900;
112+
font-style: italic;
113+
src: url('Metropolis-BlackItalic.woff2') format("woff2");
114+
}

‎app/helpers/spina/admin/pages_helper.rb

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module Spina::Admin
22
module PagesHelper
33
def asset_available?(path)
4-
if Rails.configuration.assets.compile
5-
Rails.application.precompiled_assets.include?(path)
6-
else
7-
Rails.application.assets_manifest.assets[path].present?
4+
if defined?(Propshaft)
5+
check_propshaft_asset(path)
6+
elsif defined?(Sprockets)
7+
check_sprockets_asset(path)
88
end
99
end
1010

@@ -24,5 +24,24 @@ def parts_partial_namespace(part_type)
2424
def option_label(part, value)
2525
t(["options", part.name, value].compact.join("."))
2626
end
27+
28+
private
29+
30+
def check_propshaft_asset(path)
31+
if Rails.configuration.assets.compile
32+
Rails.application.assets.load_path.find(path).present? rescue false
33+
else
34+
Rails.application.assets.asset_for(path).present? rescue false
35+
end
36+
end
37+
38+
def check_sprockets_asset(path)
39+
if Rails.configuration.assets.compile
40+
Rails.application.precompiled_assets.include?(path)
41+
else
42+
Rails.application.assets_manifest.assets[path].present?
43+
end
44+
end
45+
2746
end
2847
end

‎app/views/layouts/spina/admin/application.html.erb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010

1111
<title><%= Spina.config.backend_title %></title>
1212

13+
<!-- Fonts -->
14+
<% if defined?(Propshaft) %>
15+
<%= stylesheet_link_tag "spina/fonts-propshaft", data: {turbo_track: "reload"} %>
16+
<% elsif defined?(Sprockets) %>
17+
<%= stylesheet_link_tag "spina/fonts-sprockets", data: {turbo_track: "reload"} %>
18+
<% end %>
19+
1320
<!-- Stylesheets -->
14-
<%= stylesheet_link_tag "spina/tailwind", "spina/fonts", "spina/animate", "data-turbo-track": "reload" %>
21+
<%= stylesheet_link_tag "spina/tailwind", "spina/animate", "data-turbo-track": "reload" %>
1522

1623
<!-- Spina's importmap -->
1724
<%= spina_importmap_tags %>
@@ -25,4 +32,4 @@
2532
<%= render "spina/admin/shared/version" %>
2633
<%= content_for?(:body) ? yield(:body) : yield %>
2734
</body>
28-
</html>
35+
</html>

‎lib/spina/engine.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
require "view_component"
1212
require "jsonapi/serializer"
1313
require "browser"
14-
require "sprockets/railtie"
1514

1615
module Spina
1716
class Engine < ::Rails::Engine

‎lib/spina/railtie.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
module Spina
22
class Railtie < Rails::Railtie
33
initializer "spina.assets.precompile" do |app|
4-
app.config.assets.precompile += %w[spina/manifest]
4+
if defined?(Sprockets)
5+
# Sprockets configuration
6+
app.config.assets.precompile += %w[spina/manifest]
7+
end
58
end
69

710
initializer "spina.theme_reloader" do |app|

‎spina.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ Gem::Specification.new do |gem|
3434
gem.files = Dir["{app,config,db,lib,vendor}/**/*"] + ["Rakefile", "README.md"]
3535

3636
gem.add_dependency "rails", ">= 6.0", "< 8.0"
37-
gem.add_dependency "sprockets-rails"
3837
gem.add_dependency "pg"
3938
gem.add_dependency "bcrypt"
4039
gem.add_dependency "image_processing"

‎test/dummy/app/assets/config/manifest.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

‎test/dummy/app/javascript/packs/application.js

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.