theme-check 0.10.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/theme-check.yml +2 -6
- data/CHANGELOG.md +41 -0
- data/README.md +39 -0
- data/RELEASING.md +34 -2
- data/Rakefile +1 -1
- data/config/default.yml +39 -3
- data/config/nothing.yml +11 -0
- data/config/theme_app_extension.yml +153 -0
- data/data/shopify_liquid/objects.yml +2 -0
- data/docs/checks/asset_size_app_block_css.md +52 -0
- data/docs/checks/asset_size_app_block_javascript.md +57 -0
- data/docs/checks/asset_size_css_stylesheet_tag.md +50 -0
- data/docs/checks/deprecate_bgsizes.md +66 -0
- data/docs/checks/deprecate_lazysizes.md +61 -0
- data/docs/checks/html_parsing_error.md +50 -0
- data/docs/checks/liquid_tag.md +2 -2
- data/docs/checks/template_length.md +12 -2
- data/exe/theme-check-language-server.bat +3 -0
- data/exe/theme-check.bat +3 -0
- data/lib/theme_check.rb +15 -0
- data/lib/theme_check/analyzer.rb +25 -21
- data/lib/theme_check/asset_file.rb +3 -15
- data/lib/theme_check/bug.rb +3 -1
- data/lib/theme_check/check.rb +24 -2
- data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
- data/lib/theme_check/checks/asset_size_css.rb +11 -74
- data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +24 -0
- data/lib/theme_check/checks/asset_size_javascript.rb +10 -36
- data/lib/theme_check/checks/deprecate_bgsizes.rb +14 -0
- data/lib/theme_check/checks/deprecate_lazysizes.rb +16 -0
- data/lib/theme_check/checks/html_parsing_error.rb +12 -0
- data/lib/theme_check/checks/img_lazy_loading.rb +1 -6
- data/lib/theme_check/checks/liquid_tag.rb +2 -2
- data/lib/theme_check/checks/remote_asset.rb +2 -0
- data/lib/theme_check/checks/space_inside_braces.rb +1 -1
- data/lib/theme_check/checks/template_length.rb +18 -4
- data/lib/theme_check/cli.rb +34 -13
- data/lib/theme_check/config.rb +56 -10
- data/lib/theme_check/exceptions.rb +29 -27
- data/lib/theme_check/html_check.rb +2 -0
- data/lib/theme_check/html_visitor.rb +3 -1
- data/lib/theme_check/json_file.rb +2 -29
- data/lib/theme_check/language_server/constants.rb +8 -0
- data/lib/theme_check/language_server/document_link_engine.rb +40 -4
- data/lib/theme_check/language_server/handler.rb +1 -1
- data/lib/theme_check/language_server/server.rb +13 -2
- data/lib/theme_check/liquid_check.rb +0 -12
- data/lib/theme_check/parsing_helpers.rb +3 -1
- data/lib/theme_check/regex_helpers.rb +17 -0
- data/lib/theme_check/tags.rb +62 -8
- data/lib/theme_check/template.rb +3 -32
- data/lib/theme_check/theme_file.rb +40 -0
- data/lib/theme_check/version.rb +1 -1
- metadata +22 -3
@@ -1,27 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require "pathname"
|
3
2
|
require "zlib"
|
4
3
|
|
5
4
|
module ThemeCheck
|
6
|
-
class AssetFile
|
5
|
+
class AssetFile < ThemeFile
|
7
6
|
def initialize(relative_path, storage)
|
8
|
-
|
9
|
-
@storage = storage
|
7
|
+
super
|
10
8
|
@loaded = false
|
11
9
|
@content = nil
|
12
10
|
end
|
13
11
|
|
14
|
-
|
15
|
-
@storage.path(@relative_path)
|
16
|
-
end
|
17
|
-
|
18
|
-
def relative_path
|
19
|
-
@relative_pathname ||= Pathname.new(@relative_path)
|
20
|
-
end
|
21
|
-
|
22
|
-
def content
|
23
|
-
@content ||= @storage.read(@relative_path)
|
24
|
-
end
|
12
|
+
alias_method :content, :source
|
25
13
|
|
26
14
|
def gzipped_size
|
27
15
|
@gzipped_size ||= Zlib.gzip(content).bytesize
|
data/lib/theme_check/bug.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
require 'theme_check/version'
|
3
3
|
|
4
4
|
module ThemeCheck
|
5
|
+
class ThemeCheckError < StandardError; end
|
6
|
+
|
5
7
|
BUG_POSTAMBLE = <<~EOS
|
6
8
|
Theme Check Version: #{VERSION}
|
7
9
|
Ruby Version: #{RUBY_VERSION}
|
@@ -15,6 +17,6 @@ module ThemeCheck
|
|
15
17
|
EOS
|
16
18
|
|
17
19
|
def self.bug(message)
|
18
|
-
|
20
|
+
raise ThemeCheckError, message + BUG_POSTAMBLE
|
19
21
|
end
|
20
22
|
end
|
data/lib/theme_check/check.rb
CHANGED
@@ -9,12 +9,19 @@ module ThemeCheck
|
|
9
9
|
attr_accessor :options, :ignored_patterns
|
10
10
|
attr_writer :offenses
|
11
11
|
|
12
|
+
# The order matters.
|
12
13
|
SEVERITIES = [
|
13
14
|
:error,
|
14
15
|
:suggestion,
|
15
16
|
:style,
|
16
17
|
]
|
17
18
|
|
19
|
+
# [severity: sym] => number
|
20
|
+
SEVERITY_VALUES = SEVERITIES
|
21
|
+
.map
|
22
|
+
.with_index { |sev, i| [sev, i] }
|
23
|
+
.to_h
|
24
|
+
|
18
25
|
CATEGORIES = [
|
19
26
|
:liquid,
|
20
27
|
:translation,
|
@@ -38,6 +45,10 @@ module ThemeCheck
|
|
38
45
|
@severity if defined?(@severity)
|
39
46
|
end
|
40
47
|
|
48
|
+
def severity_value(severity)
|
49
|
+
SEVERITY_VALUES[severity]
|
50
|
+
end
|
51
|
+
|
41
52
|
def categories(*categories)
|
42
53
|
@categories ||= []
|
43
54
|
if categories.any?
|
@@ -58,7 +69,7 @@ module ThemeCheck
|
|
58
69
|
end
|
59
70
|
|
60
71
|
def docs_url(path)
|
61
|
-
"https://github.com/Shopify/theme-check/blob/
|
72
|
+
"https://github.com/Shopify/theme-check/blob/main/docs/checks/#{File.basename(path, '.rb')}.md"
|
62
73
|
end
|
63
74
|
|
64
75
|
def can_disable(disableable = nil)
|
@@ -85,7 +96,18 @@ module ThemeCheck
|
|
85
96
|
end
|
86
97
|
|
87
98
|
def severity
|
88
|
-
self.class.severity
|
99
|
+
@severity ||= self.class.severity
|
100
|
+
end
|
101
|
+
|
102
|
+
def severity=(severity)
|
103
|
+
unless SEVERITIES.include?(severity)
|
104
|
+
raise ArgumentError, "unknown severity. Use: #{SEVERITIES.join(', ')}"
|
105
|
+
end
|
106
|
+
@severity = severity
|
107
|
+
end
|
108
|
+
|
109
|
+
def severity_value
|
110
|
+
SEVERITY_VALUES[severity]
|
89
111
|
end
|
90
112
|
|
91
113
|
def categories
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when too much CSS is being referenced from a Theme App
|
4
|
+
# Extension block
|
5
|
+
class AssetSizeAppBlockCSS < LiquidCheck
|
6
|
+
severity :error
|
7
|
+
category :performance
|
8
|
+
doc docs_url(__FILE__)
|
9
|
+
|
10
|
+
# Don't allow this check to be disabled with a comment,
|
11
|
+
# since we need to be able to enforce this server-side
|
12
|
+
can_disable false
|
13
|
+
|
14
|
+
attr_reader :threshold_in_bytes
|
15
|
+
|
16
|
+
def initialize(threshold_in_bytes: 100_000)
|
17
|
+
@threshold_in_bytes = threshold_in_bytes
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_schema(node)
|
21
|
+
schema = JSON.parse(node.value.nodelist.join)
|
22
|
+
|
23
|
+
if (stylesheet = schema["stylesheet"])
|
24
|
+
size = asset_size(stylesheet)
|
25
|
+
if size && size > threshold_in_bytes
|
26
|
+
add_offense(
|
27
|
+
"CSS in Theme App Extension blocks exceeds compressed size threshold (#{threshold_in_bytes} Bytes)",
|
28
|
+
node: node
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue JSON::ParserError
|
33
|
+
# Ignored, handled in ValidSchema.
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def asset_size(name)
|
39
|
+
asset = @theme["assets/#{name}"]
|
40
|
+
return if asset.nil?
|
41
|
+
asset.gzipped_size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when too much JS is being referenced from a Theme App
|
4
|
+
# Extension block
|
5
|
+
class AssetSizeAppBlockJavaScript < LiquidCheck
|
6
|
+
severity :error
|
7
|
+
category :performance
|
8
|
+
doc docs_url(__FILE__)
|
9
|
+
|
10
|
+
# Don't allow this check to be disabled with a comment,
|
11
|
+
# since we need to be able to enforce this server-side
|
12
|
+
can_disable false
|
13
|
+
|
14
|
+
attr_reader :threshold_in_bytes
|
15
|
+
|
16
|
+
def initialize(threshold_in_bytes: 10_000)
|
17
|
+
@threshold_in_bytes = threshold_in_bytes
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_schema(node)
|
21
|
+
schema = JSON.parse(node.value.nodelist.join)
|
22
|
+
|
23
|
+
if (javascript = schema["javascript"])
|
24
|
+
size = asset_size(javascript)
|
25
|
+
if size && size > threshold_in_bytes
|
26
|
+
add_offense(
|
27
|
+
"JavaScript in Theme App Extension blocks exceeds compressed size threshold (#{threshold_in_bytes} Bytes)",
|
28
|
+
node: node
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue JSON::ParserError
|
33
|
+
# Ignored, handled in ValidSchema.
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def asset_size(name)
|
39
|
+
asset = @theme["assets/#{name}"]
|
40
|
+
return if asset.nil?
|
41
|
+
asset.gzipped_size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,89 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
|
-
class AssetSizeCSS <
|
3
|
+
class AssetSizeCSS < HtmlCheck
|
4
4
|
include RegexHelpers
|
5
5
|
severity :error
|
6
|
-
category :performance
|
6
|
+
category :html, :performance
|
7
7
|
doc docs_url(__FILE__)
|
8
8
|
|
9
|
-
Link = Struct.new(:href, :index)
|
10
|
-
|
11
|
-
LINK_TAG_HREF = %r{
|
12
|
-
<link
|
13
|
-
(?=[^>]+?rel=['"]?stylesheet['"]?) # Make sure rel=stylesheet is in the link with lookahead
|
14
|
-
[^>]+ # any non closing tag character
|
15
|
-
href= # href attribute start
|
16
|
-
(?<href>#{QUOTED_LIQUID_ATTRIBUTE}) # href attribute value (may contain liquid)
|
17
|
-
[^>]* # any non closing character till the end
|
18
|
-
>
|
19
|
-
}omix
|
20
|
-
STYLESHEET_TAG = %r{
|
21
|
-
#{Liquid::VariableStart} # VariableStart
|
22
|
-
(?:(?!#{Liquid::VariableEnd}).)*? # anything that isn't followed by a VariableEnd
|
23
|
-
\|\s*asset_url\s* # | asset_url
|
24
|
-
\|\s*stylesheet_tag\s* # | stylesheet_tag
|
25
|
-
#{Liquid::VariableEnd} # VariableEnd
|
26
|
-
}omix
|
27
|
-
|
28
9
|
attr_reader :threshold_in_bytes
|
29
10
|
|
30
11
|
def initialize(threshold_in_bytes: 100_000)
|
31
12
|
@threshold_in_bytes = threshold_in_bytes
|
32
13
|
end
|
33
14
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
next if file_size.nil?
|
44
|
-
next if file_size <= threshold_in_bytes
|
45
|
-
add_offense(
|
46
|
-
"CSS on every page load exceding compressed size threshold (#{threshold_in_bytes} Bytes).",
|
47
|
-
node: @node,
|
48
|
-
markup: stylesheet.href,
|
49
|
-
line_number: @source[0...stylesheet.index].count("\n") + 1
|
50
|
-
)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def stylesheets(source)
|
55
|
-
stylesheet_links = matches(source, LINK_TAG_HREF)
|
56
|
-
.map do |m|
|
57
|
-
Link.new(
|
58
|
-
m[:href].gsub(START_OR_END_QUOTE, ""),
|
59
|
-
m.begin(:href),
|
60
|
-
)
|
61
|
-
end
|
62
|
-
|
63
|
-
stylesheet_tags = matches(source, STYLESHEET_TAG)
|
64
|
-
.map do |m|
|
65
|
-
Link.new(
|
66
|
-
m[0],
|
67
|
-
m.begin(0),
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
|
-
stylesheet_links + stylesheet_tags
|
72
|
-
end
|
73
|
-
|
74
|
-
def href_to_file_size(href)
|
75
|
-
# asset_url (+ optional stylesheet_tag) variables
|
76
|
-
if href =~ /^#{VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
|
77
|
-
asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
|
78
|
-
asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
|
79
|
-
return if asset.nil?
|
80
|
-
asset.gzipped_size
|
81
|
-
|
82
|
-
# remote URLs
|
83
|
-
elsif href =~ %r{^(https?:)?//}
|
84
|
-
asset = RemoteAssetFile.from_src(href)
|
85
|
-
asset.gzipped_size
|
86
|
-
end
|
15
|
+
def on_link(node)
|
16
|
+
return if node.attributes['rel']&.value != "stylesheet"
|
17
|
+
file_size = href_to_file_size(node.attributes['href']&.value)
|
18
|
+
return if file_size.nil?
|
19
|
+
return if file_size <= threshold_in_bytes
|
20
|
+
add_offense(
|
21
|
+
"CSS on every page load exceeding compressed size threshold (#{threshold_in_bytes} Bytes).",
|
22
|
+
node: node
|
23
|
+
)
|
87
24
|
end
|
88
25
|
end
|
89
26
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class AssetSizeCSSStylesheetTag < LiquidCheck
|
4
|
+
include RegexHelpers
|
5
|
+
severity :error
|
6
|
+
category :liquid, :performance
|
7
|
+
doc docs_url(__FILE__)
|
8
|
+
|
9
|
+
def initialize(threshold_in_bytes: 100_000)
|
10
|
+
@threshold_in_bytes = threshold_in_bytes
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_variable(node)
|
14
|
+
used_filters = node.value.filters.map { |name, *_rest| name }
|
15
|
+
return unless used_filters.include?("stylesheet_tag")
|
16
|
+
file_size = href_to_file_size('{{' + node.markup + '}}')
|
17
|
+
return if file_size <= @threshold_in_bytes
|
18
|
+
add_offense(
|
19
|
+
"CSS on every page load exceeding compressed size threshold (#{@threshold_in_bytes} Bytes).",
|
20
|
+
node: node
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -3,52 +3,26 @@ module ThemeCheck
|
|
3
3
|
# Reports errors when trying to use too much JavaScript on page load
|
4
4
|
# Encourages the use of the Import on Interaction pattern [1].
|
5
5
|
# [1]: https://addyosmani.com/blog/import-on-interaction/
|
6
|
-
class AssetSizeJavaScript <
|
6
|
+
class AssetSizeJavaScript < HtmlCheck
|
7
7
|
include RegexHelpers
|
8
8
|
severity :error
|
9
|
-
category :performance
|
9
|
+
category :html, :performance
|
10
10
|
doc docs_url(__FILE__)
|
11
11
|
|
12
|
-
Script = Struct.new(:src, :match)
|
13
|
-
|
14
|
-
SCRIPT_TAG_SRC = %r{
|
15
|
-
<script
|
16
|
-
[^>]+ # any non closing tag character
|
17
|
-
src= # src attribute start
|
18
|
-
(?<src>#{QUOTED_LIQUID_ATTRIBUTE}) # src attribute value (may contain liquid)
|
19
|
-
[^>]* # any non closing character till the end
|
20
|
-
>
|
21
|
-
}omix
|
22
|
-
|
23
12
|
attr_reader :threshold_in_bytes
|
24
13
|
|
25
14
|
def initialize(threshold_in_bytes: 10000)
|
26
15
|
@threshold_in_bytes = threshold_in_bytes
|
27
16
|
end
|
28
17
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
file_size = src_to_file_size(script.src)
|
38
|
-
next if file_size.nil?
|
39
|
-
next if file_size <= threshold_in_bytes
|
40
|
-
add_offense(
|
41
|
-
"JavaScript on every page load exceding compressed size threshold (#{threshold_in_bytes} Bytes), consider using the import on interaction pattern.",
|
42
|
-
node: @node,
|
43
|
-
markup: script.src,
|
44
|
-
line_number: @source[0...script.match.begin(:src)].count("\n") + 1
|
45
|
-
)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def scripts(source)
|
50
|
-
matches(source, SCRIPT_TAG_SRC)
|
51
|
-
.map { |m| Script.new(m[:src].gsub(START_OR_END_QUOTE, ""), m) }
|
18
|
+
def on_script(node)
|
19
|
+
file_size = src_to_file_size(node.attributes['src']&.value)
|
20
|
+
return if file_size.nil?
|
21
|
+
return if file_size <= threshold_in_bytes
|
22
|
+
add_offense(
|
23
|
+
"JavaScript on every page load exceeds compressed size threshold (#{threshold_in_bytes} Bytes), consider using the import on interaction pattern.",
|
24
|
+
node: node
|
25
|
+
)
|
52
26
|
end
|
53
27
|
|
54
28
|
def src_to_file_size(src)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class DeprecateBgsizes < HtmlCheck
|
4
|
+
severity :suggestion
|
5
|
+
category :html, :performance
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
def on_div(node)
|
9
|
+
class_list = node.attributes["class"]&.value&.split(" ")
|
10
|
+
add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node) if class_list&.include?("lazyload")
|
11
|
+
add_offense("Use the CSS imageset attribute instead of data-bgset", node: node) if node.attributes["data-bgset"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class DeprecateLazysizes < HtmlCheck
|
4
|
+
severity :suggestion
|
5
|
+
category :html, :performance
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
def on_img(node)
|
9
|
+
class_list = node.attributes["class"]&.value&.split(" ")
|
10
|
+
add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node) if class_list&.include?("lazyload")
|
11
|
+
add_offense("Use the native srcset attribute instead of data-srcset", node: node) if node.attributes["data-srcset"]
|
12
|
+
add_offense("Use the native sizes attribute instead of data-sizes", node: node) if node.attributes["data-sizes"]
|
13
|
+
add_offense("Do not set the data-sizes attribute to auto", node: node) if node.attributes["data-sizes"]&.value == "auto"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class HtmlParsingError < HtmlCheck
|
4
|
+
severity :error
|
5
|
+
category :html
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
def on_parse_error(exception, template)
|
9
|
+
add_offense("HTML in this template can not be parsed: #{exception.message}", template: template)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -10,12 +10,7 @@ module ThemeCheck
|
|
10
10
|
def on_img(node)
|
11
11
|
loading = node.attributes["loading"]&.value&.downcase
|
12
12
|
return if ACCEPTED_LOADING_VALUES.include?(loading)
|
13
|
-
|
14
|
-
class_list = node.attributes["class"]&.value&.split(" ")
|
15
|
-
|
16
|
-
if class_list&.include?("lazyload")
|
17
|
-
add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node)
|
18
|
-
elsif loading == "auto"
|
13
|
+
if loading == "auto"
|
19
14
|
add_offense("Prefer loading=\"lazy\" to defer loading of images", node: node)
|
20
15
|
else
|
21
16
|
add_offense("Add a loading=\"lazy\" attribute to defer loading of images", node: node)
|