diff --git a/.codeclimate.yml b/.codeclimate.yml
index df67882..7561b07 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,4 +1,2 @@
languages:
Ruby: true
-exclude_paths:
- - lib/penthouse/penthouse.js
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b36aa3d..dde38a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@ test/version_tmp
tmp
.DS_Store
+/node_modules/
+.rspec_status
+/npm-debug.log
diff --git a/Gemfile b/Gemfile
index 07d2b13..1cfb04f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,4 +1,17 @@
-# A sample Gemfile
-source "https://rubygems.org"
+source 'https://rubygems.org'
-gem 'rubocop', require: false
\ No newline at end of file
+gemspec
+
+group :development, :test do
+ gem 'byebug', platform: [:ruby], require: false
+ gem 'rubocop', require: false
+end
+
+# HACK: npm install on bundle
+unless $npm_commands_hook_installed # rubocop:disable Style/GlobalVars
+ Gem.pre_install do |installer|
+ next true unless installer.spec.name == 'critical-path-css-rails'
+ require_relative './ext/npm/install'
+ end
+ $npm_commands_hook_installed = true # rubocop:disable Style/GlobalVars
+end
diff --git a/README.md b/README.md
index 183485d..3ddbbdf 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ This gem give you the ability to load only the CSS you *need* on an initial page
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.
-This gem uses [PhantomJS](https://github.com/colszowka/phantomjs-gem) and [Penthouse](https://github.com/pocketjoso/penthouse) to generate the critical CSS.
+This gem uses [Penthouse](https://github.com/pocketjoso/penthouse) to generate the critical CSS.
## Update
@@ -17,7 +17,7 @@ Versions below 0.3.0 are not compatible with this version. Please read the Upgr
Add `critical-path-css-rails` to your Gemfile:
```
-gem 'critical-path-css-rails', '~> 0.4.0'
+gem 'critical-path-css-rails', '~> 1.0.0'
```
Download and install by running:
@@ -128,9 +128,9 @@ Answer 'Y' when prompted to overwrite `critical_path_css.rake`. However, overwr
The critical-path-css-rails gem follows these version guidelines:
```
-patch version bump = updates to critical-path-css-rails and patch-level updates to Penthouse and PhantomJS
-minor version bump = minor-level updates to critical-path-css-rails, Penthouse, and PhantomJS
-major version bump = major-level updates to critical-path-css-rails, Penthouse, PhantomJS, and updates to Rails which may be backwards-incompatible
+patch version bump = updates to critical-path-css-rails and patch-level updates to Penthouse
+minor version bump = minor-level updates to critical-path-css-rails and Penthouse
+major version bump = major-level updates to critical-path-css-rails, Penthouse, and updates to Rails which may be backwards-incompatible
```
## Contributing
diff --git a/critical-path-css-rails.gemspec b/critical-path-css-rails.gemspec
index 404e650..ce7d39b 100644
--- a/critical-path-css-rails.gemspec
+++ b/critical-path-css-rails.gemspec
@@ -10,9 +10,11 @@ Gem::Specification.new do |s|
s.description = 'Only load the CSS you need for the initial viewport in Rails!'
s.license = 'MIT'
- s.add_runtime_dependency 'phantomjs', ['~> 2.1']
-
s.files = `git ls-files`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
s.require_path = 'lib'
+
+ s.add_development_dependency 'rspec', '~> 3.6'
+
+ s.extensions = ['ext/npm/extconf.rb']
end
diff --git a/ext/npm/extconf.rb b/ext/npm/extconf.rb
new file mode 100644
index 0000000..7dce95c
--- /dev/null
+++ b/ext/npm/extconf.rb
@@ -0,0 +1,3 @@
+File.write 'Makefile',
+ "make:\n\t\ninstall:\n\truby install.rb\nclean:\n\t\n"
+
diff --git a/ext/npm/install.rb b/ext/npm/install.rb
new file mode 100644
index 0000000..793ac21
--- /dev/null
+++ b/ext/npm/install.rb
@@ -0,0 +1,4 @@
+require_relative '../../lib/npm_commands'
+
+NpmCommands.new.install('--production', '.') ||
+ raise('Error while installing npm dependencies')
diff --git a/lib/critical-path-css-rails.rb b/lib/critical-path-css-rails.rb
index be64a3c..f4c431f 100644
--- a/lib/critical-path-css-rails.rb
+++ b/lib/critical-path-css-rails.rb
@@ -1,32 +1,38 @@
-module CriticalPathCss
- require 'critical_path_css/css_fetcher'
+require 'critical_path_css/configuration'
+require 'critical_path_css/css_fetcher'
+require 'critical_path_css/rails/config_loader'
+module CriticalPathCss
CACHE_NAMESPACE = 'critical-path-css'
def self.generate(route)
- Rails.cache.write(
+ ::Rails.cache.write(
route,
- CssFetcher.new.fetch_route(route),
+ CssFetcher.new(config).fetch_route(route),
namespace: CACHE_NAMESPACE,
expires_in: nil
)
end
def self.generate_all
- CssFetcher.new.fetch.each do |route, css|
- Rails.cache.write(route, css, namespace: CACHE_NAMESPACE, expires_in: nil)
+ CssFetcher.new(config).fetch.each do |route, css|
+ ::Rails.cache.write(route, css, namespace: CACHE_NAMESPACE, expires_in: nil)
end
end
def self.clear(route)
- Rails.cache.delete(route, namespace: CACHE_NAMESPACE)
+ ::Rails.cache.delete(route, namespace: CACHE_NAMESPACE)
end
def self.clear_matched(routes)
- Rails.cache.delete_matched(routes, namespace: CACHE_NAMESPACE)
+ ::Rails.cache.delete_matched(routes, namespace: CACHE_NAMESPACE)
end
def self.fetch(route)
- Rails.cache.read(route, namespace: CACHE_NAMESPACE) || ''
+ ::Rails.cache.read(route, namespace: CACHE_NAMESPACE) || ''
+ end
+
+ def self.config
+ @config ||= Configuration.new(CriticalPathCss::Rails::ConfigLoader.new.load)
end
end
diff --git a/lib/critical_path_css/configuration.rb b/lib/critical_path_css/configuration.rb
index 737a5f3..2e4b794 100644
--- a/lib/critical_path_css/configuration.rb
+++ b/lib/critical_path_css/configuration.rb
@@ -1,39 +1,29 @@
require 'erb'
module CriticalPathCss
class Configuration
- CONFIGURATION_FILENAME = 'critical_path_css.yml'
- def initialize
- @configurations = YAML.load(ERB.new(File.read(configuration_file_path)).result)[Rails.env]
+ def initialize(config)
+ @config = config
end
def base_url
- @configurations['base_url']
+ @config['base_url']
end
def css_path
- @css_path ||= begin
- relative_path = @configurations['css_path'] || manifest_path
- "#{Rails.root}/public#{relative_path}"
- end
+ @config['css_path']
end
def manifest_name
- @configurations['manifest_name']
+ @config['manifest_name']
end
def routes
- @configurations['routes']
+ @config['routes']
end
- private
-
- def configuration_file_path
- @configuration_file_path ||= Rails.root.join('config', CONFIGURATION_FILENAME)
- end
-
- def manifest_path
- @manifest_path ||= ActionController::Base.helpers.stylesheet_path(manifest_name, host: '')
+ def penthouse_options
+ @config['penthouse_options'] || {}
end
end
end
diff --git a/lib/critical_path_css/css_fetcher.rb b/lib/critical_path_css/css_fetcher.rb
index 90fbb2f..bda7314 100644
--- a/lib/critical_path_css/css_fetcher.rb
+++ b/lib/critical_path_css/css_fetcher.rb
@@ -1,12 +1,12 @@
+require 'json'
+require 'open3'
+
module CriticalPathCss
class CssFetcher
- require 'phantomjs'
- require 'critical_path_css/configuration'
-
- PENTHOUSE_PATH = "#{File.dirname(__FILE__)}/../penthouse/penthouse.js"
+ GEM_ROOT = File.expand_path(File.join('..', '..'), File.dirname(__FILE__))
- def initialize
- @config = Configuration.new
+ def initialize(config)
+ @config = config
end
def fetch
@@ -20,15 +20,50 @@ def fetch_route(route)
protected
def css_for_route(route)
- url = @config.base_url + route
-
- Phantomjs.run(
- '--ignore-ssl-errors=true',
- '--ssl-protocol=tlsv1',
- PENTHOUSE_PATH,
- url,
- @config.css_path
- )
+ options = {
+ 'url' => @config.base_url + route,
+ 'css' => @config.css_path,
+ ## optional params
+ # viewport dimensions
+ 'width' => 1300,
+ 'height' => 900,
+ # CSS selectors to always include, e.g.:
+ 'forceInclude' => [
+ # '.keepMeEvenIfNotSeenInDom',
+ # '^\.regexWorksToo'
+ ],
+ # ms; abort critical CSS generation after this timeout
+ 'timeout' => 30000,
+ # set to true to throw on CSS errors (will run faster if no errors)
+ 'strict' => false,
+ # characters; strip out inline base64 encoded resources larger than this
+ 'maxEmbeddedBase64Length' => 1000,
+ # specify which user agent string when loading the page
+ 'userAgent' => 'Penthouse Critical Path CSS Generator',
+ # ms; render wait timeout before CSS processing starts (default: 100)
+ 'renderWaitTime' => 100,
+ # set to false to load (external) JS (default: true)
+ 'blockJSRequests' => true,
+ # see `phantomjs --help` for the list of all available options
+ 'phantomJsOptions' => {
+ 'ignore-ssl-errors' => true,
+ 'ssl-protocol' => 'tlsv1'
+ },
+ 'customPageHeaders' => {
+ # use if getting compression errors like 'Data corrupted':
+ 'Accept-Encoding' => 'identity'
+ }
+ }.merge(@config.penthouse_options)
+ out, err, st = Dir.chdir(GEM_ROOT) do
+ Open3.capture3('node', 'lib/fetch-css.js', JSON.dump(options))
+ end
+ if !st.exitstatus.zero? || out.empty? && !err.empty?
+ STDOUT.puts out
+ STDERR.puts err
+ raise "Failed to get CSS for route #{route}\n" \
+ " with options=#{options.inspect}"
+ end
+ out
end
end
end
diff --git a/lib/critical_path_css/rails/config_loader.rb b/lib/critical_path_css/rails/config_loader.rb
new file mode 100644
index 0000000..c9a6add
--- /dev/null
+++ b/lib/critical_path_css/rails/config_loader.rb
@@ -0,0 +1,24 @@
+module CriticalPathCss
+ module Rails
+ class ConfigLoader
+ CONFIGURATION_FILENAME = 'critical_path_css.yml'
+
+ def load
+ config = YAML.load(ERB.new(File.read(configuration_file_path)).result)[::Rails.env]
+ config['css_path'] = "#{::Rails.root}/public" + (
+ config['css_path'] ||
+ ActionController::Base.helpers.stylesheet_path(
+ config['manifest_name'], host: ''
+ )
+ )
+ config
+ end
+
+ private
+
+ def configuration_file_path
+ @configuration_file_path ||= ::Rails.root.join('config', CONFIGURATION_FILENAME)
+ end
+ end
+ end
+end
diff --git a/lib/critical_path_css/rails/version.rb b/lib/critical_path_css/rails/version.rb
index dad8e18..ec32863 100644
--- a/lib/critical_path_css/rails/version.rb
+++ b/lib/critical_path_css/rails/version.rb
@@ -1,6 +1,5 @@
module CriticalPathCSS
module Rails
- VERSION = '0.4.0'
- PENTHOUSE_VERSION = '0.3.4'
+ VERSION = '1.0.0'
end
end
diff --git a/lib/fetch-css.js b/lib/fetch-css.js
new file mode 100644
index 0000000..6f1db63
--- /dev/null
+++ b/lib/fetch-css.js
@@ -0,0 +1,14 @@
+const penthouse = require('penthouse');
+const fs = require('fs');
+
+const penthouseOptions = JSON.parse(process.argv[2]);
+
+const STDOUT_FD = 1;
+const STDERR_FD = 2;
+
+penthouse(penthouseOptions).then(function(criticalCss) {
+ fs.writeSync(STDOUT_FD, criticalCss);
+}).catch(function(err) {
+ fs.writeSync(STDERR_FD, err);
+ process.exit(1);
+});
diff --git a/lib/npm_commands.rb b/lib/npm_commands.rb
new file mode 100644
index 0000000..5f50095
--- /dev/null
+++ b/lib/npm_commands.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# NPM wrapper with helpful error messages
+class NpmCommands
+
+ # @return [Boolean] whether the installation succeeded
+ def install(*args) # rubocop:disable Metrics/MethodLength
+ return false unless check_nodejs_installed
+ STDERR.puts 'Installing npm dependencies...'
+ install_status = Dir.chdir File.expand_path('..', File.dirname(__FILE__)) do
+ system('npm', 'install', *args)
+ end
+ STDERR.puts(
+ *if install_status
+ ['npm dependencies installed']
+ else
+ ['-' * 60,
+ 'Error: npm dependencies installation failed',
+ '-' * 60]
+ end
+ )
+ install_status
+ end
+
+ private
+
+ def check_nodejs_installed # rubocop:disable Metrics/MethodLength
+ return true if executable?('node')
+ STDERR.puts(
+ '-' * 60,
+ 'Error: critical-path-css-rails requires NodeJS and NPM.',
+ *if executable?('brew')
+ [' To install NodeJS and NPM, run:',
+ ' brew install node']
+ elsif Gem.win_platform?
+ [' To install NodeJS and NPM, we recommend:',
+ ' https://github.com/coreybutler/nvm-windows/releases']
+ else
+ [' To install NodeJS and NPM, we recommend:',
+ ' https://github.com/creationix/nvm']
+ end,
+ '-' * 60
+ )
+ end
+
+ def executable?(cmd)
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
+ exts.each do |ext|
+ exe = File.join(path, "#{cmd}#{ext}")
+ return exe if File.executable?(exe) && !File.directory?(exe)
+ end
+ end
+ nil
+ end
+end
diff --git a/lib/penthouse/penthouse.js b/lib/penthouse/penthouse.js
deleted file mode 100644
index ff585dd..0000000
--- a/lib/penthouse/penthouse.js
+++ /dev/null
@@ -1,601 +0,0 @@
-/*
-Penthouse CSS Critical Path Generator
-https://github.com/pocketjoso/penthouse
-Author: Jonas Ohlsson
-License: MIT
-Version: 0.3.4
-
-USAGE:
- phantomjs penthouse.js [options]
- Options:
- --width The viewport width in pixels. Defaults to 1300
- --height The viewport height in pixels. Defaults to 900
-
- to run on HTTPS sites two flags must be passed in, directly after phantomjs in the call:
- --ignore-ssl-errors=true --ssl-protocol=tlsv1
-
-DEPENDENCIES
- + "phantomjs" : "~1.9.7"
-
-*/
-
-
-(function() { "use strict";
-/*
- * parser for the script - can be used both for the standalone node binary and the phantomjs script
- */
-
-/*jshint unused:false*/
-
-var usageString = '[--width ] [--height ] ';
-
-function buildError(msg, problemToken, args) {
- var error = new Error(msg + problemToken);
- error.token = problemToken;
- error.args = args;
- throw error;
-}
-
-// Parses the arguments passed in
-// @returns { width, height, url, css }
-// throws an error on wrong options or parsing error
-function parseOptions(argsOriginal) {
- var args = argsOriginal.slice(0),
- validOptions = ['--width', '--height'],
- parsed = {},
- val,
- len = args.length,
- optIndex,
- option;
-
- if (len < 2) buildError('Not enough arguments, ', args);
-
- while (args.length > 2 && args[0].match(/^(--width|--height)$/)) {
- optIndex = validOptions.indexOf(args[0]);
- if (optIndex === -1) buildError('Logic/Parsing error ', args[0], args);
-
- // lose the dashes
- option = validOptions[optIndex].slice(2);
- val = args[1];
-
- parsed[option] = parseInt(val, 10);
- if (isNaN(parsed[option])) buildError('Parsing error when parsing ', val, args);
-
- // remove the two parsed arguments from the list
- args = args.slice(2);
- }
- parsed.url = args[0];
- parsed.css = args[1];
-
- if (!parsed.url) {
- buildError('Missing url/path to html file', '', args);
- }
-
- if (!parsed.css) {
- buildError('Missing css file', '', args);
- }
-
-
- return parsed;
-}
-
-if (typeof module !== 'undefined') {
- module.exports = exports = {
- parse: parseOptions,
- usage: usageString
- };
-}
-/*
-module for removing unused fontface rules - can be used both for the standalone node binary and the phantomjs script
-*/
-/*jshint unused:false*/
-
-function unusedFontfaceRemover (css){
- var toDeleteSections = [];
-
- //extract full @font-face rules
- var fontFaceRegex = /(@font-face[ \s\S]*?\{([\s\S]*?)\})/gm,
- ff;
-
- while ((ff = fontFaceRegex.exec(css)) !== null) {
-
- //grab the font name declared in the @font-face rule
- //(can still be in quotes, f.e. 'Lato Web'
- var t = /font-family[^:]*?:[ ]*([^;]*)/.exec(ff[1]);
- if (typeof t[1] === 'undefined')
- continue; //no font-family in @fontface rule!
-
- //rm quotes
- var fontName = t[1].replace(/['"]/gm, '');
-
- // does this fontname appear as a font-family or font (shorthand) value?
- var fontNameRegex = new RegExp('([^{}]*?){[^}]*?font(-family)?[^:]*?:[^;]*' + fontName + '[^,;]*[,;]', 'gmi');
-
-
- var fontFound = false,
- m;
-
- while ((m = fontNameRegex.exec(css)) !== null) {
- if (m[1].indexOf('@font-face') === -1) {
- //log('FOUND, keep rule');
- fontFound = true;
- break;
- }
- }
- if (!fontFound) {
- //NOT FOUND, rm!
-
- //can't remove rule here as it will screw up ongoing while (exec ...) loop.
- //instead: save indices and delete AFTER for loop
- var closeRuleIndex = css.indexOf('}', ff.index);
- //unshift - add to beginning of array - we need to remove rules in reverse order,
- //otherwise indeces will become incorrect again.
- toDeleteSections.unshift({
- start: ff.index,
- end: closeRuleIndex + 1
- });
- }
- }
- //now delete the @fontface rules we registed as having no matches in the css
- for (var i = 0; i < toDeleteSections.length; i++) {
- var start = toDeleteSections[i].start,
- end = toDeleteSections[i].end;
- css = css.substring(0, start) + css.substring(end);
- }
-
- return css;
-};
-
-
-
-if(typeof module !== 'undefined') {
- module.exports = unusedFontfaceRemover;
-}
-/*jshint unused:false*/
-
-/* === preFormatCSS ===
- * preformats the css to ensure we won't run into and problems in our parsing
- * removes comments (actually would be anough to remove/replace {} chars.. TODO
- * replaces } char inside content: '' properties.
- */
-
-function cssPreformatter (css){
- //remove comments from css (including multi-line coments)
- css = css.replace(/\/\*[\s\S]*?\*\//g, '');
-
- //replace Windows \r\n with \n,
- //otherwise final output might get converted into /r/r/n
- css = css.replace(/\r\n/gm, '\n');
-
- //we also need to replace eventual close curly bracket characters inside content: '' property declarations, replace them with their ASCI code equivalent
- //\7d (same as: '\' + '}'.charCodeAt(0).toString(16) );
-
- var m,
- regexP = /(content\s*:\s*['"][^'"]*)}([^'"]*['"])/gm,
- matchedData = [];
-
- //for each content: '' rule that contains at least one end bracket ('}')
- while ((m = regexP.exec(css)) !== null) {
- //we need to replace ALL end brackets in the rule
- //we can't do it in here, because it will mess up ongoing exec, store data and do after
-
- //unshift - add to beginning of array - we need to remove rules in reverse order,
- //otherwise indeces will become incorrect.
- matchedData.unshift({
- start: m.index,
- end: m.index + m[0].length,
- replaceStr: m[0].replace(/\}/gm, '\\7d')
- });
- }
-
- for (var i = 0; i < matchedData.length; i++) {
- var item = matchedData[0];
- css = css.substring(0, item.start) + item.replaceStr + css.substring(item.end);
- }
-
- return css;
-};
-
-if(typeof module !== 'undefined') {
- module.exports = cssPreformatter;
-}
-var standaloneMode = true;
-'use strict';
-var standaloneMode = standaloneMode || false;
-
-var page = require('webpage').create(),
- fs = require('fs'),
- system = require('system'),
- DEBUG = false,
- stdout = system.stdout; // for using this as a file
-
-var combineArgsString = function(argsArr) {
- return [].join.call(argsArr, ' ') + '\n';
-};
-
-// monkey patch for directing errors to stderr
-// https://github.com/ariya/phantomjs/issues/10150#issuecomment-28707859
-var errorlog = function() {
- system.stderr.write(combineArgsString(arguments));
-};
-
-var debug = function() {
- if (DEBUG) errorlog('DEBUG: ' + combineArgsString(arguments));
-};
-
-// discard stdout from phantom exit;
-var phantomExit = function(code) {
- if (page) {
- page.close();
- }
- setTimeout(function() {
- phantom.exit(code);
- }, 0);
-};
-
-//don't confuse analytics more than necessary when visiting websites
-page.settings.userAgent = 'Penthouse Critical Path CSS Generator';
-
-/* prevent page JS errors from being output to final CSS */
-page.onError = function(msg, trace) {
- //do nothing
-};
-
-page.onResourceError = function(resourceError) {
- page.reason = resourceError.errorString;
- page.reason_url = resourceError.url;
-};
-
-var main = function(options) {
- debug('main(): ', JSON.stringify(options));
-//final cleanup
-//remove all empty rules, and remove leading/trailing whitespace
- try {
- var f = fs.open(options.css, 'r');
-
- //preformat css
- var cssPreformat;
- if (standaloneMode) {
- cssPreformat = cssPreformatter;
- } else {
- cssPreformat = require('./css-preformatter.js');
- }
- options.css = cssPreformat(f.read());
- } catch (e) {
- errorlog(e);
- phantomExit(1);
- }
-
- // start the critical path CSS generation
- getCriticalPathCss(options);
-};
-
-function cleanup(css) {
- //remove all animation rules, as keyframes have already been removed
- css = css.replace(/(-webkit-|-moz-|-ms-|-o-)?animation[ ]?:[^;{}]*;/gm, '');
- //remove all empty rules, and remove leading/trailing whitespace
- return css.replace(/[^{}]*\{\s*\}/gm, '').trim();
-}
-
-/* Final function
- * Get's called from getCriticalPathCss when CSS extraction from page is done*/
-page.onCallback = function(css) {
- debug('phantom.onCallback');
-
- try {
- if (css) {
- // we are done - clean up the final css
- var finalCss = cleanup(css);
-
- // remove unused @fontface rules
- var ffRemover;
- if (standaloneMode) {
- ffRemover = unusedFontfaceRemover;
- } else {
- ffRemover = require('./unused-fontface-remover.js');
- }
- finalCss = ffRemover(finalCss);
-
- if(finalCss.trim().length === 0){
- errorlog('Note: Generated critical css was empty for URL: ' + options.url);
- }
-
- // return the critical css!
- stdout.write(finalCss);
- phantomExit(0);
- } else {
- // No css. This is not an error on our part
- // but still safer to warn the end user, in case they made a mistake
- errorlog('Note: Generated critical css was empty for URL: ' + options.url);
- // for consisteny, still generate output (will be empty)
- stdout.write(css);
- phantomExit(0);
- }
-
- } catch (ex) {
- debug('phantom.onCallback -> error', ex);
- errorlog('error: ' + ex);
- phantomExit(1);
- }
-};
-
-/*
- * Tests each selector in css file at specified resolution,
- * to see if any such elements appears above the fold on the page
- * modifies CSS - removes selectors that don't appear, and empty rules
- *
- * @param options.url the url as a string
- * @param options.css the css as a string
- * @param options.width the width of viewport
- * @param options.height the height of viewport
- ---------------------------------------------------------*/
-function getCriticalPathCss(options) {
- debug('getCriticalPathCss():', JSON.stringify(options));
-
- page.viewportSize = {
- width: options.width,
- height: options.height
- };
-
- page.open(options.url, function(status) {
- if (status !== 'success') {
- errorlog('Error opening url \'' + page.reason_url + '\': ' + page.reason);
- phantomExit(1);
- } else {
-
- debug('Starting sandboxed evaluation of CSS\n', options.css);
- // sandboxed environments - no outside references
- // arguments and return value must be primitives
- // @see http://phantomjs.org/api/webpage/method/evaluate.html
- page.evaluate(function sandboxed(css) {
- var h = window.innerHeight,
- renderWaitTime = 100, //ms TODO: user specifiable through options object
- finished = false,
- currIndex = 0,
- forceRemoveNestedRule = false;
-
- //split CSS so we can value the (selector) rules separately.
- //but first, handle stylesheet initial non nested @-rules.
- //they don't come with any associated rules, and should all be kept,
- //so just keep them in critical css, but don't include them in split
- var splitCSS = css.replace(/@(import|charset|namespace)[^;]*;/g, '');
- var split = splitCSS.split(/[{}]/g);
-
- var getNewValidCssSelector = function(i) {
- var newSel = split[i];
- /* HANDLE Nested @-rules */
-
- /*Case 1: @-rule with CSS properties inside [REMAIN]
- Can't remove @font-face rules here, don't know if used or not.
- Another check at end for this purpose.
- */
- if (/@(font-face)/gi.test(newSel)) {
- //skip over this rule
- currIndex = css.indexOf('}', currIndex) + 1;
- return getNewValidCssSelector(i + 2);
- }
- /*Case 2: @-rule with CSS properties inside [REMOVE]
- @page
- This case doesn't need any special handling,
- as this "selector" won't match anything on the page,
- and will therefor be removed, together with it's css props
- */
-
- /*Case 4: @-rule with full CSS (rules) inside [REMOVE]
- @media print|speech|aural, @keyframes
- Delete this rule and all its contents - doesn't belong in critical path CSS
- */
- else if (/@(media (print|speech|aural)|(([a-z\-])*keyframes))/gi.test(newSel)) {
- //force delete on child css rules
- forceRemoveNestedRule = true;
- return getNewValidCssSelector(i + 1);
- }
-
- /*Case 3: @-rule with full CSS (rules) inside [REMAIN]
- This test is executed AFTER Case 4,
- since we here match every remaining @media,
- after @media print has been removed by Case 4 rule)
- - just skip this particular line (i.e. keep), and continue checking the CSS inside as normal
- */
- else if (/@(media|(-moz-)?document|supports)/gi.test(newSel)) {
- return getNewValidCssSelector(i + 1);
- }
- /*
- Resume normal execution after end of @-media rule with inside CSS rules (Case 3)
- Also identify abrupt file end.
- */
- else if (newSel.trim().length === 0) {
- //abrupt file end
- if (i + 1 >= split.length) {
- //end of file
- finished = true;
- return false;
- }
- //end of @-rule (Case 3)
- forceRemoveNestedRule = false;
- return getNewValidCssSelector(i + 1);
- }
- return i;
- };
-
- var removeSelector = function(sel, selectorsKept) {
- var selPos = css.indexOf(sel, currIndex);
-
- //check what comes next: { or ,
- var nextComma = css.indexOf(',', selPos);
- var nextOpenBracket = css.indexOf('{', selPos);
-
- if (selectorsKept > 0 || (nextComma > 0 && nextComma < nextOpenBracket)) {
- //we already kept selectors from this rule, so rule will stay
-
- //more selectors in selectorList, cut until (and including) next comma
- if (nextComma > 0 && nextComma < nextOpenBracket) {
- css = css.substring(0, selPos) + css.substring(nextComma + 1);
- }
- //final selector, cut until open bracket. Also remove previous comma, as the (new) last selector should not be followed by a comma.
- else {
- var prevComma = css.lastIndexOf(',', selPos);
- css = css.substring(0, prevComma) + css.substring(nextOpenBracket);
- }
- } else {
- //no part of selector (list) matched elements above fold on page - remove whole rule CSS rule
- var endRuleBracket = css.indexOf('}', nextOpenBracket);
-
- css = css.substring(0, selPos) + css.substring(endRuleBracket + 1);
- }
- };
-
-
- var processCssRules = function() {
- for (var i = 0; i < split.length; i = i + 2) {
- //step over non DOM CSS selectors (@-rules)
- i = getNewValidCssSelector(i);
-
- //reach end of CSS
- if (finished) {
- //call final function to exit outside of phantom evaluate scope
- window.callPhantom(css);
- }
-
- var fullSel = split[i];
- //fullSel can contain combined selectors
- //,f.e. body, html {}
- //split and check one such selector at the time.
- var selSplit = fullSel.split(',');
- //keep track - if we remove all selectors, we also want to remove the whole rule.
- var selectorsKept = 0;
- var aboveFold;
-
- for (var j = 0; j < selSplit.length; j++) {
- var sel = selSplit[j];
-
- //some selectors can't be matched on page.
- //In these cases we test a slightly modified selectors instead, temp.
- var temp = sel;
-
- if (sel.indexOf(':') > -1) {
- //handle special case selectors, the ones that contain a semi colon (:)
- //many of these selectors can't be matched to anything on page via JS,
- //but that still might affect the above the fold styling
-
- //these psuedo selectors depend on an element,
- //so test element instead (would do the same for f.e. :hover, :focus, :active IF we wanted to keep them for critical path css, but we don't)
- temp = temp.replace(/(:?:before|:?:after)*/g, '');
-
- //if selector is purely psuedo (f.e. ::-moz-placeholder), just keep as is.
- //we can't match it to anything on page, but it can impact above the fold styles
- if (temp.replace(/:[:]?([a-zA-Z0-9\-\_])*/g, '').trim().length === 0) {
- currIndex = css.indexOf(sel, currIndex) + sel.length;
- selectorsKept++;
- continue;
- }
-
- //handle browser specific psuedo selectors bound to elements,
- //Example, button::-moz-focus-inner, input[type=number]::-webkit-inner-spin-button
- //remove browser specific pseudo and test for element
- temp = temp.replace(/:?:-[a-z-]*/g, '');
- }
-
- if (!forceRemoveNestedRule) {
- //now we have a selector to test, first grab any matching elements
- var el;
- try {
- el = document.querySelectorAll(temp);
- } catch (e) {
- //not a valid selector, remove it.
- removeSelector(sel, 0);
- continue;
- }
-
- //check if selector matched element(s) on page..
- aboveFold = false;
-
- for (var k = 0; k < el.length; k++) {
- var testEl = el[k];
- //temporarily force clear none in order to catch elements that clear previous content themselves and who w/o their styles could show up unstyled in above the fold content (if they rely on f.e. 'clear:both;' to clear some main content)
- testEl.style.clear = 'none';
-
- //check to see if any matched element is above the fold on current page
- //(in current viewport size)
- if (testEl.getBoundingClientRect().top < h) {
- //then we will save this selector
- aboveFold = true;
- selectorsKept++;
-
- //update currIndex so we only search from this point from here on.
- currIndex = css.indexOf(sel, currIndex);
-
- //set clear style back to what it was
- testEl.style.clear = '';
- //break, because matching 1 element is enough
- break;
- }
- //set clear style back to what it was
- testEl.style.clear = '';
- }
- } else {
- aboveFold = false;
- } //force removal of selector
-
- //if selector didn't match any elements above fold - delete selector from CSS
- if (aboveFold === false) {
- //update currIndex so we only search from this point from here on.
- currIndex = css.indexOf(sel, currIndex);
- //remove seletor (also removes rule, if nnothing left)
- removeSelector(sel, selectorsKept);
- }
- }
- //if rule stayed, move our cursor forward for matching new selectors
- if (selectorsKept > 0) {
- currIndex = css.indexOf('}', currIndex) + 1;
- }
- }
-
- //we're done - call final function to exit outside of phantom evaluate scope
- window.callPhantom(css);
- };
-
- //give some time (renderWaitTime) for sites like facebook that build their page dynamically,
- //otherwise we can miss some selectors (and therefor rules)
- //--tradeoff here: if site is too slow with dynamic content,
- // it doesn't deserve to be in critical path.
- setTimeout(processCssRules, renderWaitTime);
-
- }, options.css);
- }
- });
-}
-
-var parser, parse, usage, options;
-
-// test to see if we are running as a standalone script
-// or as part of the node module
-if (standaloneMode) {
- parse = parseOptions;
- usage = usageString;
-} else {
- parser = require('../options-parser');
- parse = parser.parse;
- usage = parser.usage;
-}
-
-try {
- options = parse(system.args.slice(1));
-} catch (ex) {
-
- errorlog('Caught error parsing arguments: ' + ex.message);
-
- // the usage string does not make sense to show if running via Node
- if(standaloneMode) {
- errorlog('\nUsage: phantomjs penthouse.js ' + usage);
- }
-
- phantomExit(1);
-}
-
-// set defaults
-if (!options.width) options.width = 1300;
-if (!options.height) options.height = 900;
-
-main(options);
-})();
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..93145c6
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,782 @@
+{
+ "name": "critical-path-css-rails",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ },
+ "apartment": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/apartment/-/apartment-1.1.1.tgz",
+ "integrity": "sha1-/ZQGzcyodTWULxWzYKGrWkqpfiY=",
+ "requires": {
+ "css": "git+https://github.com/pocketjoso/css.git#8ddea7e3cbc0a183ecf694a7a5fbc84326893893",
+ "get-stdin": "5.0.1",
+ "lodash": "3.10.1",
+ "minimist": "1.2.0"
+ }
+ },
+ "asn1": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
+ "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
+ },
+ "assert-plus": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
+ "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ="
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+ },
+ "atob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz",
+ "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M="
+ },
+ "aws-sign2": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
+ "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8="
+ },
+ "aws4": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
+ "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
+ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
+ "optional": true,
+ "requires": {
+ "tweetnacl": "0.14.5"
+ }
+ },
+ "boom": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
+ "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+ "requires": {
+ "hoek": "2.16.3"
+ }
+ },
+ "caseless": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
+ "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c="
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "requires": {
+ "ansi-styles": "2.2.1",
+ "escape-string-regexp": "1.0.5",
+ "has-ansi": "2.0.0",
+ "strip-ansi": "3.0.1",
+ "supports-color": "2.0.0"
+ }
+ },
+ "combined-stream": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
+ "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
+ "requires": {
+ "delayed-stream": "1.0.0"
+ }
+ },
+ "commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="
+ },
+ "concat-stream": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz",
+ "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=",
+ "requires": {
+ "inherits": "2.0.3",
+ "readable-stream": "2.0.6",
+ "typedarray": "0.0.6"
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "cryptiles": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
+ "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+ "requires": {
+ "boom": "2.10.1"
+ }
+ },
+ "css": {
+ "version": "git+https://github.com/pocketjoso/css.git#8ddea7e3cbc0a183ecf694a7a5fbc84326893893",
+ "requires": {
+ "inherits": "2.0.3",
+ "source-map": "0.1.43",
+ "source-map-resolve": "0.3.1",
+ "urix": "0.1.0"
+ }
+ },
+ "css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA="
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "requires": {
+ "assert-plus": "1.0.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ }
+ }
+ },
+ "debug": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
+ "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk="
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "ecc-jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
+ "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+ "optional": true,
+ "requires": {
+ "jsbn": "0.1.1"
+ }
+ },
+ "es6-promise": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.0.5.tgz",
+ "integrity": "sha1-eILzCt3lskDM+n99eMVIMwlRrkI="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "extend": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
+ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
+ },
+ "extract-zip": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz",
+ "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=",
+ "requires": {
+ "concat-stream": "1.5.0",
+ "debug": "0.7.4",
+ "mkdirp": "0.5.0",
+ "yauzl": "2.4.1"
+ }
+ },
+ "extsprintf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz",
+ "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA="
+ },
+ "fd-slicer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+ "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+ "requires": {
+ "pend": "1.2.0"
+ }
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+ },
+ "form-data": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
+ "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
+ "requires": {
+ "asynckit": "0.4.0",
+ "combined-stream": "1.0.5",
+ "mime-types": "2.1.16"
+ }
+ },
+ "fs-extra": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz",
+ "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=",
+ "requires": {
+ "graceful-fs": "4.1.11",
+ "jsonfile": "2.4.0",
+ "klaw": "1.3.1"
+ }
+ },
+ "generate-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
+ "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ="
+ },
+ "generate-object-property": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
+ "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
+ "requires": {
+ "is-property": "1.0.2"
+ }
+ },
+ "get-stdin": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
+ "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g="
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "requires": {
+ "assert-plus": "1.0.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ }
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
+ },
+ "har-validator": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
+ "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
+ "requires": {
+ "chalk": "1.1.3",
+ "commander": "2.11.0",
+ "is-my-json-valid": "2.16.0",
+ "pinkie-promise": "2.0.1"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "requires": {
+ "ansi-regex": "2.1.1"
+ }
+ },
+ "hasha": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz",
+ "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=",
+ "requires": {
+ "is-stream": "1.1.0",
+ "pinkie-promise": "2.0.1"
+ }
+ },
+ "hawk": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+ "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+ "requires": {
+ "boom": "2.10.1",
+ "cryptiles": "2.0.5",
+ "hoek": "2.16.3",
+ "sntp": "1.0.9"
+ }
+ },
+ "hoek": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
+ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
+ },
+ "http-signature": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
+ "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
+ "requires": {
+ "assert-plus": "0.2.0",
+ "jsprim": "1.4.0",
+ "sshpk": "1.13.1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "is-my-json-valid": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz",
+ "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=",
+ "requires": {
+ "generate-function": "2.0.0",
+ "generate-object-property": "1.2.0",
+ "jsonpointer": "4.0.1",
+ "xtend": "4.0.1"
+ }
+ },
+ "is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ="
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "optional": true
+ },
+ "jsesc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+ "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s="
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+ },
+ "jsonfile": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+ "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+ "requires": {
+ "graceful-fs": "4.1.11"
+ }
+ },
+ "jsonpointer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
+ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk="
+ },
+ "jsprim": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz",
+ "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=",
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.0.2",
+ "json-schema": "0.2.3",
+ "verror": "1.3.6"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ }
+ }
+ },
+ "kew": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
+ "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s="
+ },
+ "klaw": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+ "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
+ "requires": {
+ "graceful-fs": "4.1.11"
+ }
+ },
+ "lodash": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+ "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
+ },
+ "mime-db": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz",
+ "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg="
+ },
+ "mime-types": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz",
+ "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=",
+ "requires": {
+ "mime-db": "1.29.0"
+ }
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ },
+ "mkdirp": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
+ "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ }
+ }
+ },
+ "oauth-sign": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+ "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+ },
+ "pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+ },
+ "penthouse": {
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/penthouse/-/penthouse-0.11.5.tgz",
+ "integrity": "sha1-/+y0LwrsYkhYWC8nLqLcIYGHyVI=",
+ "requires": {
+ "apartment": "1.1.1",
+ "css": "git+https://github.com/pocketjoso/css.git#8ddea7e3cbc0a183ecf694a7a5fbc84326893893",
+ "css-mediaquery": "0.1.2",
+ "jsesc": "1.3.0",
+ "os-tmpdir": "1.0.2",
+ "phantomjs-prebuilt": "2.1.14",
+ "regenerator-runtime": "0.10.5",
+ "tmp": "0.0.31"
+ }
+ },
+ "phantomjs-prebuilt": {
+ "version": "2.1.14",
+ "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz",
+ "integrity": "sha1-1T0xH8+30dCN2yQBRVjxGIxRbaA=",
+ "requires": {
+ "es6-promise": "4.0.5",
+ "extract-zip": "1.5.0",
+ "fs-extra": "1.0.0",
+ "hasha": "2.2.0",
+ "kew": "0.7.0",
+ "progress": "1.1.8",
+ "request": "2.79.0",
+ "request-progress": "2.0.1",
+ "which": "1.2.14"
+ }
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "requires": {
+ "pinkie": "2.0.4"
+ }
+ },
+ "process-nextick-args": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+ },
+ "progress": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74="
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+ },
+ "qs": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
+ "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw="
+ },
+ "readable-stream": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+ "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+ "requires": {
+ "core-util-is": "1.0.2",
+ "inherits": "2.0.3",
+ "isarray": "1.0.0",
+ "process-nextick-args": "1.0.7",
+ "string_decoder": "0.10.31",
+ "util-deprecate": "1.0.2"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+ "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
+ },
+ "request": {
+ "version": "2.79.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
+ "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
+ "requires": {
+ "aws-sign2": "0.6.0",
+ "aws4": "1.6.0",
+ "caseless": "0.11.0",
+ "combined-stream": "1.0.5",
+ "extend": "3.0.1",
+ "forever-agent": "0.6.1",
+ "form-data": "2.1.4",
+ "har-validator": "2.0.6",
+ "hawk": "3.1.3",
+ "http-signature": "1.1.1",
+ "is-typedarray": "1.0.0",
+ "isstream": "0.1.2",
+ "json-stringify-safe": "5.0.1",
+ "mime-types": "2.1.16",
+ "oauth-sign": "0.8.2",
+ "qs": "6.3.2",
+ "stringstream": "0.0.5",
+ "tough-cookie": "2.3.2",
+ "tunnel-agent": "0.4.3",
+ "uuid": "3.1.0"
+ }
+ },
+ "request-progress": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz",
+ "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=",
+ "requires": {
+ "throttleit": "1.0.0"
+ }
+ },
+ "resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
+ },
+ "sntp": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
+ "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+ "requires": {
+ "hoek": "2.16.3"
+ }
+ },
+ "source-map": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+ "requires": {
+ "amdefine": "1.0.1"
+ }
+ },
+ "source-map-resolve": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz",
+ "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=",
+ "requires": {
+ "atob": "1.1.3",
+ "resolve-url": "0.2.1",
+ "source-map-url": "0.3.0",
+ "urix": "0.1.0"
+ }
+ },
+ "source-map-url": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz",
+ "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk="
+ },
+ "sshpk": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
+ "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
+ "requires": {
+ "asn1": "0.2.3",
+ "assert-plus": "1.0.0",
+ "bcrypt-pbkdf": "1.0.1",
+ "dashdash": "1.14.1",
+ "ecc-jsbn": "0.1.1",
+ "getpass": "0.1.7",
+ "jsbn": "0.1.1",
+ "tweetnacl": "0.14.5"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ },
+ "stringstream": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
+ "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "requires": {
+ "ansi-regex": "2.1.1"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+ },
+ "throttleit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+ "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw="
+ },
+ "tmp": {
+ "version": "0.0.31",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz",
+ "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=",
+ "requires": {
+ "os-tmpdir": "1.0.2"
+ }
+ },
+ "tough-cookie": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
+ "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=",
+ "requires": {
+ "punycode": "1.4.1"
+ }
+ },
+ "tunnel-agent": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
+ "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us="
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "optional": true
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
+ "urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "uuid": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
+ "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
+ },
+ "verror": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz",
+ "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=",
+ "requires": {
+ "extsprintf": "1.0.2"
+ }
+ },
+ "which": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
+ "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=",
+ "requires": {
+ "isexe": "2.0.0"
+ }
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
+ },
+ "yauzl": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+ "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+ "requires": {
+ "fd-slicer": "1.0.1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1e13ae2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "critical-path-css-rails",
+ "version": "1.0.0",
+ "description": "NPM dependencies of critical-path-css-rails",
+ "private": true,
+ "directories": {
+ "lib": "lib"
+ },
+ "dependencies": {
+ "penthouse": "=0.11.5"
+ },
+ "license": "MIT"
+}
diff --git a/spec/css_fetcher_spec.rb b/spec/css_fetcher_spec.rb
new file mode 100644
index 0000000..0475c9a
--- /dev/null
+++ b/spec/css_fetcher_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CssFetcher' do
+ before :all do
+ StaticFileServer.start
+ end
+
+ after :all do
+ StaticFileServer.stop
+ end
+
+ it 'fetches css' do
+ config = CriticalPathCss::Configuration.new(
+ 'base_url' => StaticFileServer.url,
+ 'css_path' => 'spec/fixtures/static/test.css',
+ 'routes' => ['/test.html']
+ )
+ fetcher = CriticalPathCss::CssFetcher.new(config)
+ expect(fetcher.fetch).to(
+ eq('/test.html' => "p {\n color: red;\n}\n")
+ )
+ end
+end
diff --git a/spec/fixtures/static/test.css b/spec/fixtures/static/test.css
new file mode 100644
index 0000000..3d9a2b2
--- /dev/null
+++ b/spec/fixtures/static/test.css
@@ -0,0 +1,3 @@
+p {
+ color: red;
+}
diff --git a/spec/fixtures/static/test.html b/spec/fixtures/static/test.html
new file mode 100644
index 0000000..a16a45d
--- /dev/null
+++ b/spec/fixtures/static/test.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+ Hello world
+
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..7b07287
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'bundler/setup'
+require 'critical-path-css-rails'
+
+require 'support/static_file_server'
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = '.rspec_status'
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/spec/support/static_file_server.rb b/spec/support/static_file_server.rb
new file mode 100644
index 0000000..345baa9
--- /dev/null
+++ b/spec/support/static_file_server.rb
@@ -0,0 +1,45 @@
+require 'socket'
+
+module StaticFileServer
+ class << self
+ def start # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
+ @port = get_free_port
+ rd, wt = IO.pipe
+ @pid = fork do
+ require 'webrick'
+ rd.close
+ server = WEBrick::HTTPServer.new(
+ DocumentRoot: File.expand_path('spec/fixtures/static'),
+ Port: @port,
+ BindAddress: '127.0.0.1',
+ StartCallback: lambda do
+ # write "1", signal a server start message
+ wt.write(1)
+ wt.close
+ end
+ )
+ trap('INT') { server.shutdown }
+ server.start
+ end
+ wt.close
+ # read a byte for the server start signal
+ rd.read(1)
+ rd.close
+ end
+
+ def stop
+ Process.kill('INT', @pid)
+ end
+
+ def url
+ "http://localhost:#{@port}"
+ end
+
+ def get_free_port
+ server = TCPServer.new('127.0.0.1', 0)
+ port = server.addr[1]
+ server.close
+ port
+ end
+ end
+end