Skip to content

Commit 201d96f

Browse files
Merge pull request #18 from glebm/penthouse-npm
Use penthouse from npm
2 parents 47d7896 + 90a8e36 commit 201d96f

22 files changed

+1096
-657
lines changed

.codeclimate.yml

-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
languages:
22
Ruby: true
3-
exclude_paths:
4-
- lib/penthouse/penthouse.js

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ test/version_tmp
1717
tmp
1818

1919
.DS_Store
20+
/node_modules/
21+
.rspec_status
22+
/npm-debug.log

Gemfile

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
# A sample Gemfile
2-
source "https://rubygems.org"
1+
source 'https://rubygems.org'
32

4-
gem 'rubocop', require: false
3+
gemspec
4+
5+
group :development, :test do
6+
gem 'byebug', platform: [:ruby], require: false
7+
gem 'rubocop', require: false
8+
end
9+
10+
# HACK: npm install on bundle
11+
unless $npm_commands_hook_installed # rubocop:disable Style/GlobalVars
12+
Gem.pre_install do |installer|
13+
next true unless installer.spec.name == 'critical-path-css-rails'
14+
require_relative './ext/npm/install'
15+
end
16+
$npm_commands_hook_installed = true # rubocop:disable Style/GlobalVars
17+
end

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This gem give you the ability to load only the CSS you *need* on an initial page
66

77
This gem assumes that you'll load the rest of the CSS asyncronously. At the moment, the suggested way is to use the [loadcss-rails](https://github.com/michael-misshore/loadcss-rails) gem.
88

