jekyll-simple-assets 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95843e3923fd2624c717ab799fe08f4497bd7c7d55febc86b5dd3bbf7ec9d6e7
4
- data.tar.gz: 0e93ea72b7a5b06521ec61a7a8264120d1221c0e98a5a86ed426340c188f431f
3
+ metadata.gz: f9a88ce587480260a2b882650c270bdc2109b2e85fb0171259b19134d8860c7e
4
+ data.tar.gz: 89d8af9b0ccc245da99c45e43369cfb636e978d862ad80a7bf01a595f2df75f6
5
5
  SHA512:
6
- metadata.gz: 4307d2592ab2f9885305b2fe23b8fdcdfe86f9e2d2edc41212ba7f1de32b756b054d7fc20273666b64120aac11bdc9256abba9d0d0372a32fa10966d0d798182
7
- data.tar.gz: ebcd8ed79fb7d17969a669928fa2e90c957c7f83dbccd6942676d952cb33e00596f483a37cef191e9c1624170511af235ad1888d3a53afb2ce8b3ff64e1794ea
6
+ metadata.gz: d4104483ef4f80d627cab01fbc07799aae26849d4015ec03671b4cd74ed09728d39f7f2ffdcef339c2fc5a7ace21933b8218c200a34750216ac40234f3e7bb69
7
+ data.tar.gz: 144171dcfee48d6800d2796ed8eb14772ea82873914e5b53f616a206bfe1af28b73c6c2ba5b265edaae9de24931877f5f0a69cfd2ee5d6fce50939186a72f4fb
data/Gemfile.lock ADDED
@@ -0,0 +1,81 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jekyll-simple-assets (0.6.0)
5
+ css_parser
6
+ jekyll
7
+ terser
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ addressable (2.7.0)
13
+ public_suffix (>= 2.0.2, < 5.0)
14
+ colorator (1.1.0)
15
+ concurrent-ruby (1.1.8)
16
+ css_parser (1.9.0)
17
+ addressable
18
+ em-websocket (0.5.2)
19
+ eventmachine (>= 0.12.9)
20
+ http_parser.rb (~> 0.6.0)
21
+ eventmachine (1.2.7)
22
+ execjs (2.7.0)
23
+ ffi (1.15.0)
24
+ forwardable-extended (2.6.0)
25
+ http_parser.rb (0.6.0)
26
+ i18n (1.8.9)
27
+ concurrent-ruby (~> 1.0)
28
+ jekyll (4.2.0)
29
+ addressable (~> 2.4)
30
+ colorator (~> 1.0)
31
+ em-websocket (~> 0.5)
32
+ i18n (~> 1.0)
33
+ jekyll-sass-converter (~> 2.0)
34
+ jekyll-watch (~> 2.0)
35
+ kramdown (~> 2.3)
36
+ kramdown-parser-gfm (~> 1.0)
37
+ liquid (~> 4.0)
38
+ mercenary (~> 0.4.0)
39
+ pathutil (~> 0.9)
40
+ rouge (~> 3.0)
41
+ safe_yaml (~> 1.0)
42
+ terminal-table (~> 2.0)
43
+ jekyll-sass-converter (2.1.0)
44
+ sassc (> 2.0.1, < 3.0)
45
+ jekyll-watch (2.2.1)
46
+ listen (~> 3.0)
47
+ kramdown (2.3.1)
48
+ rexml
49
+ kramdown-parser-gfm (1.1.0)
50
+ kramdown (~> 2.0)
51
+ liquid (4.0.3)
52
+ listen (3.5.0)
53
+ rb-fsevent (~> 0.10, >= 0.10.3)
54
+ rb-inotify (~> 0.9, >= 0.9.10)
55
+ mercenary (0.4.0)
56
+ pathutil (0.16.2)
57
+ forwardable-extended (~> 2.6)
58
+ public_suffix (4.0.6)
59
+ rb-fsevent (0.10.4)
60
+ rb-inotify (0.10.1)
61
+ ffi (~> 1.0)
62
+ rexml (3.2.4)
63
+ rouge (3.26.0)
64
+ safe_yaml (1.0.5)
65
+ sassc (2.4.0)
66
+ ffi (~> 1.9)
67
+ terminal-table (2.0.0)
68
+ unicode-display_width (~> 1.1, >= 1.1.1)
69
+ terser (1.1.3)
70
+ execjs (>= 0.3.0, < 3)
71
+ unicode-display_width (1.7.0)
72
+
73
+ PLATFORMS
74
+ ruby
75
+
76
+ DEPENDENCIES
77
+ bundler
78
+ jekyll-simple-assets!
79
+
80
+ BUNDLED WITH
81
+ 2.1.4
data/README.md CHANGED
@@ -12,21 +12,22 @@ webpack.
12
12
 
13
13
  #### contenthash
14
14
 
15
- Returns an (md5) hash based on the contents of the path given.
15
+ Returns a base64 encoded md5 hash based on the contents of the path given.
16
16
 
17
17
  ```liquid
18
18
  {% contenthash assets/js/app.js %}
19
- // 329CF90732DF0C89EF8FB0B7E9416FE3
19
+ // Mpz5BzLfDInvj7C36UFv4w==
20
20
  ```
21
21
 
22
22
  #### asset
23
23
 
24
24
  Returns a relative url to the path given, with a hash based on the content of
25
25
  the file as a query string.
26
+ Will link to a minified version of the file if possible.
26
27
 
