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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +2 -6
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +39 -0
  5. data/RELEASING.md +34 -2
  6. data/Rakefile +1 -1
  7. data/config/default.yml +39 -3
  8. data/config/nothing.yml +11 -0
  9. data/config/theme_app_extension.yml +153 -0
  10. data/data/shopify_liquid/objects.yml +2 -0
  11. data/docs/checks/asset_size_app_block_css.md +52 -0
  12. data/docs/checks/asset_size_app_block_javascript.md +57 -0
  13. data/docs/checks/asset_size_css_stylesheet_tag.md +50 -0
  14. data/docs/checks/deprecate_bgsizes.md +66 -0
  15. data/docs/checks/deprecate_lazysizes.md +61 -0
  16. data/docs/checks/html_parsing_error.md +50 -0
  17. data/docs/checks/liquid_tag.md +2 -2
  18. data/docs/checks/template_length.md +12 -2
  19. data/exe/theme-check-language-server.bat +3 -0
  20. data/exe/theme-check.bat +3 -0
  21. data/lib/theme_check.rb +15 -0
  22. data/lib/theme_check/analyzer.rb +25 -21
  23. data/lib/theme_check/asset_file.rb +3 -15
  24. data/lib/theme_check/bug.rb +3 -1
  25. data/lib/theme_check/check.rb +24 -2
  26. data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
  27. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
  28. data/lib/theme_check/checks/asset_size_css.rb +11 -74
  29. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +24 -0
  30. data/lib/theme_check/checks/asset_size_javascript.rb +10 -36
  31. data/lib/theme_check/checks/deprecate_bgsizes.rb +14 -0
  32. data/lib/theme_check/checks/deprecate_lazysizes.rb +16 -0
  33. data/lib/theme_check/checks/html_parsing_error.rb +12 -0
  34. data/lib/theme_check/checks/img_lazy_loading.rb +1 -6
  35. data/lib/theme_check/checks/liquid_tag.rb +2 -2
  36. data/lib/theme_check/checks/remote_asset.rb +2 -0
  37. data/lib/theme_check/checks/space_inside_braces.rb +1 -1
  38. data/lib/theme_check/checks/template_length.rb +18 -4
  39. data/lib/theme_check/cli.rb +34 -13
  40. data/lib/theme_check/config.rb +56 -10
  41. data/lib/theme_check/exceptions.rb +29 -27
  42. data/lib/theme_check/html_check.rb +2 -0
  43. data/lib/theme_check/html_visitor.rb +3 -1
  44. data/lib/theme_check/json_file.rb +2 -29
  45. data/lib/theme_check/language_server/constants.rb +8 -0
  46. data/lib/theme_check/language_server/document_link_engine.rb +40 -4
  47. data/lib/theme_check/language_server/handler.rb +1 -1
  48. data/lib/theme_check/language_server/server.rb +13 -2
  49. data/lib/theme_check/liquid_check.rb +0 -12
  50. data/lib/theme_check/parsing_helpers.rb +3 -1
  51. data/lib/theme_check/regex_helpers.rb +17 -0
  52. data/lib/theme_check/tags.rb +62 -8
  53. data/lib/theme_check/template.rb +3 -32
  54. data/lib/theme_check/theme_file.rb +40 -0
  55. data/lib/theme_check/version.rb +1 -1
  56. 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
- @relative_path = relative_path
9
- @storage = storage
7
+ super
10
8
  @loaded = false
11
9
  @content = nil
12
10
  end
13
11
 
14
- def path
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
@@ -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
- abort(message + BUG_POSTAMBLE)
20
+ raise ThemeCheckError, message + BUG_POSTAMBLE
19
21
  end
20
22
  end
@@ -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/master/docs/checks/#{File.basename(path, '.rb')}.md"
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 < LiquidCheck
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 on_document(node)
35
- @node = node
36
- @source = node.template.source
37
- record_offenses
38
- end
39
-
40
- def record_offenses
41
- stylesheets(@source).each do |stylesheet|
42
- file_size = href_to_file_size(stylesheet.href)
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 < LiquidCheck
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 on_document(node)
30
- @node = node
31
- @source = node.template.source
32
- record_offenses
33
- end
34
-
35
- def record_offenses
36
- scripts(@source).each do |script|
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)