Extending Sprockets - ruby-on-rails

I want to extend sprockets, and I think I want to do post-processing.
When in development, after application.css is created, I want to use css-flip https://github.com/twitter/css-flip to make a rtl copy of application.css, eg application.rtl.css bidi support.
Right now, I'm just trying to get something working.
I created config/initializers/sprockets.rb
require "hello_world_processor"
Sprockets.register_preprocessor('application/javascript', HelloWorldProcessor.new)
Then lib/hello_world_processor.rb
class HelloWorldProcessor
def call(input)
puts "hello world"
return { data: input[:data] + "\n'hello world'" }
end
end
I was hoping to at least see a "hello world" in the server logs, but no luck.
Anything jumping out to you as totally wrong?

Related

How to set up ActionCable with Importmaps in Rails 6? (MRI and Jruby)

Actioncable - an overview.
I am using jruby 9.2.16 (hence ruby 2.5.7) with rails 6.1.6.1.
I am not sure if only in development or only without ssl (wss) Actioncable can be used with the simple client side:
var ws = new WebSocket('ws://0.0.0.0:3000/channels');
ws.onmessage = function(e){ console.log(e.data); }
But at least I didn't get it running to do "Streaming from Channel" using wss in production, as it works locally (visible starting 'redis-cli' in terminal, and then 'monitor').
So I tried to implement the actioncable client scripts and hence 8 days got lost.
First I struggled with the fact that there is no description which is somehow complete. Many people publish particular solutions, but this is like gambling: maybe you are lucky.
Second, files are named in a way that seems to be general even though they are only about actioncable (the folder 'javascript', or 'application.js')
It is misleading to not call them 'actioncable_files' and 'actioncable_app.js' and voilà there are problems because of several files with the same name.
The next problem is, that lots has to be changed only, because the orderly structure of files is ignored. Javascripts no longer are in assets, but why?
They could be in assets/javascripts/actioncable/ ?
So manifest.js has to be changed, and even attributes have to be added in application.rb (attr_accessor :importmap). Something you find after some days in a forum.
but even more confusing: importmap is a gem, which requires some directories and which somehow has to be installed (rake app:update:bin, rails importmap:install) and someone wrote, the order of the gems were relevant but you cannot only deinstall actioncable gem, because rails depends on that. Dependencies could be organized using a permutation of priority.
importmaps is not working in firefox, so you need shims additionally
But in the end, everything what importmap does, looks like reinventing the wheel to me: it loads javascript files. Something, that can be done easily manually.
Also importing modules is possible in simple javascript. And what else then a javascript file shall it be, that the browser finally will work with?
Now importmap creates from a string '#rails/actioncable' another string 'actioncable.esm.js', which in my case (after 8 days of working 15 h to get it to work, still is not found automatically.
I cannot find that file, or any description where it is generated, or if it is only a link, or has to be compiled somehwere, but it looks to me, as if importmap is completely redundant and only makes things very complicated. I don't understand the benefit from writing a string 'xyz' which is translated in a laborious way to set up into another string 'xyz_2', which also might not be found. And if there are variables, they can be loaded directly using the same idea like action_cable_meta_tag.
Technically Actioncable is only doing what Faye did before. So why do we need all that so called "modern" way, which I think only is reinventing the wheel?
So, I would like to create a description on how to install actioncable in an easy way - without unnecessary tools and clearly.
But first I need to get it to work on my own. And therefore the question is: what shall I do due to:
GET http://0.0.0.0:3000/actioncable.esm.js net::ERR_ABORTED 404 (Not Found)
Thanks everyone for any idea!
I've never set up ActionCable before and never used it (except second hand through Turbo::Broadcastable). Seems like you had quite the journey. I used rails guides and rails github to set it up.
First of all, from importmap-rails:
Note: In order to use JavaScript from Rails frameworks like Action Cable, Action Text, and Active Storage, you must be running Rails 7.0+. This was the first version that shipped with ESM compatible builds of these libraries.
https://github.com/rails/importmap-rails#installation
Challenge accepted. I'll use mri for now (I'll try with your versions later, to see if anything weird comes up).
ActionCable
$ rails _6.1.6.1_ new cable --skip-javascript
$ cd cable
# https://github.com/rails/importmap-rails#installation
$ bin/bundle add importmap-rails
$ bin/rails importmap:install
# ActionCable guide seems rather crusty. Until this section:
# https://guides.rubyonrails.org/v6.1/action_cable_overview.html#connect-consumer
# A generator for the client side js is mentioned in the code comment.
$ bin/rails g channel chat
# Oops, generated server side rb as well.
# This should really be at the start of the guide.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
# NOTE: just keep it simple
stream_from "some_channel"
end
end
The directory app/javascript seems generic, because it is. This is for all Javascript stuff, used by shakapacker, jsbundling-rails, importmap-rails and others. I've described it a bit here: https://stackoverflow.com/a/73174481/207090
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
// NOTE: We have to check if our set up is online first,
// before chatting and authenticating or anything else.
console.log("ChatChannel connected")
},
disconnected() {},
received(data) {}
});
To broadcast a message, call this somewhere in your app [sic]:
ActionCable.server.broadcast("some_channel", "some message")
Ok, we have to make a controller anyway:
$ bin/rails g scaffold Message content
$ bin/rails db:migrate
$ open http://localhost:3000/messages
$ bin/rails s
Also, channels have to be imported somewhere, to load on the page. javascript_importmap_tags in the layout only imports application:
https://github.com/rails/importmap-rails#usage
<script type="module">import "application"</script>
<!-- ^ -->
<!-- this imports the pinned `application` -->
Makes sense to import channels in application.js. Can't import ./channels/index because it has require. We'd have to use node for it to work or do something else to import all the channels. Manual way is the simplest:
// app/javascript/channels/index.js
// NOTE: it works a little differently with importmaps that I haven't mentioned yet.
// skip this index file for now, and import channels in application.js
// app/javascript/application.js
import "./channels/chat_channel"
Browser console shows missing #rails/actioncable. Nobody told me to install it yet. Use pin command to add it:
https://github.com/rails/importmap-rails#using-npm-packages-via-javascript-cdns
$ bin/importmap pin #rails/actioncable
Refresh the browser:
ChatChannel connected chat_channel:5
We got our javascript on the page. Let's make it broadcast:
# app/controllers/messages_controller.rb
# POST /messages
def create
#message = Message.create(message_params)
ActionCable.server.broadcast("some_channel", #message.content)
end
<!-- app/views/messages/index.html.erb -->
<div id="chat"></div> <!-- output for broadcasted messages -->
<!-- since I have no rails ujs, for my purposes: bushcraft { remote: true } -->
<!-- v -->
<%= form_with model: Message.new, html: { onsubmit: "remote(event, this)" } do |f| %>
<%= f.text_field :content %>
<%= f.submit %>
<% end %>
<script type="text/javascript">
// you can skip this, I assume you have `rails_ujs` installed or `turbo`.
// { remote: true } or { local: false } is all you need on the form.
function remote(e, form) {
e.preventDefault();
fetch(form.action, {method: form.method, body: new FormData(form)})
form["message[content]"].value = ""
}
</script>
We know we're connected. The form is submitting to MessagesController#create without refreshing where we're broadcasting to "some_channel". All that's left is to do is output data on the page:
https://guides.rubyonrails.org/v6.1/action_cable_overview.html#client-server-interactions-subscriptions
// app/javascript/channels/chat_channel.js
// update received() function
received(data) {
document.querySelector("#chat")
.insertAdjacentHTML("beforeend", `<p>${data}</p>`)
}
ActionCable done. Now let's fix importmaps.
Importmaps
Something I didn't mention before and it is super important to understand.
Everything works, but, only in development, I explained why here:
https://stackoverflow.com/a/73136675/207090
URLs and relative or absolute paths will not be mapped and more importantly will bypass the asset pipeline sprockets. To actually use importmaps, all the files in app/javascript/channels have to be mapped, aka pinned, and then referred to only by the pinned name when importing.
# config/importmap.rb
# NOTE: luckily there is a command to help with bulk pins
pin_all_from "app/javascript/channels", under: "channels"
pin "application", preload: true
pin "#rails/actioncable", to: "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js"
# NOTE: the big reveal -> follow me ->------------------------------------------------------------------^^^^^^^^^^^^^^^^^^
# NOTE: this only works in rails 7+
# pin "#rails/actioncable", to: "actioncable.esm.js"
# `actioncable.esm.js` is in the asset pipeline so to speak and can be found here:
# https://github.com/rails/rails/tree/v7.0.3.1/actioncable/app/assets/javascripts
For some info on pin and pin_all_from:
https://stackoverflow.com/a/72855705/207090
You can see the importmaps this creates in the browser or in the terminal:
$ bin/importmap json
{
"imports": {
"application": "/assets/application-3ac17ae8a9bbfcdc9571d7ffac88746f5a76b18c149fdaf02fa7ed721b3e7c49.js",
"#rails/actioncable": "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js",
"channels": "/assets/channels/index-78e712d4a980790be34a2e859a2bd9a1121f9f3b508bd3f7de89889ff75828a0.js",
"channels/chat_channel": "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js",
"channels/consumer": "/assets/channels/consumer-b0ce945e7ae055dba9cceb062a47080dd9c7794a600762c19d38dbde3ba8ff0d.js"
}# ^ ^
} # | |
# names you use urls browser uses
# | to import ^ to actually get it
# | |
# `---> importmaped to ----'
For importmaps info (not the importmap-rails gem):
https://github.com/WICG/import-maps
Importmaps do not import anything, they map name to url. If you make name look like url with /name, ./name, ../name, http://js.cdn/name there is nothing to map.
import "channels/chat_channel"
// stays unchanged and is now the same as
import "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js"
// because we have an importmap for "channels/chat_channel"
You don't want to use the second form with an absolute path in you js files, because the digest hash changes on file updates to invalidate the browser cache (this is handled by sprockets).
Convert all the imports:
import consumer from "./consumer"
import "./channels/chat_channel"
to match the pinned names:
import consumer from "channels/consumer"
import "channels/chat_channel"
// import "channels" // is mapped to `channels/index`
// TODO: want to auto import channels in index file?
// just get all the pins named *_channel and import them,
// like stumulus-loading does for controllers:
// https://github.com/hotwired/stimulus-rails/blob/v1.1.0/app/assets/javascripts/stimulus-loading.js#L8
Jruby
Same setup on jruby. I just installed it and updated my Gemfile:
# Gemfile
ruby "2.5.7", engine: "jruby", engine_version: "9.2.16.0"
gem "activerecord-jdbcsqlite3-adapter"
gem "importmap-rails", "< 0.8" # after version 0.8.0 ruby >= 2.7 is required
First error, when starting the server:
NoMethodError: private method `importmap=' called for #<Cable::Application:0x496a31da>
importmap= method is defined here:
https://github.com/rails/importmap-rails/blob/v0.7.6/lib/importmap/engine.rb#L4
Rails::Application.send(:attr_accessor, :importmap)
In jruby it defines private methods when used this way:
>> A = Class.new
>> A.attr_accessor(:m)
>> A.new.m
NoMethodError (private method 'm' called for #<A:0x5f2f577>)
The fix is to override the definition in you app, or make those methods public:
# config/application.rb
module Cable
class Application < Rails::Application
# make them public
public :importmap, :importmap=
config.load_defaults 6.1
end
end
That's it. No other issues. You should expect some set backs anyway, because you're using jruby which is quite behind the mri. Ruby 2.5 EOL'd on Apr 05, 2021. You can't expect latest gems to play nice with old ruby versions.

Mixing Ruby and bash commands -- mv returns "x and y are the same file"

So I have a Ruby script (using Ruby because we have a library of pre-existing code that I need to use). From within Ruby I am using backticks to call Linux commands, specifically in this case the "mv" command. I am trying to move one file to another location but I keep getting the error message that x and y are "the same file" even though they are very clearly NOT the same file.
Here is the code in Ruby:
#!/usr/local/rvm/rubies/ruby-2.1.1/bin/ruby
masterFiles=[]
masterFiles << "/mnt/datadrive/Data Capture/QualityControl/UH_HRA_SVY/Scans and DataOutput/Data/UH_HRA_SVY_DATA.txt"
masterFiles << "/mnt/datadrive/Data Capture/QualityControl/UH_HRA_SVY_SPAN/Scans and DataOutput/Data/UH_HRA_SVY_SPAN_DATA.txt"
tm=Time.new.strftime("%Y%m%d")
masterFiles.each do |mf|
if File.exist?(mf)
qmf=39.chr + mf + 39.chr
`cat #{qmf} >> /tmp/QM`
savename=39.chr + \
"/mnt/datadrive/Data Capture/QualityControl/UH_HRA_SVY/Scans and DataOutput/Data/DailyFiles/" + \
File.basename(mf).gsub(".txt","_"+tm) + ".txt" + 39.chr
`mv #{qmf} #{savename}`
end
end
The error that I get is this:
mv: `/mnt/datadrive/Data Capture/QualityControl/UH_HRA_SVY_SPAN/Scans
and DataOutput/Data/UH_HRA_SVY_SPAN_DATA.txt' and `/mnt/datadrive/Data
Capture/QualityControl/UH_HRA_SVY/Scans and
DataOutput/Data/DailyFiles/UH_HRA_SVY_SPAN_DATA_20140530.txt' are the
same file
If I change this line:
`mv #{qmf} #{savename}`
To this:
puts "mv #{qmf} #{savename}"
And then run the output, it works as expected.
I am pretty sure that this has to do with spaces in the path. I have tried every combination of double-quoting, triple-quoting, quadruple-quoting, and back-slashing I can think of to resolve this but no go. I have also tried using FileUtils.mv but get what is basically the same error worded differently.
Can anybody help ? Thanks a lot.
p.s. I realize it's entirely possible that I could be going about this in an entirely wrong-headed way, so feel free to point that out if so. However, I am trying to use the tools which I already have some knowledge of (cat, mv, etc) instead of re-inventing the wheel.
You could use FileUtils.mv
I often do aliases like so:
require 'fileutils'
def mv(from, to)
FileUtils.mv(from, to)
end
And inside the mv() method I do additional safeguards, i.e. if the file does not exist, if there is a lack of permissions and so forth.
If you then still have problems with filenames that have ' ' blank characters, try to put the file into a "" quote like:
your_target_location = "foo/bar bla"

Illegal nesting with SASS everytime

I’m trying to create my own website and I’m using Nanoc. I’m also writing my files in HAML and SASS.
When I’m writing into my SASS file, I always have the error
Haml::SyntaxError: Illegal nesting: nesting within plain text is illegal
when I compile (nanoc compile).
My sass files are in /content/css and I would like they go in /output/css
The thing I don’t understand is that if I put spaces or if I put a tab, it doesn’t compile. The only thing which works is when I don’t put any spaces or tabs. It compiles but the CSS in output doesn’t work.
I looked here before : https://groups.google.com/forum/#!topic/nanoc/UI4VccZCDD4 but it doesn't correct my compilation error.
I let my style.sass file and my Rules file below.
What is causing this issue and how can I resolve it?
div.title
width: 80%
background-color: #b7b8b2
If I put no spaces before width and background, it compiles but doesn't work.
#!/usr/bin/env ruby
require 'compass'
Compass.add_project_configuration 'config.rb' # when using Compass 0.10
### Compile rules
compile '/content/css/*' do
filter :sass, Compass.sass_engine_options # add the second parameter for Compass
end
compile '*' do
if item.binary?
# don’t filter binary items
else
filter :haml
layout 'default'
end
end
### Route rules
route '/content/css/*' do
#'/style.css'
# item.identifier.chop + '.css' #so that the /content/stylesheet.sass item is compiled in sass
unless item.identifier.start_with?('/content/css/_') # for partials
item.identifier.gsub(/\/$/, '') + '.css'
end
end
route '*' do
if item.binary?
# Write item with identifier /foo/ to /foo.ext
item.identifier.chop + '.' + item[:extension]
else
# Write item with identifier /foo/ to /foo/index.html
item.identifier + 'index.html'
end
end
### Layout rules
layout '*', :haml
The CSS compilation rule has an error. It should say
compile '/css/*'
instead of
compile '/content/css/*'
Item identifiers do not start with /content.

Karma + Rails: File structure?

When using the karma javascript test library (née Testacular) together with Rails, where should test files and mocked data go be placed?
It seems weird to have them in /assets/ because we don’t actually want to serve them to users. (But I guess if they are simply never precompiled, then that’s not an actual problem, right?)
Via this post: https://groups.google.com/forum/#!topic/angular/Mg8YjKWbEJ8
I'm experimenting with something that looks like this:
// list of files / patterns to load in the browser
files: [
'http://localhost:3000/assets/application.js',
'spec/javascripts/*_spec.coffee',
{
pattern: 'app/assets/javascripts/*.{js,coffee}',
watched: true,
included: false,
served: false
}
],
It watches app js files, but doesn't include them or serve them, instead including the application.js served by rails and sprockets.
I've also been fiddling with https://github.com/lucaong/sprockets-chain , but haven't found a way to use requirejs to include js files from within gems (such as jquery-rails or angularjs-rails).
We ended up putting tests and mocked data under the Rails app’s spec folder and configuring Karma to import them as well as our tested code from app/assets.
Works for us. Other thoughts are welcome.
Our config/karma.conf.js file:
basePath = '../';
files = [
JASMINE,
JASMINE_ADAPTER,
//libs
'vendor/assets/javascripts/angular/angular.js',
'vendor/assets/javascripts/angular/angular-*.js',
'vendor/assets/javascripts/jquery-1.9.1.min.js',
'vendor/assets/javascripts/underscore-min.js',
'vendor/assets/javascripts/angular-strap/angular-strap.min.js',
'vendor/assets/javascripts/angular-ui/angular-ui.js',
'vendor/assets/javascripts/angular-bootstrap/ui-bootstrap-0.2.0.min.js',
//our app!
'app/assets/javascripts/<our-mini-app>/**',
// and our tests
'spec/javascripts/<our-mini-app>/lib/angular/angular-mocks.js',
'spec/javascripts/<our-mini-app>/unit/*.coffee',
// mocked data
'spec/javascripts/<our-mini-app>/mocked-data/<data-file>.js.coffee',
];
autoWatch = true;
browsers = 'PhantomJS'.split(' ')
preprocessors = {
'**/*.coffee': 'coffee'
}
I found this project helpful as a starting point. https://github.com/monterail/rails-angular-karma-example. It is explained by the authors on their blog.
It's an example rails app with angular.js and karma test runner.

Rails detect changes to files programatically

I would like to write a method that programatically detects whether any of the files in my rails app have been changed. Is it possible do do something like an MD5 of the whole app and store that in a session variable?
This is mostly for having some fun with cache manifest. I already have a dynamically generated cache and it works well in production. But in my dev environment, I would like the id of that cache to update whenever I change anything in the app directory (as opposed to every 10 seconds, which is how I have it setup right now).
Update
File.ctime(".") would be perfect, except that "." is not marked as having changed when deeper directory files have changed.
Does it make sense to iterate through all directories in "." and add together the ctimes for each?
Have you considered using Guard.
You can programatically do anything whenever a file in your project changes.
There is a nice railscast about it
There is a simple ruby gem called filewatcher. This is the most advanced example:
require 'filewatcher'
FileWatcher.new(["README.rdoc"]).watch() do |filename, event|
if(event == :changed)
puts "File updated: " + filename
end
if(event == :delete)
puts "File deleted: " + filename
end
if(event == :new)
puts "New file: " + filename
end
end
File.ctime is the key. Iterate through all files and create a unique id based on the sum of all their ctimes:
cache_id = 0
Dir.glob('./**/*') do |this_file|
ignore_files = ['.', '..', "log"]
ignore_files.each do |ig|
next if this_file == ig
end
cache_id += File.ctime(this_file).to_i if File.directory?(this_file)
end
Works like a charm, page only re-caches when it needs to, even in development.

Resources