I created a small CSS preprocessor, which is somewhat similar to SASS, and now I would like to test it in a 'real life' scenario, so I'm trying to create a rails plugin for it, like the way sass-rails works.
I tried this, without being really sure of what I was doing:
require 'toss-ruby'
require 'sprockets'
module Toss
module Rails
class Template < ::Tilt::Template
def prepare
end
def evaluate(scope, locals, &block)
g = ::Toss::Generator.new
g.parse_string data
g.generate_string
end
end
end
end
Sprockets.register_engine '.toss', ::Toss::Rails::Template
According to the documentation, the last line should register my template so sprockets can use it, but it doesn't happen, so I assume it's never called. How do some gems like thin, sass-rails, etc... manages to work only by being put in a Gemfile ? When and how is their code called ?
There is a convention that executes code in gems that are included in your Gemfile.
If a file exists in the lib directory of a gem's codebase with the same name as the gem and with a .rb extension, it is required by default when Bundler loads the gem.
Take a gem called "Mygem" as a example. This is a typical directory structure for a gem:
mygem/ (gem's root directory)
|-- lib/
| |-- mygem.rb <-- automatically required!
| `-- mygem/
| |-- base.rb
| `-- version.rb
|-- test/
|-- bin/
|-- Rakefile
`-- mygem.gemspec
When Bundler loads the 'mygem' gem, it automatically requires lib/mygem.rb. Rails plugins and engines rely on this behavior to load their code. Typically, they use this file to require other files in the lib directory. In this case, the lib/mygem.rb file may require lib/mygem/base.rb file, etc.
While developing a gem, it's common to add it to your Gemfile by using the Bundler :path directive, which tells Bundler to look for the gem at a certain path on your local filesystem, rather than on rubygems. Let's assume you are building your gem under the vendor/engines directory of your test application.
Your test app's Gemfile:
gem 'rails'
gem 'mygem', path: './vendor/engines/mygem'
When the Rails app is started, one of the first steps is to load it's gems. When the 'mygem' gem is loaded, Bundler requires './vendor/engines/mygem/lib/mygem.rb' which executes the code in it. Any code you put in there will be run even before Rails is initialized.
If you have code that needs to be run as part of application initialization, you need to follow the guide for creating Rails plugins/engines. Typically, you'll inherit your plugin from ::Rails::Engine and call initializer in it and pass a block containing your initialization code.
With all this in mind, have a look at source code for the sass-rails gem again, specifically lib/sass-rails.rb which gets required when the gem is loaded and how it recursively requires lib/sass/rails/railtie.rb, which does the necessary setup and initialization for the plugin to integrate with Rails.
Related
I'm trying to extract several classes from a Rails app into its own Gem so that I can reuse the code from a Sinatra app.
In the Rails app I have the following structure:
app > classes > api > (bunch of folders and subfiles)
I'm trying to move the api folder into a gem, for that I created a new gem using bundler:
bundle gem myapp-core --no-exe --no-coc --no-mit --no-ext
so I ended up with a file structure like:
myapp-core > lib > myapp > core > version.rb
myapp-core > lib > myapp > core.rb
I've copied the api folder to myapp-core > lib > myapp > api and tried to require it from sinatra doing:
require 'myapp/api/somefile.rb'
but that didn't work, I have of course added the gem to the Gemfile of the sinatra app.
I tried all kinds of combinations of where to put the folder and how to require the files in it but I either get cannot load such file or uninitialized constant Api (NameError).
What is the correct way to go about this so that ideally from both Sinatra and Rails I would just add the gem to the Gemfile, require whatever file I need and the code that uses those API files would remain unchanged or change as little as possible?
Without the Rails auto-loader you'll need to establish all the intermediate class and module components yourself.
This usually means you need to either declare things like:
module Api
class Somefile
# ...
end
end
Where that Api module is automatically declared, allowing you to require lib/api/somefile directly, or you need to shift this responsibility up the chain:
# lib/api.rb
module Api
# ...
end
require_relative './api/somefile'
Where that automatically loads in any dependencies and you now do require 'api' instead, presuming your $LOAD_PATH includes this particular lib/ path.
I am in the middle of developing a gem for rails and i stuck with this issue. My logic is I have a gem created, and the gem installed to my local machine. What I need is when I type gem_name --install, there is a file called test.rb should be copied to inside a rails project/ config/initializers/. The file to be copied is currently placed in a folder in my gem. I have tried
Dir.pwd
but it is not give me results as expected. Please find a solution for me and TIA..
For a rails gem you would use a generator together with a "template" file.
class FooGenerator < Rails::Generators::NamedBase
source_root File.expand_path("../templates", __FILE__)
def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
end
end
This will would copy the file when the user of the gem runs rails generate foo.
Make sure to read through the rails guides sections on generators and creating engines as there are quite a few gotchas and conventions.
I am developing a Rails app where most of the code not specific to the app has been written inside of various gems, including some Rails engines and some 3rd party gems for which I am enhancing or fixing bugs.
gem 'mygem', path: File.expath_path('../../mygem', __FILE__)
Since a lot of the code in these gems is really part of the app, it's still changing frequently. I'd like to be able to utilize the Rails feature where code is reloaded on each request when in development (i.e. when config.cache_classes is false), but this is only done within the normal application structure by default.
How can I configure Rails to reload gem code on each request, just like with the app code?
I have found through trial and error that several steps are required, with the help of ActiveSupport.
Add activesupport as a dependency in the .gemspec files
spec.add_dependency 'activesupport'
Include ActiveSupport::Dependencies in the top-level module of your gem (this was the most elusive requirement)
require 'bundler'; Bundler.setup
require 'active_support/dependencies'
module MyGem
unloadable
include ActiveSupport::Dependencies
end
require 'my_gem/version.rb'
# etc...
Set up your gem to use autoloading. You an either manually use ruby autoload declarations to map symbols into filenames, or use the Rails-style folder-structure-to-module-hierarchy rules (see ActiveSupport #constantize)
In each module and class in your gem, add unloadable.
module MyModule
unloadable
end
In each file that depends on a module or class from the gem, including in the gem itself, declare them at the top of each file using require_dependency. Look up the path of the gem as necessary to properly resolve the paths.
require_dependency "#{Gem.loaded_specs['my_gem'].full_gem_path}/lib/my_gem/myclass"
If you get exceptions after modifying a file and making a request, check that you haven't missed a dependency.
For some interesting details see this comprehensive post on Rails (and ruby) autoloading.
The solution that I used for Rails 6, with a dedicated Zeitwerk class loader and file checker :
Add the gem to the Rails project using the path: option in Gemfile
gem 'mygem', path: 'TODO' # The root directory of the local gem
In the development.rb, setup the classloader and the file watcher
gem_path = 'TODO' # The root directory of the local gem, the same used in Gemfile
# Create a Zeitwerk class loader for each gem
gem_lib_path = gem_path.join('lib').join(gem_path.basename)
gem_loader = Zeitwerk::Registry.loader_for_gem(gem_lib_path)
gem_loader.enable_reloading
gem_loader.setup
# Create a file watcher that will reload the gem classes when a file changes
file_watcher = ActiveSupport::FileUpdateChecker.new(gem_path.glob('**/*')) do
gem_loader.reload
end
# Plug it to Rails to be executed on each request
Rails.application.reloaders << Class.new do
def initialize(file_watcher)
#file_watcher = file_watcher
end
def updated?
#file_watcher.execute_if_updated
end
end.new(file_watcher)
With this, on each request, the class loader will reload the gem classes if one of them has been modified.
For a detailed walkthrough, see my article Embed a gem in a Rails project and enable autoreload.
I am creating a gem that will contain the foundation-rails gem along w/ some common variables that are used across my applications. I have created a stylesheet at vendor/assets/stylesheets/foundation.scss. I load this from within my application as such
Gemfile
gem 'foobar-foundation-rails', path: '...'
app/assets/stylesheets/application.css
//= require foundation
This is a good starting point but how do I include the foundation-rails gem's stylesheet from within this file? I am unsure how to reference another gem's assets
I think the best approach is to put the responsibility for the require statements in your rails app's javascripts file. This is most likely not functionality you want to bury in a gem, as it hides what is happening.
Then make sure you require your gem's css file before the foundation-rails require. However you should put a dependency requirement in your gem's gemspec file to ensure that the foundation-rails gem will be installed by bundler when your gem is installed.
Also you may have to "namespace" your gems style sheet in order to avoid namespace collisions.
vendor/assets/stylesheets/foobar_foundation_rails/foundation.css
Which would change the require in your stylesheet file to
require 'foobar_foundation_rails/foundation.scss'
Lastly, the naming of a gem establishes how the gem gets required. When you use dashes Rails expects things to be required, and hence your gem's directory structure to follow
lib/foobar/foundation/rails
As opposed to an underscore naming foobar_foundation_rails
lib/foobar_foundation_rails
Unless you're going to build an "extension" to the foundation-rails gem, which would need to be called foundation-rails-foobar, you may want to go with the underscore syntax to save yourself some require headaches. The devise gem is a good example of extension gems.
I'm not sure if this sort of thing is very common, but I keep finding myself trying to create gems that are just wrappers around a Rails application.
My gem will have a generator to create a config.ru but the Rails application will live inside the gem's lib directory. I need to know how to "embed" a Rails application and configure it so that it can be run inside the gem.
For example:
$ mygem new project
mygem created a directory called "project" with the following files:
project/config.ru
project/widgets/
project/foobars/
My gem will also generate some directories that will need to be added to Rails somehow so that I can access the code in those directories from the Rails app living inside the Gem.
Any help or advice you can give me would be appreciated.
To clarify, I'm not trying to create a Rails engine, or plugin to a Rails application. I'm trying to create a fully-fledged Rails application, but package it as a gem so that a user of my gem can run the gem (the rails app) without needing to know that it's using Rails behind the scenes.
Update: Okay, I've got a little bit working now. I've created the gem and generated the rails project inside the gem's lib directory.
$ bundle gem my_gem && cd my_gem/lib
$ rails new my_gem --skip-bundle
Which leaves me with:
my_gem/
my_gem.gemspec
bin/my_gem
lib/
my_gem.rb
my_gem/
version.rb # generated by bundler
# the rails app:
app/
config/
Gemfile
...etc
Since this gem requires Rails, I started adding the gems defined in the Rails Gemfile as dependencies in the gem's Gemspec, but I'm a little confused as to how to handle the assets group in the Gemfile.
# Rails Gemfile
group :assets do
gem 'sass-rails', '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'
gem 'therubyracer', :platforms => :ruby
gem 'uglifier', '>= 1.0.3'
end
# gemspec
Gem::Specification.new do |gem|
gem.name = "my_gem"
# ...
gem.add_dependency 'rails', '3.2.8'
gem.add_dependency 'sqlite3'
gem.add_dependency 'jquery-rails'
# how to add the assets group gems?
end
Try this and see if it helps you make progress.
Gems are just directories of files, and you can put whatever files you want into a gem.
Create:
Create a blank gem full-blown Rails project:
$ bundle gem my_gem
Then a Rails app:
$ rails new my_app --skip-bundle
Copy the Rails files into the gem:
$ cp -R my_app/* my_gem
Bundle everything into your Rails app:
$ cd my_gem
$ bundle install --binstubs --path vendor/bundle
$ cd -
Make the Rakefile have the gem tasks and the Rails setup:
#!/usr/bin/env rake
require "bundler/gem_tasks"
require File.expand_path('../config/application', __FILE__)
MyApp::Application.load_tasks
Verify that it starts:
$ rails server
Load Path:
To control where Rails looks for files, such as "external" configuration files, you can use the file config/application.rb with any directory paths like this:
# Add additional load paths for your own custom dirs
# config.load_paths += %W( #{config.root}/../customdir )
Note the ".." which means go above the Rails directory. This gives you a path relative to the gem.
If you prefer you can specify an absolute path, for example if you know the user will always keep his external files in "~/myfiles/". You can also choose to use ENV vars to send in whatever directory you want.
If you read about load path capabilties, look for lines that are shorthand for adding a directory to the front of the load path because you may want to put your external diretories first:
$:.unshift File.dirname(__FILE__)
Gem Build:
Edit my_gem.gemspec to add your own description, homepage, summary, etc. then build:
$ gem build my_gem.gemspec
Successfully built RubyGem
Name: my_gem
Version: 0.0.1
File: my_gem-0.0.1.gem
Now your Rails app is packaged as a gem.
The config.ru should be a typical Rails one. No special changes AFAIK.
When your user wants to install your app:
$ gem install my_gem
The gem will install in the user's typical gem directory. If you want to adjust this, see this page on rubygems: http://docs.rubygems.org/read/chapter/3
Crate:
You may also want to investigate the Crate project:
Crate: Packaging Standalone Ruby Applications
http://www.slideshare.net/copiousfreetime/crate-packaging-standalone-ruby-applications
Rack:
To use config.ru here is the typical Rails setup:
# Rails.root/config.ru
require "config/environment"
use Rails::Rack::LogTailer
use ActionDispatch::Static
run ActionController::Dispatcher.new
For your project, you want to require some files before Rails. You'll want to learn about the Ruby "require" and how it finds files using LOAD_PATH.
The easy way:
# Rails.root/config.ru
require_relative 'filename'
require "config/environment"
Or to put the user's custom directory up couple directory levels:
require './../../filename' # not the best for security
Or to use an absolute path, read about File.expand_path:
File.expand_path(__FILE__)
Or to use the current directory and put it on the load path:
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'filename'
Lots of choices for you to consider. Hope this helps!
What about the question, "How am I going to run the Rails application inside the gem?".
A Rails application has controllers and views to run a web server. What you need are actions to create, list, update, and destroy. Exposing these actions without a web server is essentially having such methods in a class. That's a normal standard type of gem in the first place.
So maybe your questions is really, how do I write a gem where I have ActiveRecord, and the other Rails stuff.
First, you need to make your gem dependent on the Rails gems you need. You do this in the gemspec file for your gem.
Then it really is just a matter of your gem code doing a require of the right Rails gems you need.
I'm not sure if this will help, as I read through everything and I couldn't find the motivation behind why you were doing this. One of the reasons I came up with was making something that can be used on a desktop environment. In that case you could try using something like Bowline. If you just want to provide an application that others can download and use and install themselves, then you can probably assume they can follow at least basic developer kind of instructions and you could just provide the whole app on github or as a zip file. See an example of someone else doing something similar over on Fat Free CRM's github page.