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