27
28
  ```liquid
28
29
  {% asset assets/js/app.js %}
29
- // /assets/js/app.js?v=329CF90732DF0C89EF8FB0B7E9416FE3
30
+ // /assets/js/app.js?v=Mpz5BzLfDInvj7C36UFv4w==
30
31
  ```
31
32
 
32
33
  #### bundle
@@ -58,9 +59,120 @@ Returns an md5 hash of the input string.
58
59
  {{ 'some text' | md5 }}
59
60
  ```
60
61
 
62
+ #### uglify
63
+
64
+ Minifies javascript given as an input string (uses terser, and terser settings
65
+ even though the filter is called `uglify`
66
+
67
+ ```liquid
68
+ {{ 'const foo = "bar"' | uglify }}
69
+ ```
70
+
61
71
  ## Content hashes
62
72
 
63
73
  How the content hashes work is by generating a placeholder string that is
64
74
  passed to the template. Once all of the sites files and pages have been
65
75
  processed and copied over, the content hashes are worked out, and the
66
76
  placeholder string in the pages output is replaced with the hash.
77
+
78
+ Because of this you need to be careful with using capture tags around or trying
79
+ to manipulate the output of the contenthash or asset tags.
80
+
81
+ By default the generation of content hashes is only enabled for production
82
+ builds (if JEKYLL_ENV is set to 'production').
83
+
84
+ ## Critical CSS
85
+
86
+ This plugin can also be used to generate critical css stylesheets. See
87
+ configuration for more information on doing this. To generate critical css,
88
+ [critical](https://github.com/addyosmani/critical) must be installed.
89
+
90
+ ## Javascript/Typescript bundling
91
+
92
+ This plugin can also be used to bundle javascript and typescript files and
93
+ modules using esbuild. Node modules can be included just by requiring them.
94
+ Local modules can be required under the `@simple-assets` namespace, which
95
+ will look for js files in the `_js` and `_ts` directories.
96
+
97
+ For example:
98
+ ```javascript
99
+ // assets/js/main.js
100
+
101
+ // require node_module
102
+ var leftpad = require('left-pad');
103
+
104
+ // will look for left-pad.js or left-pad.ts at _js/ or _ts/ in the
105
+ // project root
106
+ var rightpad = require('@simple-assets/right-pad');
107
+ ```
108
+
109
+ You can also put variables in the liquid frontmatter of a js/ts file (that is
110
+ not under `_js` or `_ts`) to configure esbuild.
111
+
112
+ ```liquid
113
+ ---
114
+ bundle: false # don't bundle or run esbuild on this file
115
+ esbuild_flags: --target=es5 # pass any flags to esbuild for this file
116
+ ---
117
+ ```
118
+
119
+ To bundle javascript [esbuild](https://github.com/evanw/esbuild) must be
120
+ installed.
121
+
122
+ ## Javascript minification
123
+
124
+ The terser package is also included and will minify files if `terser_enabled`
125
+ is set to true in config.yml, or if JEKYLL_ENV is set to production. Any
126
+ options can be passed to terser under the `terser` key in config.yml
127
+ (see configuration for example, or
128
+ [here](https://github.com/ahorek/terser-ruby) for detailed options).
129
+ Additionally you can set terser settings in the liquid frontmatter of a js file
130
+ in the same manner as in config.yml, also under the `terser` key.
131
+
132
+ ## Configuration
133
+
134
+ ```
135
+ simple_assets:
136
+ # If set to true generation of content hashes will be enabled, even in a non
137
+ # production build.
138
+ # default: false
139
+ hashing_enabled: true
140
+
141
+ # If set to true, source maps will be generated for javascript
142
+ # default: false
143
+ source_maps_enabled: true
144
+
145
+ # The length of the content hashes generated.
146
+ # default: 16
147
+ hash_length: 8
148
+
149
+ # Options for generating a critical css file using the `critical` npm module
150
+ critical_css:
151
+
152
+ # Array of source css files used to take the critical css from
153
+ css_files:
154
+ - assets/css/style.css
155
+
156
+ # Array of critical css files to generate
157
+ files:
158
+ # The path of the critical css file output
159
+ - output_file: assets/css/critical.css
160
+ # The path of the input page used to generate the critical css
161
+ # either this option or layout can be used
162
+ input_page_path: _drafts/webmentions-static-site.md
163
+ # The layout to use to generate critical css
164
+ # (will use the first page found with this layout)
165
+ layout: post
166
+ # If the rules should be removed from the original source css files
167
+ extract: true
168
+
169
+ # Options for bundling javascript/typescript with the `esbuild` npm module
170
+ # Set to true to enable
171
+ bundle: true
172
+
173
+ # Options for javascript minification with terser
174
+ terser:
175
+ # any options for terser can be put here and will be passed to it
176
+ output:
177
+ ascii_only: true
178
+ ```
@@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
19
19
  spec.required_ruby_version = ">= 2.3.0"
20
20
 
21
21
  spec.add_runtime_dependency "jekyll"
22
+ spec.add_runtime_dependency "css_parser"
23
+ spec.add_runtime_dependency "terser"
22
24
 
23
25
  spec.add_development_dependency "bundler"
24
26
  end
@@ -2,9 +2,50 @@
2
2
 
3
3
  require 'digest'
4
4
  require 'pathname'
5
+ require 'open3'
6
+ require 'shellwords'
7
+
8
+ require 'jekyll-simple-assets/content-hash'
9
+ require 'jekyll-simple-assets/critical'
10
+ require 'jekyll-simple-assets/esbuild'
11
+ require 'jekyll-simple-assets/terser'
5
12
 
6
13
  module Jekyll
7
14
  module SimpleAssets
15
+ def self.site (site = nil)
16
+ @@site = site if site
17
+
18
+ @@site
19
+ end
20
+
21
+ def self.config
22
+ @@config ||= @@site.config['simple_assets']
23
+ end
24
+
25
+ def self.hashing_enabled?
26
+ config['hashing_enabled'] || ENV['JEKYLL_ENV'] == 'production'
27
+ end
28
+
29
+ def self.critical_css_enabled?
30
+ config.key?('critical_css')
31
+ end
32
+
33
+ def self.esbuild_enabled?
34
+ config.key?('bundle') && config['bundle'] != false
35
+ end
36
+
37
+ def self.esbuild_config_file (file = nil)
38
+ @@esbuild_config_file ||= file
39
+ end
40
+
41
+ def self.terser_enabled?
42
+ config['terser_enabled'] || ENV['JEKYLL_ENV'] == 'production'
43
+ end
44
+
45
+ def self.source_maps_enabled?
46
+ config['source_maps_enabled']
47
+ end
48
+
8
49
  module SimpleAssetsFilters
9
50
  def md5 (input)
10
51
  Digest::MD5.hexdigest(input)
@@ -13,13 +54,13 @@ module Jekyll
13
54
 
14
55
  class BundleTag < Jekyll::Tags::IncludeTag
15
56
  def tag_includes_dirs(context)
16
- [ "_js", "_assets" ].freeze
57
+ [ "_js", "_javascript", "_assets" ].freeze
17
58
  end
18
59
  end
19
60
 
20
61
  class BundleRawTag < Jekyll::Tags::IncludeTag
21
62
  def tag_includes_dirs(context)
22
- [ "_js", "_assets" ].freeze
63
+ [ "_js", "_javascript", "_assets" ].freeze
23
64
  end
24
65
 
25
66
  def render(context)
@@ -35,198 +76,105 @@ module Jekyll
35
76
 
36
77
  return unless File.file? path
37
78
 
38
- begin
39
- content = File.read path
40
- end
79
+ content = File.read path
41
80
 
42
81
  content
43
82
  end
44
83
  end
84
+ end
85
+ end
45
86
 
46
- def self.page_assets_map
47
- @@page_assets_map ||= {}
48
- end
49
-
50
- def self.asset_placeholder_map
51
- @@asset_placeholder_map ||= {}
52
- end
53
-
54
- def self.asset_contenthash_map
55
- @@asset_contenthash_map ||= {}
56
- end
57
-
58
- def self.get_placeholder (asset_path)
59
- asset_placeholder_map[asset_path] ||= Digest::MD5.hexdigest(asset_path)
60
- end
61
-
62
- def self.relative_url (site, path)
63
- "#{ site.config['baseurl'] }/#{ path }".gsub(%r{/{2,}}, '/')
64
- end
65
-
66
- class AssetTag < Liquid::Tag
67
- def initialize (tag_name, text, tokens)
68
- super
69
- @text = text
70
- end
71
-
72
- def get_value (context, expression)
73
- result = nil
74
-
75
- unless expression.empty?
76
- lookup_path = expression.split('.')
77
- result = context
78
- lookup_path.each do |variable|
79
- result = result[variable] if result
80
- end
81
- end
82
-
83
- case result
84
- when 'true'
85
- result = true
86
- when 'false'
87
- result = false
88
- end
89
-
90
- result || expression
91
- end
92
-
93
- def render (context)
94
- site = context.registers[:site]
95
- page = context.environments.first['page']
96
-
97
- args = Shellwords.split(@text)
87
+ Liquid::Template.register_tag('bundle', Jekyll::SimpleAssets::BundleTag)
88
+ Liquid::Template.register_tag('bundle_raw', Jekyll::SimpleAssets::BundleRawTag)
98
89
 
99
- page_path = context['page']['path'].sub(/^\//, '')
90
+ Liquid::Template.register_filter(Jekyll::SimpleAssets::SimpleAssetsFilters)
100
91
 
101
- asset_path = get_value(context, args[0]).sub(/^\//, '')
92
+ Jekyll::Hooks.register :site, :post_render, priority: :low do |site, payload|
93
+ Jekyll::SimpleAssets::site(site)
102
94
 
103
- if ENV['JEKYLL_ENV'] == 'production'
104
- SimpleAssets::page_assets_map[page_path] ||= {}
105
- SimpleAssets::page_assets_map[page_path][asset_path] ||= {}
106
- SimpleAssets::page_assets_map[page_path][asset_path][@type] ||= []
95
+ potential_assets = []
107
96
 
108
- placeholder = SimpleAssets::get_placeholder(asset_path)
97
+ potential_assets += site.pages
98
+ potential_assets += site.static_files
109
99
 
110
- SimpleAssets::page_assets_map[page_path][asset_path][@type] << placeholder
100
+ potential_pages = potential_assets
111
101
 
112
- "#{ @type }::#{ placeholder }"
113
- else
114
- SimpleAssets::relative_url(site, asset_path)
115
- end
116
- end
117
- end
102
+ site.collections.each do |collection_name, collection|
103
+ potential_pages = potential_pages + collection.docs
104
+ end
118
105
 
119
- class PathTag < AssetTag
120
- def initialize (tag_name, text, tokens)
121
- super
122
- @type = 'path'
123
- end
106
+ if Jekyll::SimpleAssets::critical_css_enabled?
107
+ potential_assets.each do |asset|
108
+ Jekyll::SimpleAssets::make_temp_css_files_for_critical(asset)
124
109
  end
125
110
 
126
- class ContentHashTag < AssetTag
127
- def initialize (tag_name, text, tokens)
128
- super
129
- @type = 'contenthash'
130
- end
111
+ potential_pages.each do |doc|
112
+ Jekyll::SimpleAssets::get_html_input_for_critical(doc, site)
131
113
  end
132
114
 
133
- def self.resolve_asset_content_hashes (asset, site)
134
- asset_path = asset.path.sub("#{ site.config['source'] }/", '')
135
-
136
- if File.extname(asset_path) == '.scss'
137
- asset_path = Pathname.new(asset_path).sub_ext('.css').to_path()
138
- elsif File.extname(asset_path) == '.ts'
139
- asset_path = Pathname.new(asset_path).sub_ext('.js').to_path()
140
- end
141
-
142
- return unless SimpleAssets::asset_placeholder_map[asset_path]
143
- return if SimpleAssets::asset_contenthash_map[asset_path]
144
-
145
- content = ""
146
-
147
- # Prefer reading from output if available, because in theory should
148
- # be faster than from disk, but fall back to reading from disk for
149
- # static assets.
150
- if asset.respond_to? :output
151
- content = asset.output
152
- elsif File.file? asset_path
153
- content = File.read asset_path
154
- elsif File.file? asset.path
155
- content = File.read asset.path
156
- else
157
- Jekyll.logger.warn "SimpleAssets", "File: #{ asset_path } not found"
158
- end
115
+ Jekyll::SimpleAssets::generate_critical_css(site)
116
+ end
159
117
 
160
- if content.nil?
161
- Jekyll.logger.warn "SimpleAssets", "#{ asset_path } has no content"
162
- end
118
+ if Jekyll::SimpleAssets::hashing_enabled?
119
+ potential_assets.each do |asset|
120
+ Jekyll::SimpleAssets::resolve_asset_content_hashes(asset, site)
121
+ end
163
122
 
164
- SimpleAssets::asset_contenthash_map[asset_path] = Digest::MD5.hexdigest(content)
123
+ if Jekyll::SimpleAssets::critical_css_enabled?
124
+ Jekyll::SimpleAssets::resolve_critical_css_content_hashes(site)
165
125
  end
166
126
 
167
- def self.replace_placeholders_for_asset (doc, site)
127
+ potential_pages.each do |doc|
168
128
  page_path = doc.path.sub("#{ site.config['source'] }/", '')
169
129
 
170
- return unless SimpleAssets::page_assets_map[page_path]
171
-
172
- SimpleAssets::page_assets_map[page_path].each do |asset_path, types|
173
- types.each do |type, placeholders|
174
- placeholders.each do |placeholder_hash|
175
- unless SimpleAssets::asset_contenthash_map[asset_path]
176
- Jekyll.logger.warn "SimpleAssets", "No contenthash for: #{ asset_path } not found"
177
- end
178
-
179
- replacement = SimpleAssets::asset_contenthash_map[asset_path]
180
-
181
- if type == 'path'
182
- replacement = "#{ asset_path }?v=#{ replacement }"
183
-
184
- replacement = SimpleAssets::relative_url(site, replacement)
185
- end
130
+ if Jekyll::SimpleAssets::page_assets_map[page_path]
131
+ doc.output = Jekyll::SimpleAssets::replace_placeholders_for_path(page_path, doc.output)
132
+ end
186
133
 
187
- placeholder = "#{ type }::#{ SimpleAssets::asset_placeholder_map[asset_path] }"
134
+ if doc.extname =~ /^\.(j|t)s$/i and Jekyll::SimpleAssets::should_minify_file?(doc)
135
+ min_path, minified = Jekyll::SimpleAssets::minify_file(doc)
188
136
 
189
- if placeholders.size > 1
190
- doc.output = doc.output.gsub(placeholder, replacement)
191
- else
192
- doc.output = doc.output.sub(placeholder, replacement)
193
- end
194
- end
137
+ if min_path and Jekyll::SimpleAssets::page_assets_map[page_path]
138
+ minified = Jekyll::SimpleAssets::replace_placeholders_for_path(page_path, minified)
195
139
  end
140
+
141
+ File.write(File.join(Jekyll::SimpleAssets::site.config['destination'], min_path), minified)
196
142
  end
197
143
  end
198
144
  end
199
145
  end
200
146
 
201
- Liquid::Template.register_tag('asset', Jekyll::SimpleAssets::PathTag)
202
- Liquid::Template.register_tag('contenthash', Jekyll::SimpleAssets::ContentHashTag)
203
- Liquid::Template.register_tag('bundle', Jekyll::SimpleAssets::BundleTag)
204
- Liquid::Template.register_tag('bundle_raw', Jekyll::SimpleAssets::BundleRawTag)
205
-
206
- Liquid::Template.register_filter(Jekyll::SimpleAssets::SimpleAssetsFilters)
207
-
208
- if ENV['JEKYLL_ENV'] == 'production'
209
- Jekyll::Hooks.register :site, :post_render do |site, payload|
210
- potential_assets = []
211
-
212
- potential_assets += site.pages
213
- potential_assets += site.static_files
147
+ Jekyll::Hooks.register :pages, :post_render do |page, payload|
148
+ unless Jekyll::SimpleAssets::esbuild_config_file
149
+ Jekyll::SimpleAssets::generate_esbuild_config_file()
150
+ end
214
151
 
215
- potential_assets.each do |asset|
216
- Jekyll::SimpleAssets::resolve_asset_content_hashes(asset, site)
152
+ if page.extname =~ /^\.(j|t)s$/i
153
+ if Jekyll::SimpleAssets::esbuild_enabled?
154
+ Jekyll::SimpleAssets::esbuild_bundle_file(page, payload, Jekyll::SimpleAssets::esbuild_config_file.path)
217
155
  end
218
156
 
219
- docs = []
220
-
221
- site.pages.each do |doc|
222
- Jekyll::SimpleAssets::replace_placeholders_for_asset(doc, site)
157
+ if Jekyll::SimpleAssets::should_minify_file?(page)
158
+ min_path = page.path.sub(/\.(j|t)s$/i, '.min.js')
159
+ File.write(File.join(Jekyll::SimpleAssets::site.config['destination'], min_path), '')
223
160
  end
161
+ end
162
+ end
224
163
 
225
- site.collections.each do |collection_name, collection|
226
- collection.docs.each do |doc|
227
- Jekyll::SimpleAssets::replace_placeholders_for_asset(doc, site)
228
- end
164
+ Jekyll::Hooks.register :site, :post_read do |site|
165
+ css_pages = [];
166
+
167
+ site.pages.each do |doc|
168
+ if doc.extname == '.scss'
169
+ css_pages << doc
170
+ site.pages = site.pages - [ doc ]
229
171
  end
230
172
  end
173
+
174
+ site.pages = css_pages + site.pages
231
175
  end
232
176
 
177
+ # Jekyll::Hooks.register :pages, :post_render do |document|
178
+ # puts 'rendered:' + document.path
179
+ # end
180
+
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module SimpleAssets
5
+
6
+
7
+ def self.hash_length
8
+ config['hash_length'] || 16
9
+ end
10
+
11
+ def self.page_assets_map
12
+ @@page_assets_map ||= {}
13
+ end
14
+
15
+ def self.asset_placeholder_map
16
+ @@asset_placeholder_map ||= {}
17
+ end
18
+
19
+ def self.asset_contenthash_map
20
+ @@asset_contenthash_map ||= {}
21
+ end
22
+
23
+ def self.get_placeholder (asset_path)
24
+ asset_placeholder_map[asset_path] ||= Digest::MD5.base64digest(asset_path)
25
+ end
26
+
27
+ def self.relative_url (path)
28
+ "#{ @@site.config['baseurl'] || '' }/#{ path }".gsub(%r{/{2,}}, '/')
29
+ end
30
+
31
+ class AssetTag < Liquid::Tag
32
+ def initialize (tag_name, text, tokens)
33
+ super
34
+ @text = text
35
+ end
36
+
37
+ def get_value (context, expression)
38
+ result = nil
39
+
40
+ unless expression.empty?
41
+ lookup_path = expression.split('.')
42
+ result = context
43
+ lookup_path.each do |variable|
44
+ result = result[variable] if result
45
+ end
46
+ end
47
+
48
+ case result
49
+ when 'true'
50
+ result = true
51
+ when 'false'
52
+ result = false
53
+ end
54
+
55
+ result || expression
56
+ end
57
+
58
+ def render (context)
59
+ site = SimpleAssets::site(context.registers[:site])
60
+
61
+ page = context.environments.first['page']
62
+
63
+ args = Shellwords.split(@text)
64
+
65
+ page_path = context['page']['path'].sub(/^\//, '')
66
+
67
+ asset_path = get_value(context, args[0]).sub(/^\//, '')
68
+
69
+ if SimpleAssets::hashing_enabled?
70
+ SimpleAssets::page_assets_map[page_path] ||= {}
71
+ SimpleAssets::page_assets_map[page_path][asset_path] ||= {}
72
+ SimpleAssets::page_assets_map[page_path][asset_path][@type] ||= []
73
+
74
+ placeholder = SimpleAssets::get_placeholder(asset_path)
75
+
76
+ unless SimpleAssets::page_assets_map[page_path][asset_path][@type].include? placeholder
77
+ SimpleAssets::page_assets_map[page_path][asset_path][@type] << placeholder
78
+ end
79
+
80
+ "#{ @type }::#{ placeholder }"
81
+ else
82
+ if @type == 'path'
83
+ SimpleAssets::relative_url(asset_path)
84
+ else
85
+ placeholder[0, SimpleAssets::hash_length]
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ class PathTag < AssetTag
92
+ def initialize (tag_name, text, tokens)
93
+ super
94
+ @type = 'path'
95
+ end
96
+ end
97
+
98
+ class ContentHashTag < AssetTag
99
+ def initialize (tag_name, text, tokens)
100
+ super
101
+ @type = 'contenthash'
102
+ end
103
+ end
104
+
105
+ def self.resolve_asset_content_hashes (asset, site)
106
+ asset_path = asset.path.sub("#{ site.config['source'] }/", '')
107
+
108
+ if File.extname(asset_path) == '.scss'
109
+ asset_path = Pathname.new(asset_path).sub_ext('.css').to_path()
110
+ elsif File.extname(asset_path) == '.ts'
111
+ asset_path = Pathname.new(asset_path).sub_ext('.js').to_path()
112
+ end
113
+
114
+ return unless SimpleAssets::asset_placeholder_map[asset_path]
115
+ return if SimpleAssets::asset_contenthash_map[asset_path]
116
+
117
+ content = ""
118
+
119
+ # Prefer reading from output if available, because in theory should
120
+ # be faster than from disk, but fall back to reading from disk for
121
+ # static assets.
122
+ if asset.respond_to? :output
123
+ content = asset.output
124
+ elsif File.file? asset_path
125
+ content = File.read asset_path
126
+ elsif File.file? asset.path
127
+ content = File.read asset.path
128
+ else
129
+ Jekyll.logger.warn "SimpleAssets:", "File: #{ asset_path } not found"
130
+ end
131
+
132
+ if content.nil?
133
+ Jekyll.logger.warn "SimpleAssets:", "#{ asset_path } has no content"
134
+ end
135
+
136
+ base64hash = Digest::MD5.base64digest(content)
137
+
138
+ hash = base64hash[0, SimpleAssets::hash_length].gsub(/[+\/]/, '_')
139
+
140
+ SimpleAssets::asset_contenthash_map[asset_path] = hash
141
+ end
142
+
143
+ def self.replace_placeholders_for_path (page_path, input)
144
+ output = input
145
+
146
+ SimpleAssets::page_assets_map[page_path].each do |asset_path, types|
147
+ types.each do |type, placeholders|
148
+ placeholders.each do |placeholder_hash|
149
+ unless SimpleAssets::asset_contenthash_map[asset_path]
150
+ Jekyll.logger.warn "SimpleAssets:", "No contenthash for: #{ asset_path } not found"
151
+
152
+ next
153
+ end
154
+
155
+ replacement = SimpleAssets::asset_contenthash_map[asset_path]
156
+
157
+ if type == 'path'
158
+ min_path = asset_path.sub(/\.([^\.]*?)$/, '.min.\1')
159
+ url = (File.file? File.join(SimpleAssets::site.config['destination'], min_path)) ? min_path : asset_path
160
+
161
+ replacement = "#{ url }?v=#{ replacement }"
162
+
163
+ replacement = SimpleAssets::relative_url(replacement)
164
+ end
165
+
166
+ placeholder = "#{ type }::#{ SimpleAssets::asset_placeholder_map[asset_path] }"
167
+
168
+ output = output.gsub(placeholder, replacement)
169
+ end
170
+ end
171
+ end
172
+
173
+ output
174
+ end
175
+
176
+
177
+ end
178
+ end
179
+
180
+ Liquid::Template.register_tag('asset', Jekyll::SimpleAssets::PathTag)
181
+ Liquid::Template.register_tag('contenthash', Jekyll::SimpleAssets::ContentHashTag)
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'css_parser'
4
+ require 'tempfile'
5
+
6
+ module Jekyll
7
+ module SimpleAssets
8
+
9
+
10
+ def self.critical_css_source_files ()
11
+ @@critical_css_source_files ||= {}
12
+ end
13
+
14
+ def self.make_temp_css_files_for_critical (asset)
15
+ SimpleAssets::config['critical_css']['css_files'].each do |path|
16
+ next unless asset.path == path || asset.path == path.sub(/\.css$/, '.scss')
17
+
18
+ f = Tempfile.new([ 'css-source', '.css' ])
19
+ f.write asset.output
20
+ f.close
21
+
22
+ Jekyll.logger.debug("SimpleAssets:", "Created new temp file for css: #{ asset.path } at: #{ f.path }")
23
+
24
+ SimpleAssets::critical_css_source_files[path] = { 'file' => f, 'page' => asset }
25
+ end
26
+ end
27
+
28
+ def self.get_html_input_for_critical (doc, site)
29
+ return unless doc.respond_to? '[]'
30
+
31
+ SimpleAssets::config['critical_css']['files'].each do |file|
32
+ next if file['html']
33
+
34
+ page_path = doc.path.sub("#{ site.config['source'] }/", '')
35
+
36
+ next unless page_path == file['input_page_path'] || file['layout'] == doc['layout']
37
+
38
+ file['html'] = doc.output
39
+ end
40
+ end
41
+
42
+ def self.generate_critical_css (site)
43
+ css_files_str = ''
44
+
45
+ SimpleAssets::critical_css_source_files.each do |_, f|
46
+ css_files_str += "--css #{ f['file'].path } "
47
+
48
+ f['css'] = CssParser::Parser.new
49
+ f['css'].load_string! f['page'].output
50
+ end
51
+
52
+ SimpleAssets::config['critical_css']['files'].each do |file|
53
+ css_path = File.join(site.config['destination'], file['output_file'])
54
+
55
+ html = file['html']
56
+
57
+ critical_cmd = "npx critical #{ css_files_str }"
58
+
59
+ Jekyll.logger.debug("SimpleAssets:", "Running command: #{ critical_cmd }")
60
+
61
+ Open3.popen3(critical_cmd) do |stdin, stdout, stderr, wait_thr|
62
+ stdin.write(html)
63
+ stdin.close
64
+
65
+ if !wait_thr.value.success? || stderr.read != ''
66
+ Jekyll.logger.error("SimpleAssets:", 'Critical (css) error:')
67
+ stderr.each do |line|
68
+ Jekyll.logger.error("", line)
69
+ end
70
+ elsif stderr.read != ''
71
+ Jekyll.logger.error("SimpleAssets:", 'Critical (css) error:')
72
+ stderr.each do |line|
73
+ Jekyll.logger.error("", line)
74
+ end
75
+ end
76
+
77
+ critical_css_str = stdout.read
78
+
79
+ asset_path = file['output_file'].sub(/^\//, '')
80
+
81
+ base64hash = Digest::MD5.base64digest(critical_css_str)
82
+
83
+ hash = base64hash[0, SimpleAssets::hash_length].gsub(/[+\/]/, '_')
84
+
85
+ SimpleAssets::asset_contenthash_map[asset_path] = hash
86
+
87
+ IO.write(css_path, critical_css_str)
88
+
89
+ site.keep_files << asset_path
90
+
91
+ if file['extract']
92
+ critical_css = CssParser::Parser.new
93
+
94
+ critical_css.load_string! critical_css_str
95
+
96
+ SimpleAssets::critical_css_source_files.each do |_, f|
97
+ f['css'].each_rule_set do |source_rule_set, source_media_type|
98
+ critical_css.each_rule_set do |critical_rule_set, critical_media_type|
99
+ if critical_rule_set.selectors.join(',') == source_rule_set.selectors.join(',')
100
+ f['css'].remove_rule_set! source_rule_set, source_media_type
101
+ f['extract'] = true
102
+
103
+ break
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ SimpleAssets::critical_css_source_files.each do |_, f|
113
+ leftover_css = f['css'].to_s if f['extract']
114
+
115
+ # css_parser leaves blank keyframes so fix them
116
+ keyframes = f['page'].output.scan(/@keyframes\s+(?:.*?)\s*{(?:\s*\S*?\s*{.*?}\s*)+}/m)
117
+ keyframes.each { |keyframe| leftover_css += keyframe }
118
+
119
+ f['page'].output
120
+ end
121
+ end
122
+
123
+ def self.resolve_critical_css_content_hashes (site)
124
+ SimpleAssets::critical_css_source_files.each do |_, source_file|
125
+ page = source_file['page']
126
+ page_path = page.path.sub("#{ site.config['source'] }/", '')
127
+
128
+ SimpleAssets::config['critical_css']['files'].each do |file|
129
+ css_path = File.join(site.config['destination'], file['output_file'])
130
+ content = IO.read(css_path)
131
+
132
+ critical = SimpleAssets::replace_placeholders_for_path(page_path, content)
133
+
134
+ IO.write(css_path, critical)
135
+ end
136
+ end
137
+ end
138
+
139
+
140
+ end
141
+ end
@@ -0,0 +1,92 @@
1
+ module Jekyll
2
+ module SimpleAssets
3
+
4
+ def self.generate_esbuild_config_file ()
5
+ source_path = Jekyll::SimpleAssets::site.config['source']
6
+
7
+ tsconfig_path = File.join(source_path, 'jsconfig.json')
8
+ jsconfig_path = File.join(source_path, 'tsconfig.json')
9
+
10
+ config = {}
11
+
12
+ if File.file?(tsconfig_path)
13
+ config = JSON.parse(File.read(tsconfig_path))
14
+ elsif File.file?(jsconfig_path)
15
+ config = JSON.parse(File.read(jsconfig_path))
16
+ end
17
+
18
+ f = Tempfile.new([ 'jsconfig.esbuild.', '.json' ])
19
+
20
+ config['compilerOptions'] = {} unless config['compilerOptions']
21
+
22
+ base_url = source_path
23
+ if config['compilerOptions']['baseUrl']
24
+ base_url = File.join(source_path, config['compilerOptions']['baseUrl'])
25
+ end
26
+ config['compilerOptions']['baseUrl'] = Pathname.new(base_url).relative_path_from(Pathname.new(f.path))
27
+
28
+ config['compilerOptions']['paths'] = {} unless config['compilerOptions']['paths']
29
+
30
+ relative_path = Pathname.new(source_path).relative_path_from(Pathname.new(base_url))
31
+
32
+ config['compilerOptions']['paths']['@simple-assets/*'] = [
33
+ File.join(relative_path, '_js/*'),
34
+ File.join(relative_path, '_ts/*'),
35
+ ]
36
+
37
+ f.write config.to_json
38
+ f.close
39
+
40
+ Jekyll::SimpleAssets::esbuild_config_file(f)
41
+
42
+ Jekyll.logger.debug("SimpleAssets:", "esbuild: using tsconfig #{ f.path }")
43
+ end
44
+
45
+ def self.esbuild_bundle_file (page, payload, config_path)
46
+ if page.data['bundle'] == false
47
+ return
48
+ end
49
+
50
+ bundle_cmd = "npx esbuild --bundle --tsconfig=#{ config_path }"
51
+
52
+ if page.data['esbuild_flags']
53
+ bundle_cmd += ' ' + page.data['esbuild_flags']
54
+ end
55
+
56
+ node_env = '--define:process.env.NODE_ENV="' + (ENV['JEKYLL_ENV'] != 'production' ? "'development'" : "'production'") + '"'
57
+
58
+ bundle_cmd += ' ' + node_env + " --sourcefile=#{ page.path[/[^\/]*$/] }"
59
+
60
+ if SimpleAssets::source_maps_enabled?
61
+ bundle_cmd += ' ' + '--sourcemap=inline'
62
+ end
63
+
64
+ dir = File.dirname(page.path)
65
+
66
+ Jekyll.logger.info("SimpleAssets:", 'bundling: ' + page.path)
67
+ Jekyll.logger.debug("SimpleAssets:", 'running command: ' + bundle_cmd)
68
+
69
+ Open3.popen3(bundle_cmd, :chdir => dir) do |stdin, stdout, stderr, wait_thr|
70
+ stdin.write(page.output)
71
+ stdin.close
72
+
73
+ bundled = stdout.read
74
+
75
+ if !wait_thr.value.success? || stderr.read != ''
76
+ Jekyll.logger.error("SimpleAssets:", 'esbuild error:')
77
+ stderr.each do |line|
78
+ Jekyll.logger.error("", line)
79
+ end
80
+ elsif stderr.read != ''
81
+ Jekyll.logger.error("SimpleAssets:", 'esbuild error:')
82
+ stderr.each do |line|
83
+ Jekyll.logger.error("", line)
84
+ end
85
+ end
86
+
87
+ page.output = bundled
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,100 @@
1
+ require 'fileutils'
2
+ require 'terser'
3
+
4
+ module Jekyll
5
+
6
+ module SimpleAssets
7
+ def self.symbolize_keys (hash)
8
+ return {} if hash.nil?
9
+
10
+ hash.inject({}) do |result, (key, value)|
11
+
12
+ new_key = case key
13
+ when String then key.to_sym
14
+ else key
15
+ end
16
+ new_value = case value
17
+ when Hash then symbolize_keys(value)
18
+ else value
19
+ end
20
+
21
+ if new_value.is_a?(String) and new_value.match(/^\/.*\/$/)
22
+ new_value = /#{ Regexp.quote(new_value[1..-2]) }/
23
+ end
24
+
25
+ result[new_key] = new_value
26
+
27
+ result
28
+ end
29
+ end
30
+
31
+ def self.merge_recursively(a, b)
32
+ a.merge(b) {|key, a_item, b_item| SimpleAssets::merge_recursively(a_item, b_item) }
33
+ end
34
+
35
+ def self.should_minify_file? (page)
36
+ return false unless page.respond_to? :output
37
+ return false unless SimpleAssets::terser_enabled?
38
+ return false if page.data['do_not_compress'] == true
39
+ return true
40
+ end
41
+
42
+ def self.minify_file (page)
43
+ return unless SimpleAssets::should_minify_file?(page)
44
+
45
+ site_config = symbolize_keys(SimpleAssets::site.config['simple_assets']['terser'])
46
+ page_config = symbolize_keys(page.data['terser'])
47
+
48
+ config = SimpleAssets::merge_recursively(site_config || {}, page_config || {})
49
+
50
+ map_path = page.path + '.map'
51
+ if SimpleAssets::source_maps_enabled?
52
+ config = SimpleAssets::merge_recursively(config, {
53
+ :source_map => { :filename => page.path[/[^\/]*$/], :url => true },
54
+ })
55
+
56
+ minified, source_map = Terser.new(config).compile_with_map(page.output)
57
+ else
58
+ minified = Terser.new(config).compile(page.output)
59
+ end
60
+
61
+ min_path = page.path.sub(/\.(j|t)s$/i, '.min.js')
62
+ Jekyll.logger.info("SimpleAssets:", 'minified: ' + min_path)
63
+
64
+ if source_map
65
+ File.write(File.join(SimpleAssets::site.config['destination'], map_path), source_map)
66
+ minified = minified.gsub(/^\/\/#\s*?source(Mapping)?URL=.*$/, '')
67
+ minified += "\n//# sourceMappingURL=#{ SimpleAssets::relative_url(map_path) }"
68
+
69
+ Jekyll.logger.debug("SimpleAssets:", 'created source map: ' + map_path)
70
+
71
+ SimpleAssets::site.config['keep_files'] << map_path
72
+ end
73
+
74
+ if SimpleAssets::page_assets_map[page.path]
75
+ SimpleAssets::page_assets_map[min_path] = SimpleAssets::page_assets_map[page.path]
76
+ end
77
+
78
+ SimpleAssets::site.config['keep_files'] << min_path
79
+
80
+ return min_path, minified
81
+ end
82
+
83
+ module UglifyFilter
84
+ def uglify (input)
85
+ return if input.nil?
86
+
87
+ return input.split('\n').join('\\\n') unless SimpleAssets::terser_enabled?
88
+
89
+ config = @context.registers[:site].config['simple_assets']
90
+
91
+ terser_config = SimpleAssets::symbolize_keys(config['terser'])
92
+
93
+ Terser.new(terser_config).compile(input)
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ Liquid::Template.register_filter(Jekyll::SimpleAssets::UglifyFilter)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module SimpleAssets
5
- VERSION = "0.1.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-simple-assets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sophie Askew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-19 00:00:00.000000000 Z
11
+ date: 2021-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: css_parser
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: terser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: bundler
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -47,10 +75,15 @@ extra_rdoc_files: []
47
75
  files:
48
76
  - ".gitignore"
49
77
  - Gemfile
78
+ - Gemfile.lock
50
79
  - LICENSE
51
80
  - README.md
52
81
  - jekyll-simple-assets.gemspec
53
82
  - lib/jekyll-simple-assets.rb
83
+ - lib/jekyll-simple-assets/content-hash.rb
84
+ - lib/jekyll-simple-assets/critical.rb
85
+ - lib/jekyll-simple-assets/esbuild.rb
86
+ - lib/jekyll-simple-assets/terser.rb
54
87
  - lib/jekyll-simple-assets/version.rb
55
88
  homepage: https://github.com/syldexiahime/jekyll-simple-assets
56
89
  licenses:
@@ -71,8 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
104
  - !ruby/object:Gem::Version
72
105
  version: '0'
73
106
  requirements: []
74
- rubyforge_project:
75
- rubygems_version: 2.7.6
107
+ rubygems_version: 3.1.3
76
108
  signing_key:
77
109
  specification_version: 4
78
110
  summary: Some simple asset utils for jekyll