rails_critical_css 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +67 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +11 -0
- data/LICENSE +21 -0
- data/README.md +69 -0
- data/Rakefile +3 -0
- data/lib/critical_css_generator/actions/after_render.rb +56 -0
- data/lib/critical_css_generator/actions/before_render.rb +49 -0
- data/lib/critical_css_generator/actions/helpers.rb +57 -0
- data/lib/critical_css_generator/actions.rb +57 -0
- data/lib/critical_css_generator/config.rb +37 -0
- data/lib/critical_css_generator/extractor.rb +92 -0
- data/lib/critical_css_generator/helpers.rb +33 -0
- data/lib/critical_css_generator/jobs/extractor.rb +37 -0
- data/lib/critical_css_generator.rb +20 -0
- data/lib/js/css-extractor.js +20 -0
- data/rails_critical_css.gemspec +14 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd4b7e4c280804bf93b6dd1d53781ded82f99b53ecd91e745eed3720c1c8673a
|
4
|
+
data.tar.gz: 98a1ba570526be8b716cd23ee96e081e598b16a477d8cb5940631dffddf4327c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a38d5c49bf15e78494cf3c19973aa87b3e5f7db39cb6316a74d0d80c97278106f290a4ab6a5b8dfec3effa022489410e6fafedb306bdaef31715cbe18a5fd0f3
|
7
|
+
data.tar.gz: 8a8843bd2855cf7b04a88c44a97d9a08f8c3216bcf72201ca1a69999e65373e45c85187c8dc929a616d85870679723b0492dc98a6eb4ee14ef0688e1510d6191
|
data/.gitignore
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
*.rbc
|
2
|
+
capybara-*.html
|
3
|
+
.rspec
|
4
|
+
/db/*.sqlite3
|
5
|
+
/db/*.sqlite3-journal
|
6
|
+
/db/*.sqlite3-[0-9]*
|
7
|
+
/public/system
|
8
|
+
/coverage/
|
9
|
+
/spec/tmp
|
10
|
+
*.orig
|
11
|
+
rerun.txt
|
12
|
+
pickle-email-*.html
|
13
|
+
|
14
|
+
# Ignore all logfiles and tempfiles.
|
15
|
+
/log/*
|
16
|
+
/tmp/*
|
17
|
+
!/log/.keep
|
18
|
+
!/tmp/.keep
|
19
|
+
|
20
|
+
# TODO Comment out this rule if you are OK with secrets being uploaded to the repo
|
21
|
+
config/initializers/secret_token.rb
|
22
|
+
config/master.key
|
23
|
+
|
24
|
+
# Only include if you have production secrets in this file, which is no longer a Rails default
|
25
|
+
# config/secrets.yml
|
26
|
+
|
27
|
+
# dotenv
|
28
|
+
# TODO Comment out this rule if environment variables can be committed
|
29
|
+
.env
|
30
|
+
|
31
|
+
## Environment normalization:
|
32
|
+
/.bundle
|
33
|
+
/vendor/bundle
|
34
|
+
|
35
|
+
# these should all be checked in to normalize the environment:
|
36
|
+
# Gemfile.lock, .ruby-version, .ruby-gemset
|
37
|
+
|
38
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
39
|
+
.rvmrc
|
40
|
+
|
41
|
+
# if using bower-rails ignore default bower_components path bower.json files
|
42
|
+
/vendor/assets/bower_components
|
43
|
+
*.bowerrc
|
44
|
+
bower.json
|
45
|
+
|
46
|
+
# Ignore pow environment settings
|
47
|
+
.powenv
|
48
|
+
|
49
|
+
# Ignore Byebug command history file.
|
50
|
+
.byebug_history
|
51
|
+
|
52
|
+
# Ignore node_modules
|
53
|
+
node_modules/
|
54
|
+
|
55
|
+
# Ignore precompiled javascript packs
|
56
|
+
/public/packs
|
57
|
+
/public/packs-test
|
58
|
+
/public/assets
|
59
|
+
|
60
|
+
# Ignore yarn files
|
61
|
+
/yarn-error.log
|
62
|
+
yarn-debug.log*
|
63
|
+
.yarn-integrity
|
64
|
+
|
65
|
+
# Ignore uploaded files in development
|
66
|
+
/storage/*
|
67
|
+
!/storage/.keep
|
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
source 'https://rubygems.org'
|
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Mateusz Bagiński
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# rails-critical-css
|
2
|
+
|
3
|
+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
|
4
|
+
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/mati365/rails-critical-css?style=flat-square)
|
5
|
+
![GitHub issues](https://img.shields.io/github/issues/mati365/rails-critical-css?style=flat-square)
|
6
|
+
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
|
7
|
+
|
8
|
+
Generate on demand critical css for component actions with minimum effort.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
```bash
|
13
|
+
gem 'rails_critical_css'
|
14
|
+
```
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
Install `penthouse` NPM package in your project. Be sure that node_modules/ directory is present on production builds.
|
19
|
+
|
20
|
+
In initializer:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
CriticalCssGenerator.config do |c|
|
24
|
+
c.keep_larger_media_queries = true
|
25
|
+
c.height = 19999 # prevent CLS on for example footer
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
In controller:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class ExampleController < ApplicationController
|
33
|
+
action_critical_css :show, cache_key: :critical_css_cache_key
|
34
|
+
# or
|
35
|
+
action_critical_css :show, cache_key: -> { 'cache_key' }
|
36
|
+
# or
|
37
|
+
action_critical_css :show, cache_key: 'cache_key'
|
38
|
+
|
39
|
+
...
|
40
|
+
|
41
|
+
def critical_css_cache_key
|
42
|
+
"some-dynamic-key-#{random_variable}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
In template:
|
48
|
+
|
49
|
+
```slim
|
50
|
+
= critical_css_asset file: 'some-file-to-be-prepended-to-criticals', critical: true
|
51
|
+
= critical_css_tags
|
52
|
+
link rel="stylesheet" href="css/vendors.css" rel='stylesheet' type='text/css'
|
53
|
+
link rel="stylesheet" href="css/app.css" rel='stylesheet' type='text/css'
|
54
|
+
```
|
55
|
+
|
56
|
+
## Testing
|
57
|
+
|
58
|
+
Enable redis caching on localhost and precompile locally assets. Critical CSS are generated using background job so they are injected to HTML after next requests (it takes around 3s)
|
59
|
+
|
60
|
+
## License
|
61
|
+
|
62
|
+
The MIT License (MIT)
|
63
|
+
Copyright (c) 2021 Mateusz Bagiński
|
64
|
+
|
65
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
66
|
+
|
67
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
68
|
+
|
69
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator::Actions
|
4
|
+
class AfterRender
|
5
|
+
include CriticalCssGenerator::Actions::Helpers
|
6
|
+
|
7
|
+
def initialize(filter_options)
|
8
|
+
@packed_options = filter_options.slice(
|
9
|
+
:css, :cache_key, :cache_store, :cache_prefix
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def after(controller)
|
14
|
+
return if controller.critical_css_cache.present?
|
15
|
+
|
16
|
+
@controller = controller
|
17
|
+
options = eval_options(controller, @packed_options)
|
18
|
+
cache_path = gen_critical_css_cache_path(options, options[:cache_key])
|
19
|
+
|
20
|
+
return if cache_path == false
|
21
|
+
|
22
|
+
CriticalCssGenerator::Jobs::Extractor.perform_if_semaphore_is_released(
|
23
|
+
html: controller.full_html_response,
|
24
|
+
cache: {
|
25
|
+
path: cache_path,
|
26
|
+
store: options[:cache_store],
|
27
|
+
},
|
28
|
+
css: {
|
29
|
+
assets: assets_mapped_paths,
|
30
|
+
},
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def assets_mapped_paths
|
37
|
+
items = group_assets_by_type(@controller.critical_css_assets)
|
38
|
+
output = items[:inline] || []
|
39
|
+
|
40
|
+
if items[:files]
|
41
|
+
output += items[:files].map do |i|
|
42
|
+
file = Rails.env.development? ? i[:file] : Rails.application.assets_manifest.assets["#{i[:file]}.css"]
|
43
|
+
|
44
|
+
{
|
45
|
+
critical: i[:critical],
|
46
|
+
file: absolute_asset_file_path(@controller, file, 'css')
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
output
|
52
|
+
rescue
|
53
|
+
[]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator::Actions
|
4
|
+
class BeforeRender
|
5
|
+
include CriticalCssGenerator::Actions::Helpers
|
6
|
+
|
7
|
+
def initialize(filter_options)
|
8
|
+
@packed_options = filter_options.slice(:css, :cache_key, :cache_prefix)
|
9
|
+
end
|
10
|
+
|
11
|
+
def around(controller)
|
12
|
+
@controller = controller
|
13
|
+
|
14
|
+
options = eval_options(controller, @packed_options)
|
15
|
+
cache_path = gen_critical_css_cache_path(options, options[:cache_key])
|
16
|
+
|
17
|
+
# load already compiled css from cache
|
18
|
+
critical_css_cache = cache_path.presence && Rails.cache.read(cache_path)
|
19
|
+
controller.critical_css_cache = critical_css_cache
|
20
|
+
controller.critical_css_enabled = true
|
21
|
+
|
22
|
+
yield
|
23
|
+
return unless css_extracting_allowed?
|
24
|
+
|
25
|
+
if critical_css_cache.present? && critical_css_cache[:lazy_css_blocks].present?
|
26
|
+
controller.response.body = inject_lazy_css_to_footer(critical_css_cache[:lazy_css_blocks])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def css_extracting_allowed?
|
33
|
+
@controller.request.get? \
|
34
|
+
&& @controller.response.status == 200 \
|
35
|
+
&& @controller.full_html_response.present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def inject_lazy_css_to_footer(css)
|
39
|
+
html = @controller.full_html_response
|
40
|
+
lazy_css_injection_index = html.index('</body>')
|
41
|
+
|
42
|
+
[
|
43
|
+
html[0..lazy_css_injection_index - 1],
|
44
|
+
css,
|
45
|
+
html[lazy_css_injection_index..-1],
|
46
|
+
].join('')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator::Actions::Helpers
|
4
|
+
def group_assets_by_type(assets)
|
5
|
+
return [] unless assets.present?
|
6
|
+
|
7
|
+
assets.group_by do |item|
|
8
|
+
item[:file].present? ? :files : :inline
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def eval_options(controller, packed_options)
|
13
|
+
packed_options.transform_values do |option|
|
14
|
+
eval_option(controller, option)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def eval_option(controller, option)
|
19
|
+
option = option.to_proc if option.is_a?(Symbol)
|
20
|
+
|
21
|
+
if option.is_a?(Proc)
|
22
|
+
case option.arity
|
23
|
+
when -2, -1, 1
|
24
|
+
controller.instance_exec(controller, &option)
|
25
|
+
when 0
|
26
|
+
controller.instance_exec(&option)
|
27
|
+
end
|
28
|
+
elsif option.respond_to?(:call)
|
29
|
+
option.call(controller)
|
30
|
+
else
|
31
|
+
option
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def absolute_asset_file_path(controller, asset, extension = nil)
|
36
|
+
return nil unless asset.present?
|
37
|
+
|
38
|
+
suffix = extension ? ".#{extension}" : ''
|
39
|
+
suffixed_asset_name = asset.include?('.') ? asset : asset + suffix
|
40
|
+
|
41
|
+
if Rails.env.development?
|
42
|
+
filename = controller.view_context.asset_path(suffixed_asset_name)
|
43
|
+
File.join(Rails.public_path, filename)
|
44
|
+
else
|
45
|
+
File.join(Rails.public_path, Rails.application.config.assets.prefix, suffixed_asset_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def gen_critical_css_cache_path(options, path)
|
50
|
+
return nil unless path
|
51
|
+
|
52
|
+
controller, action = @controller.request.path_parameters.values_at(:controller, :action)
|
53
|
+
path = eval_option(controller, path)
|
54
|
+
|
55
|
+
"#{options[:cache_prefix]}-#{controller}##{action}#{path}"
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator::Actions
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include CriticalCssGenerator::Helpers
|
8
|
+
|
9
|
+
attr_accessor :lazy_css_blocks,
|
10
|
+
:critical_css_assets,
|
11
|
+
:critical_css_cache,
|
12
|
+
:critical_css_enabled
|
13
|
+
|
14
|
+
helper_method :append_critical_css_asset,
|
15
|
+
:append_css_tags_assets
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract_assets_from_css_tags(str)
|
19
|
+
return [] unless str.present?
|
20
|
+
|
21
|
+
str
|
22
|
+
.scan(/assets\/([^?"]*)-[^?-]+.css/)
|
23
|
+
.flatten
|
24
|
+
.map { |i| i.sub('.self', '') }
|
25
|
+
end
|
26
|
+
|
27
|
+
def append_critical_css_asset(file:, critical: false)
|
28
|
+
(@critical_css_assets ||= []) << {
|
29
|
+
file: file,
|
30
|
+
critical: critical,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def append_css_tags_assets(str)
|
35
|
+
extract_assets_from_css_tags(str).each do |asset|
|
36
|
+
append_critical_css_asset file: asset
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def full_html_response
|
41
|
+
response.body
|
42
|
+
end
|
43
|
+
|
44
|
+
class_methods do
|
45
|
+
def action_critical_css(*actions)
|
46
|
+
options = actions.extract_options!
|
47
|
+
return unless cache_configured?
|
48
|
+
|
49
|
+
options[:unless] ||= -> { request.query_string.present? }
|
50
|
+
options[:cache_prefix] ||= 'critical-css'
|
51
|
+
|
52
|
+
filter_options = options.extract!(:if, :unless).merge(only: actions)
|
53
|
+
around_action CriticalCssGenerator::Actions::BeforeRender.new(options), filter_options
|
54
|
+
after_action CriticalCssGenerator::Actions::AfterRender.new(options), filter_options
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator
|
4
|
+
class Config
|
5
|
+
class << self
|
6
|
+
attr_writer :width, :height, :keep_larger_media_queries,
|
7
|
+
:render_wait_time, :penthouse_options
|
8
|
+
|
9
|
+
def width
|
10
|
+
@width ||= 1200
|
11
|
+
end
|
12
|
+
|
13
|
+
def height
|
14
|
+
@height ||= 900
|
15
|
+
end
|
16
|
+
|
17
|
+
def render_wait_time
|
18
|
+
@render_wait_time ||= 2000
|
19
|
+
end
|
20
|
+
|
21
|
+
def keep_larger_media_queries
|
22
|
+
@keep_larger_media_queries ||= false
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_json_config(props = {})
|
26
|
+
{
|
27
|
+
width: width,
|
28
|
+
height: height,
|
29
|
+
keepLargerMediaQueries: keep_larger_media_queries,
|
30
|
+
renderWaitTime: render_wait_time,
|
31
|
+
}
|
32
|
+
.merge!(@penthouse_options || {})
|
33
|
+
.merge!(props)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator
|
4
|
+
class Extractor
|
5
|
+
include CriticalCssGenerator::Actions::Helpers
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@html, @css = options.values_at(:html, :css)
|
9
|
+
end
|
10
|
+
|
11
|
+
def try_extract
|
12
|
+
tmp_html_file = tmp_extractor_file(@html, extension: 'html')
|
13
|
+
tmp_css_file = tmp_concat_assets_array(@css[:assets], extension: 'css')
|
14
|
+
return nil unless tmp_html_file.present? && tmp_css_file.present?
|
15
|
+
|
16
|
+
stdout, stderr = Open3.capture2e(
|
17
|
+
'node lib/critical_css_generator/js/css-extractor.js',
|
18
|
+
stdin_data: Extractor.extractor_process_input(
|
19
|
+
html_path: tmp_html_file.path,
|
20
|
+
css_path: tmp_css_file.path,
|
21
|
+
)
|
22
|
+
)
|
23
|
+
|
24
|
+
if stderr.try(:success?) && !stdout.try(:include?, 'UnhandledPromiseRejectionWarning')
|
25
|
+
[
|
26
|
+
extract_critical_assets(@css[:assets]),
|
27
|
+
stdout.try(:gsub, /src:[^;]+;/, ''),
|
28
|
+
].join(' ')
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
rescue
|
33
|
+
nil
|
34
|
+
ensure
|
35
|
+
tmp_html_file&.delete
|
36
|
+
tmp_css_file&.delete
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.extractor_process_input(html_path:, css_path:)
|
40
|
+
config = ::CriticalCssGenerator::Config.as_json_config({
|
41
|
+
url: "file://#{html_path}",
|
42
|
+
css: css_path,
|
43
|
+
})
|
44
|
+
|
45
|
+
JSON.generate(config)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def extract_critical_assets(assets)
|
51
|
+
return '' unless assets.present?
|
52
|
+
|
53
|
+
critical_assets = assets.filter { |item| item[:critical] }
|
54
|
+
critical_assets
|
55
|
+
.map { |asset| File.read(asset[:file]) }
|
56
|
+
.compact
|
57
|
+
.join(' ')
|
58
|
+
end
|
59
|
+
|
60
|
+
def tmp_dir
|
61
|
+
File.join(Rails.root, 'tmp')
|
62
|
+
end
|
63
|
+
|
64
|
+
def tmp_extractor_file(content = nil, extension: nil)
|
65
|
+
f = Tempfile.new(['critical-css-tmp', ".#{extension || 'bin'}"], tmp_dir, encoding: 'utf-8')
|
66
|
+
if content.present?
|
67
|
+
f.write(content)
|
68
|
+
f.rewind
|
69
|
+
end
|
70
|
+
|
71
|
+
f.close
|
72
|
+
f
|
73
|
+
end
|
74
|
+
|
75
|
+
def tmp_concat_assets_array(assets, extension: nil)
|
76
|
+
return nil unless assets.present?
|
77
|
+
|
78
|
+
items = group_assets_by_type(assets)
|
79
|
+
file = tmp_extractor_file(
|
80
|
+
(items[:inline] || []).join('\n'),
|
81
|
+
extension: extension
|
82
|
+
)
|
83
|
+
|
84
|
+
paths = (items[:files] || []).map { |asset| asset[:file] }.compact
|
85
|
+
unless paths.empty?
|
86
|
+
Open3.pipeline_w(['cat', *paths], out: file.path)
|
87
|
+
end
|
88
|
+
|
89
|
+
file
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator::Helpers
|
4
|
+
def critical_css
|
5
|
+
@critical_css_cache.try(:[], :css)
|
6
|
+
end
|
7
|
+
|
8
|
+
def critical_css_enabled?
|
9
|
+
@critical_css_enabled
|
10
|
+
end
|
11
|
+
|
12
|
+
def critical_css?
|
13
|
+
critical_css.present? && critical_css.instance_of?(String)
|
14
|
+
end
|
15
|
+
|
16
|
+
def critical_css_asset(attrs)
|
17
|
+
append_critical_css_asset(attrs)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def critical_css_tags(preserve_content: true, &block)
|
22
|
+
content = capture(&block)
|
23
|
+
|
24
|
+
if critical_css_enabled? && critical_css?
|
25
|
+
css_tags = content_tag(:style, critical_css.html_safe, type: 'text/css')
|
26
|
+
css_tags = "#{css_tags}#{content}".html_safe if preserve_content
|
27
|
+
css_tags.html_safe
|
28
|
+
else
|
29
|
+
append_css_tags_assets(content)
|
30
|
+
content
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CriticalCssGenerator::Jobs
|
4
|
+
class Extractor < ActiveJob::Base
|
5
|
+
class << self
|
6
|
+
def semaphore_key(cache_path)
|
7
|
+
"semaphore-#{cache_path}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform_if_semaphore_is_released(attrs)
|
11
|
+
semaphore = semaphore_key(attrs[:cache][:path])
|
12
|
+
return if Rails.cache.exist?(semaphore)
|
13
|
+
|
14
|
+
Rails.cache.write(semaphore, '1', { expires_in: 15.minutes })
|
15
|
+
perform_later(attrs)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def perform(cache:, css:, html:)
|
20
|
+
semaphore = self.class.semaphore_key(cache[:path])
|
21
|
+
critical_css = ::CriticalCssGenerator::Extractor.new(css: css, html: html).try_extract
|
22
|
+
|
23
|
+
# store it as wrapped css, do not regenerate for
|
24
|
+
# each request critical_css if something go wrong
|
25
|
+
# with css-extractor
|
26
|
+
Rails.cache.write(
|
27
|
+
cache[:path],
|
28
|
+
{
|
29
|
+
css: critical_css,
|
30
|
+
},
|
31
|
+
cache[:store]
|
32
|
+
)
|
33
|
+
|
34
|
+
Rails.cache.delete(semaphore)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'critical_css_generator/config'
|
5
|
+
require 'critical_css_generator/actions'
|
6
|
+
require 'critical_css_generator/actions/helpers'
|
7
|
+
require 'critical_css_generator/extractor'
|
8
|
+
require 'critical_css_generator/jobs/extractor'
|
9
|
+
require 'critical_css_generator/actions/after_render'
|
10
|
+
require 'critical_css_generator/actions/before_render'
|
11
|
+
require 'critical_css_generator/helpers'
|
12
|
+
|
13
|
+
module CriticalCssGenerator
|
14
|
+
def self.config
|
15
|
+
yield CriticalCssGenerator::Config
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActionController::Base.send(:include, CriticalCssGenerator::Actions)
|
20
|
+
ActionView::Base.send(:include, CriticalCssGenerator::Helpers)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
const penthouse = require('penthouse');
|
2
|
+
const process = require('process');
|
3
|
+
const fs = require('fs');
|
4
|
+
|
5
|
+
const FD = {
|
6
|
+
STDIN: 0,
|
7
|
+
STDOUT: 1,
|
8
|
+
STDERR: 2,
|
9
|
+
};
|
10
|
+
|
11
|
+
const config = JSON.parse(fs.readFileSync(FD.STDIN, 'utf-8'));
|
12
|
+
|
13
|
+
penthouse(config)
|
14
|
+
.then((criticalCss) => {
|
15
|
+
fs.writeSync(FD.STDOUT, criticalCss);
|
16
|
+
})
|
17
|
+
.catch((err) => {
|
18
|
+
fs.writeSync(FD.STDERR, err);
|
19
|
+
process.exit(1);
|
20
|
+
})
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'rails_critical_css'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.summary = 'Critical CSS rails generator'
|
5
|
+
s.authors = ['Mateusz Bagiński']
|
6
|
+
|
7
|
+
s.license = 'MIT'
|
8
|
+
s.email = 'cziken58@gmail.com'
|
9
|
+
s.homepage = 'https://github.com/Mati365/rails-critical-css'
|
10
|
+
s.files = `git ls-files`.split($/)
|
11
|
+
|
12
|
+
s.require_paths << 'lib'
|
13
|
+
s.required_ruby_version = '>= 2.4.0'
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_critical_css
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mateusz Bagiński
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-31 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: cziken58@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- ".gitignore"
|
20
|
+
- Gemfile
|
21
|
+
- Gemfile.lock
|
22
|
+
- LICENSE
|
23
|
+
- README.md
|
24
|
+
- Rakefile
|
25
|
+
- lib/critical_css_generator.rb
|
26
|
+
- lib/critical_css_generator/actions.rb
|
27
|
+
- lib/critical_css_generator/actions/after_render.rb
|
28
|
+
- lib/critical_css_generator/actions/before_render.rb
|
29
|
+
- lib/critical_css_generator/actions/helpers.rb
|
30
|
+
- lib/critical_css_generator/config.rb
|
31
|
+
- lib/critical_css_generator/extractor.rb
|
32
|
+
- lib/critical_css_generator/helpers.rb
|
33
|
+
- lib/critical_css_generator/jobs/extractor.rb
|
34
|
+
- lib/js/css-extractor.js
|
35
|
+
- rails_critical_css.gemspec
|
36
|
+
homepage: https://github.com/Mati365/rails-critical-css
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 2.4.0
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.2.5
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Critical CSS rails generator
|
60
|
+
test_files: []
|