theme-check 0.10.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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)