Dynamic CSS using Sprockets - ruby-on-rails

I'm working on a site builder in rails and I would like to render the sites css using Sprockets SCSS processors. Since the user can change colors and logos, I can't use Sprockets precompilation so I've started working on a Rails SCSS template handler to handle dynamic views.
The goal is to compile 'app/views/sites/show.css.scss' any time /sites/43543.css is requested. Here's what I have so far. You'll notice I first run the template through the ERB processor and then attempt to run it through Sprockets.
https://gist.github.com/3870095
Manuel Meurer came up with an alternative solution that writes the ERB output to a path and then triggers the Asset Pipeline to compile it. I was able to get his solution to work locally but it wont work on heroku because the asset path is not writable. Files can only be written to the tmp directory and those files are only guaranteed for a single request.
http://www.krautcomputing.com/blog/2012/03/27/how-to-compile-custom-sass-stylesheets-dynamically-during-runtime/

After a long day I was able to solve my problem thanks to John Feminella and his post on google. The challenging part for me was figuring out how to create a new Sprockets::Context. Luckily John's solution doesn't require a Context.
Updated gist here
Attempt #1
This code does not allow importing css files from the asset pipeline.
#import "foundation"; works because foundation is loaded as a compass module
#import "custom_css"; results in an error message saying the file could not be found
def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)
%{
options = Compass.configuration.to_sass_engine_options.merge(
:syntax => :scss,
:custom => {:resolver => ::Sass::Rails::Resolver.new(CompassRails.context)},
)
Sass::Engine.new((begin;#{erb};end), options).render
}
end
Attempt #2
This code fails to embed base64 urls using asset-data-url
def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)
%{
compiler = Compass::Compiler.new *Compass.configuration.to_compiler_arguments
options = compiler.options.merge({
:syntax => :scss,
:custom => {:resolver => ::Sass::Rails::Resolver.new(CompassRails.context)},
})
Sass::Engine.new((begin;#{erb};end), options).render
}
end
Attempt 3
Turns out you can use empty values while creating the context. Code below works in development but throws an error in production.
ActionView::Template::Error (can't modify immutable index)
It appears the error occurs in Sprockets::Index which is used instead of Sprockets::Environment in production. Switching to Sprockets::Environment doesn't solve the problem either.
def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)
%{
context = CompassRails.context.new(::Rails.application.assets, '', Pathname.new(''))
resolver = ::Sass::Rails::Resolver.new(context)
compiler = Compass::Compiler.new *Compass.configuration.to_compiler_arguments
options = compiler.options.merge({
:syntax => :scss,
:custom => {:resolver => resolver}
})
Sass::Engine.new((begin;#{erb};end), options).render
}
end

Related

Rails PDFKit - Errno::ENOENT (No such file or directory) when using to_file

Whenever I try to generate a pdf using to_file, the process will just hang, and when I stop the development server I get Errno::ENOENT (No such file or directory - path/to/pdf). However, I am able to render a pdf inline using to_pdf. I'm also able to generate PDFs from the command line in the same folder that I'm trying to generate them in with to_file.
I'm using Rails 3.2.12 and pdfkit 0.8.2. I've tried using wkhtmltopdf versions 0.9.6 through 0.12.4. I'm on Ubuntu 14.04.
Example from controller:
html = render_to_string(:action => "show.html.erb", :formats => :html)
kit.stylesheets << "{Rails.root}/app/assets/stylesheets/stylesheet1.css"
kit.stylesheets << "#{Rails.root}/vendor/assets/stylesheets/stylesheet2.css"
kit.to_file("#{Rails.root}/folder_to_write_to/generated_pdf.pdf")
Turns out the issue was the asset pipeline conflicting with wkhtmltopdf. Added config.threadsafe! to development.rb and it started working.
Another issue can be the default options passed. For example, when I left the default print_media_type option in place, found this message in the log:
The switch --print-media-type, is not support using unpatched qt, and will be ignored."
Only when I override that does it work for me, either in the initializer or like so:
PDFKit.new(html, {print_media_type: false})
The message says it'll be ignored, but it wasn't. It was causing the file to not get generated.

Manually compiling Sass in Rails 4.2

I'm manually compiling Sass in a Rails 4.2 app, and I'm running into issue with Sass' #import. It's giving me the following error - "File to import not found or unreadable..." Here's a code snippet.
scheme_css_dir = "#{Rails.root}/app/assets/schemes/#{scheme}/css"
template = File.read("#{scheme_css_dir}/styles.css.scss")
engine = Sass::Engine.new(template, {
:syntax => :scss,
:cache => false,
:read_cache => false,
:style => :compressed,
:load_paths => [scheme_css_dir]
})
output = engine.render
styles.css.scss contains several #import statements, and it's failing on the first one. Here's what the first one looks like.
#import "./flexslider";
There is a flexslider.css.scss file in the same directory as style.css.scss. I'm supplying that directory in the :load_paths option. Interestingly enough, if I change the #import line to...
#import "./flexslider.css.scss";
...then it gets to the next #import line in the file - where it throws the same error. This style.css.scss manifest complies successfully during deploy as is (without adding extensions to all the #imports).
What am I missing? Is there additional config that I need in order to use Sass::Engine to compile css manifest just like Rails would during deploy?
FYI - I'm using the latest version of sass-rails (5.0.3).
Any help is much appreciated. Thanks!
UPDATE: Thanks to papirtiger's comment, I was able to resolve the issue with #import by updated the extensions of my files from .css.scss to .scss. However, I'm still running into errors with asset helpers - specifically image-url and font-url. Here's the error I'm seeing...
undefined method `[]' for nil:NilClass
The error occurs in a sprockets_context method in sprockets (3.1.0) lib/sprockets/sass_processor.rb
def sprockets_context
options[:sprockets][:context]
end
Any thoughts on how to get pass this hurdle?

Sprockets/Rails: Find all files Sprockets knows how to compile

For Konacha, a Rails engine for testing Rails apps, we need a way to find all files that Sprockets can compile to JavaScript.
Right now we use something like
Dir['spec/javascripts/**/*_spec.*']
but this picks up .bak, .orig, and other backup files.
Can Sprockets tell us somehow whether it knows how to compile a file, so that backup files would be automatically excluded?
content_type_of doesn't help:
Rails.application.assets.content_type_of('test/javascripts/foo.js.bak')
=> "application/javascript"
You can iterate through all the files in a Sprockets::Environment's load path using the each_file method:
Rails.application.assets.each_file { |pathname| ... }
The block will be invoked with a Pathname instance for the fully expanded path of each file in the load path.
each_file returns an Enumerator, so you can skip the block and get an array with to_a, or call include? on it. For example, to check whether a file is in the load path:
assets = Rails.application.assets
pathname1 = Pathname.new("test/javascripts/foo.js").expand_path
pathname2 = Pathname.new("test/javascripts/foo.js.bak").expand_path
assets.each_file.include?(pathname1) # => true
assets.each_file.include?(pathname2) # => false

Conditional javascript require in the asset pipeline

I'm struggling with the asset pipeline. I'm loading dojo from Google CDN putting this in my template:
= javascript_include_tag 'http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js', :'data-dojo-config' => %Q(dojoBlankHtmlUrl:'/blank.html', baseUrl: 'assets/', modulePaths: {custom: 'javascripts/modules'})
I just want a fallback to a local version if running locally or if the CDN is down. I thought of doing this:
script typeof(dojo) === "undefined" && document.write(unescape('%3Cscript src="js/libs/dojo-1.6.1.min.js"%3E%3C/script%3E'));
But I don't like it as it works out of the asset pipeline. I want to keep dojo in vendors/assets/javascripts/dojo. How can I get the fallback to be served by the asset pipeline.
Is there a way do declare conditional require in the asset pipeline. What I want is to run some javascript tests, and depending on the result serve a file.
Thanks
I suggest you use yepnope, a lightweight library for loading libraries like this in parallel (for speed) and it gives you the option to run some other code to test if the library is loaded. For example:
yepnope([{
load: 'http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js',
complete: function () {
if (!window.jQuery) {
yepnope('asset_path('you_local_copy_of_dojo') ');
}
}
}])
(Note: You will need erb tags around the asset_path helper)
The local dojo file would be in the assets/javascript folder, but not included in the application manifest. You need to add the dojo file to the precompile array:
config.assets.precompile += 'your_local_file.js'
And this will make it available to the asset_path helper.
Thanks Richard!
I don't want to have yepnope to load one library. It would be overkill imo. Here is the solution I came up with, based on your help (written in slim):
1/ In vendors/assets/javascripts/, I have my dojo.js.
2/ In config/application.rb:
# Precompile these assets files
config.assets.precompile += ['dojo.js']
3/ In the template:
= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/dojo/#{Settings.dojoVersion}/dojo/dojo.xd.js", :'data-dojo-config' => %Q(dojoBlankHtmlUrl:'/blank.html', baseUrl: 'assets/', modulePaths: {custom: 'javascripts/modules'})
script = "typeof(dojo) === \"undefined\" && document.write(unescape('%3Cscript src=\"#{asset_path('dojo')}\"%3E%3C/script%3E'));".html_safe
I also posted on the Rails Google Group to request the addition of two options to the javascript_include_tag, :test and :local that would take care of all the work. We'll see.

Rails Asset Caching Breaks First few page loads

We're using Rails asset caching for JS and CSS like this:
<%= stylesheet_link_tag 'reset','global','admins','autocomplete', 'date_input', 'tablesorter', 'partners', 'jqmodal', :media => 'screen', :cache => set_asset_cache(:admins) %>
<%= javascript_include_tag :defaults, 'autocomplete', 'searchbox', 'jqmodal', :cache => set_asset_cache(:admins) %>
In our deploy we call rake tmp:assets:clear each time. The problem is that the first few page loads after a deploy come up with no css or js on the page. I guess until the cached all.js and all.css have been regenerated.
We deploy many times per day and this is scary for any users who happen to come across a busted page.
Have people found any way to make this smoother so the new cached assets are guaranteed to be there on the first new page load?
The AssetHat gem addresses this exact problem. Instead of concatenating assets the first time a page is loaded (which increases that page's load time), it concatenates assets on deploy instead. As a bonus, the gem also minifies your CSS and JS, which saves precious bytes.
After setup, usage is pretty simple:
Use include_css :bundle => 'admins' and include_js :bundle => 'admins' in your layout. (The bundle contents are set in a config file to keep your layout lightweight.)
Add rake asset_hat:minify to your deploy script. My company has been using it in production with Capistrano for about a year now.
There's more info in the readme and docs, and I'd be happy to hear any questions/ideas!
You could try warming the cache during deployment using wget, as an example (shamelessly reposted):
wget -r -nd --delete-after http://whatever.com/~popular/page/
However, this would have to be executed after you switch your symlink to your new deployment. A possibly more elegant solution might be to call the asset caching methods manually in your deploy, though I'm not sure how feasible that is. Here's where the caching is performed in Rails:
# File vendor/rails/actionpack/lib/action_view/helpers/asset_tag_helper.rb, line 273
273: def javascript_include_tag(*sources)
274: options = sources.extract_options!.stringify_keys
275: concat = options.delete("concat")
276: cache = concat || options.delete("cache")
277: recursive = options.delete("recursive")
278:
279: if concat || (ActionController::Base.perform_caching && cache)
280: joined_javascript_name = (cache == true ? "all" : cache) + ".js"
281: joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : JAVASCRIPTS_DIR, joined_javascript_name)
282:
283: unless ActionController::Base.perform_caching && File.exists?(joined_javascript_path)
284: write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive))
285: end
286: javascript_src_tag(joined_javascript_name, options)
287: else
288: expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n")
289: end
290: end
You might be able to modify the caching code and run it manually on deploy.

Resources