9-
This gem uses [PhantomJS](https://github.com/colszowka/phantomjs-gem) and [Penthouse](https://github.com/pocketjoso/penthouse) to generate the critical CSS.
9+
This gem uses [Penthouse](https://github.com/pocketjoso/penthouse) to generate the critical CSS.
1010

1111
## Update
1212

@@ -17,7 +17,7 @@ Versions below 0.3.0 are not compatible with this version. Please read the Upgr
1717
Add `critical-path-css-rails` to your Gemfile:
1818

1919
```
20-
gem 'critical-path-css-rails', '~> 0.4.0'
20+
gem 'critical-path-css-rails', '~> 1.0.0'
2121
```
2222

2323
Download and install by running:
@@ -128,9 +128,9 @@ Answer 'Y' when prompted to overwrite `critical_path_css.rake`. However, overwr
128128
The critical-path-css-rails gem follows these version guidelines:
129129

130130
```
131-
patch version bump = updates to critical-path-css-rails and patch-level updates to Penthouse and PhantomJS
132-
minor version bump = minor-level updates to critical-path-css-rails, Penthouse, and PhantomJS
133-
major version bump = major-level updates to critical-path-css-rails, Penthouse, PhantomJS, and updates to Rails which may be backwards-incompatible
131+
patch version bump = updates to critical-path-css-rails and patch-level updates to Penthouse
132+
minor version bump = minor-level updates to critical-path-css-rails and Penthouse
133+
major version bump = major-level updates to critical-path-css-rails, Penthouse, and updates to Rails which may be backwards-incompatible
134134
```
135135

136136
## Contributing

critical-path-css-rails.gemspec

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ Gem::Specification.new do |s|
1010
s.description = 'Only load the CSS you need for the initial viewport in Rails!'
1111
s.license = 'MIT'
1212

13-
s.add_runtime_dependency 'phantomjs', ['~> 2.1']
14-
1513
s.files = `git ls-files`.split("\n")
1614
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
1715
s.require_path = 'lib'
16+
17+
s.add_development_dependency 'rspec', '~> 3.6'
18+
19+
s.extensions = ['ext/npm/extconf.rb']
1820
end

ext/npm/extconf.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
File.write 'Makefile',
2+
"make:\n\t\ninstall:\n\truby install.rb\nclean:\n\t\n"
3+

ext/npm/install.rb

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
require_relative '../../lib/npm_commands'
2+
3+
NpmCommands.new.install('--production', '.') ||
4+
raise('Error while installing npm dependencies')

lib/critical-path-css-rails.rb

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,38 @@
1-
module CriticalPathCss
2-
require 'critical_path_css/css_fetcher'
1+
require 'critical_path_css/configuration'
2+
require 'critical_path_css/css_fetcher'
3+
require 'critical_path_css/rails/config_loader'
34

5+
module CriticalPathCss
46
CACHE_NAMESPACE = 'critical-path-css'
57

68
def self.generate(route)
7-
Rails.cache.write(
9+
::Rails.cache.write(
810
route,
9-
CssFetcher.new.fetch_route(route),
11+
CssFetcher.new(config).fetch_route(route),
1012
namespace: CACHE_NAMESPACE,
1113
expires_in: nil
1214
)
1315
end
1416

1517
def self.generate_all
16-
CssFetcher.new.fetch.each do |route, css|
17-
Rails.cache.write(route, css, namespace: CACHE_NAMESPACE, expires_in: nil)
18+
CssFetcher.new(config).fetch.each do |route, css|
19+
::Rails.cache.write(route, css, namespace: CACHE_NAMESPACE, expires_in: nil)
1820
end
1921
end
2022

2123
def self.clear(route)
22-
Rails.cache.delete(route, namespace: CACHE_NAMESPACE)
24+
::Rails.cache.delete(route, namespace: CACHE_NAMESPACE)
2325
end
2426

2527
def self.clear_matched(routes)
26-
Rails.cache.delete_matched(routes, namespace: CACHE_NAMESPACE)
28+
::Rails.cache.delete_matched(routes, namespace: CACHE_NAMESPACE)
2729
end
2830

2931
def self.fetch(route)
30-
Rails.cache.read(route, namespace: CACHE_NAMESPACE) || ''
32+
::Rails.cache.read(route, namespace: CACHE_NAMESPACE) || ''
33+
end
34+
35+
def self.config
36+
@config ||= Configuration.new(CriticalPathCss::Rails::ConfigLoader.new.load)
3137
end
3238
end
+8-18
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,29 @@
11
require 'erb'
22
module CriticalPathCss
33
class Configuration
4-
CONFIGURATION_FILENAME = 'critical_path_css.yml'
54

6-
def initialize
7-
@configurations = YAML.load(ERB.new(File.read(configuration_file_path)).result)[Rails.env]
5+
def initialize(config)
6+
@config = config
87
end
98

109
def base_url
11-
@configurations['base_url']
10+
@config['base_url']
1211
end
1312

1413
def css_path
15-
@css_path ||= begin
16-
relative_path = @configurations['css_path'] || manifest_path
17-
"#{Rails.root}/public#{relative_path}"
18-
end
14+
@config['css_path']
1915
end
2016

2117
def manifest_name
22-
@configurations['manifest_name']
18+
@config['manifest_name']
2319
end
2420

2521
def routes
26-
@configurations['routes']
22+
@config['routes']
2723
end
2824

29-
private
30-
31-
def configuration_file_path
32-
@configuration_file_path ||= Rails.root.join('config', CONFIGURATION_FILENAME)
33-
end
34-
35-
def manifest_path
36-
@manifest_path ||= ActionController::Base.helpers.stylesheet_path(manifest_name, host: '')
25+
def penthouse_options
26+
@config['penthouse_options'] || {}
3727
end
3828
end
3929
end

lib/critical_path_css/css_fetcher.rb

+50-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
require 'json'
2+
require 'open3'
3+
14
module CriticalPathCss
25
class CssFetcher
3-
require 'phantomjs'
4-
require 'critical_path_css/configuration'
5-
6-
PENTHOUSE_PATH = "#{File.dirname(__FILE__)}/../penthouse/penthouse.js"
6+
GEM_ROOT = File.expand_path(File.join('..', '..'), File.dirname(__FILE__))
77

8-
def initialize
9-
@config = Configuration.new
8+
def initialize(config)
9+
@config = config
1010
end
1111

1212
def fetch
@@ -20,15 +20,50 @@ def fetch_route(route)
2020
protected
2121

2222
def css_for_route(route)
23-
url = @config.base_url + route
24-
25-
Phantomjs.run(
26-
'--ignore-ssl-errors=true',
27-
'--ssl-protocol=tlsv1',
28-
PENTHOUSE_PATH,
29-
url,
30-
@config.css_path
31-
)
23+
options = {
24+
'url' => @config.base_url + route,
25+
'css' => @config.css_path,
26+
## optional params
27+
# viewport dimensions
28+
'width' => 1300,
29+
'height' => 900,
30+
# CSS selectors to always include, e.g.:
31+
'forceInclude' => [
32+
# '.keepMeEvenIfNotSeenInDom',
33+
# '^\.regexWorksToo'
34+
],
35+
# ms; abort critical CSS generation after this timeout
36+
'timeout' => 30000,
37+
# set to true to throw on CSS errors (will run faster if no errors)
38+
'strict' => false,
39+
# characters; strip out inline base64 encoded resources larger than this
40+
'maxEmbeddedBase64Length' => 1000,
41+
# specify which user agent string when loading the page
42+
'userAgent' => 'Penthouse Critical Path CSS Generator',
43+
# ms; render wait timeout before CSS processing starts (default: 100)
44+
'renderWaitTime' => 100,
45+
# set to false to load (external) JS (default: true)
46+
'blockJSRequests' => true,
47+
# see `phantomjs --help` for the list of all available options
48+
'phantomJsOptions' => {
49+
'ignore-ssl-errors' => true,
50+
'ssl-protocol' => 'tlsv1'
51+
},
52+
'customPageHeaders' => {
53+
# use if getting compression errors like 'Data corrupted':
54+
'Accept-Encoding' => 'identity'
55+
}
56+
}.merge(@config.penthouse_options)
57+
out, err, st = Dir.chdir(GEM_ROOT) do
58+
Open3.capture3('node', 'lib/fetch-css.js', JSON.dump(options))
59+
end
60+
if !st.exitstatus.zero? || out.empty? && !err.empty?
61+
STDOUT.puts out
62+
STDERR.puts err
63+
raise "Failed to get CSS for route #{route}\n" \
64+
" with options=#{options.inspect}"
65+
end
66+
out
3267
end
3368
end
3469
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module CriticalPathCss
2+
module Rails
3+
class ConfigLoader
4+
CONFIGURATION_FILENAME = 'critical_path_css.yml'
5+
6+
def load
7+
config = YAML.load(ERB.new(File.read(configuration_file_path)).result)[::Rails.env]
8+
config['css_path'] = "#{::Rails.root}/public" + (
9+
config['css_path'] ||
10+
ActionController::Base.helpers.stylesheet_path(
11+
config['manifest_name'], host: ''
12+
)
13+
)
14+
config
15+
end
16+
17+
private
18+
19+
def configuration_file_path
20+
@configuration_file_path ||= ::Rails.root.join('config', CONFIGURATION_FILENAME)
21+
end
22+
end
23+
end
24+
end
+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module CriticalPathCSS
22
module Rails
3-
VERSION = '0.4.0'
4-
PENTHOUSE_VERSION = '0.3.4'
3+
VERSION = '1.0.0'
54
end
65
end

lib/fetch-css.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const penthouse = require('penthouse');
2+
const fs = require('fs');
3+
4+
const penthouseOptions = JSON.parse(process.argv[2]);
5+
6+
const STDOUT_FD = 1;
7+
const STDERR_FD = 2;
8+
9+
penthouse(penthouseOptions).then(function(criticalCss) {
10+
fs.writeSync(STDOUT_FD, criticalCss);
11+
}).catch(function(err) {
12+
fs.writeSync(STDERR_FD, err);
13+
process.exit(1);
14+
});

lib/npm_commands.rb

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
# NPM wrapper with helpful error messages
4+
class NpmCommands
5+
6+
# @return [Boolean] whether the installation succeeded
7+
def install(*args) # rubocop:disable Metrics/MethodLength
8+
return false unless check_nodejs_installed
9+
STDERR.puts 'Installing npm dependencies...'
10+
install_status = Dir.chdir File.expand_path('..', File.dirname(__FILE__)) do
11+
system('npm', 'install', *args)
12+
end
13+
STDERR.puts(
14+
*if install_status
15+
['npm dependencies installed']
16+
else
17+
['-' * 60,
18+
'Error: npm dependencies installation failed',
19+
'-' * 60]
20+
end
21+
)
22+
install_status
23+
end
24+
25+
private
26+
27+
def check_nodejs_installed # rubocop:disable Metrics/MethodLength
28+
return true if executable?('node')
29+
STDERR.puts(
30+
'-' * 60,
31+
'Error: critical-path-css-rails requires NodeJS and NPM.',
32+
*if executable?('brew')
33+
[' To install NodeJS and NPM, run:',
34+
' brew install node']
35+
elsif Gem.win_platform?
36+
[' To install NodeJS and NPM, we recommend:',
37+
' https://github.com/coreybutler/nvm-windows/releases']
38+
else
39+
[' To install NodeJS and NPM, we recommend:',
40+
' https://github.com/creationix/nvm']
41+
end,
42+
'-' * 60
43+
)
44+
end
45+
46+
def executable?(cmd)
47+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
48+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
49+
exts.each do |ext|
50+
exe = File.join(path, "#{cmd}#{ext}")
51+
return exe if File.executable?(exe) && !File.directory?(exe)
52+
end
53+
end
54+
nil
55+
end
56+
end

0 commit comments

Comments
 (0)