theme-check 0.10.2 → 1.3.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 +51 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +39 -0
- data/RELEASING.md +34 -2
- data/bin/theme-check +29 -0
- data/bin/theme-check-language-server +29 -0
- data/config/default.yml +46 -3
- data/config/nothing.yml +11 -0
- data/config/theme_app_extension.yml +168 -0
- data/data/shopify_liquid/objects.yml +2 -0
- data/docs/checks/app_block_valid_tags.md +40 -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/liquid_tag.md +2 -2
- data/docs/checks/missing_template.md +25 -0
- data/docs/checks/pagination_size.md +44 -0
- data/docs/checks/template_length.md +12 -2
- data/docs/checks/undefined_object.md +5 -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 +26 -4
- data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
- 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 +11 -37
- data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
- 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/img_lazy_loading.rb +2 -7
- data/lib/theme_check/checks/img_width_and_height.rb +3 -3
- data/lib/theme_check/checks/liquid_tag.rb +2 -2
- data/lib/theme_check/checks/missing_template.rb +21 -5
- data/lib/theme_check/checks/pagination_size.rb +65 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
- data/lib/theme_check/checks/remote_asset.rb +4 -2
- data/lib/theme_check/checks/space_inside_braces.rb +27 -7
- data/lib/theme_check/checks/template_length.rb +18 -4
- data/lib/theme_check/checks/undefined_object.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks.rb +11 -1
- data/lib/theme_check/cli.rb +52 -15
- data/lib/theme_check/config.rb +56 -10
- data/lib/theme_check/corrector.rb +4 -0
- data/lib/theme_check/exceptions.rb +29 -27
- data/lib/theme_check/file_system_storage.rb +12 -0
- data/lib/theme_check/html_check.rb +1 -0
- data/lib/theme_check/html_node.rb +37 -16
- data/lib/theme_check/html_visitor.rb +17 -3
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/json_file.rb +2 -29
- data/lib/theme_check/json_printer.rb +26 -0
- 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 +6 -2
- data/lib/theme_check/language_server/server.rb +13 -2
- data/lib/theme_check/liquid_check.rb +0 -12
- data/lib/theme_check/node.rb +6 -4
- data/lib/theme_check/offense.rb +56 -3
- data/lib/theme_check/parsing_helpers.rb +7 -4
- data/lib/theme_check/position.rb +98 -14
- data/lib/theme_check/regex_helpers.rb +20 -0
- data/lib/theme_check/tags.rb +62 -8
- data/lib/theme_check/template.rb +3 -32
- data/lib/theme_check/theme.rb +2 -0
- data/lib/theme_check/theme_file.rb +40 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +16 -0
- data/theme-check.gemspec +1 -1
- metadata +26 -7
- data/bin/liquid-server +0 -4
@@ -21,8 +21,10 @@ The default configuration for this check is the following:
|
|
21
21
|
```yaml
|
22
22
|
TemplateLength:
|
23
23
|
enabled: true
|
24
|
-
max_length:
|
24
|
+
max_length: 500
|
25
25
|
exclude_schema: true
|
26
|
+
exclude_stylesheet: true
|
27
|
+
exclude_javascript: true
|
26
28
|
```
|
27
29
|
|
28
30
|
### `max_length`
|
@@ -31,7 +33,15 @@ The `max_length` (Default: `200`) option determines the maximum number of lines
|
|
31
33
|
|
32
34
|
### `exclude_schema`
|
33
35
|
|
34
|
-
The `exclude_schema` (Default: `true`) option determines if the schema
|
36
|
+
The `exclude_schema` (Default: `true`) option determines if the lines inside `{% schema %}` blocks from a template should be excluded from the line count.
|
37
|
+
|
38
|
+
### `exclude_stylesheet`
|
39
|
+
|
40
|
+
The `exclude_stylesheet` (Default: `true`) option determines if the lines inside `{% stylesheet %}` blocks from a template should be excluded from the line count.
|
41
|
+
|
42
|
+
### `exclude_javascript`
|
43
|
+
|
44
|
+
The `exclude_javascript` (Default: `true`) option determines if the lines inside `{% javascript %}` blocks from a template should be excluded from the line count.
|
35
45
|
|
36
46
|
## When Not To Use It
|
37
47
|
|
@@ -33,8 +33,13 @@ The default configuration for this check is the following:
|
|
33
33
|
```yaml
|
34
34
|
UndefinedObject:
|
35
35
|
enabled: true
|
36
|
+
exclude_snippets: true
|
36
37
|
```
|
37
38
|
|
39
|
+
### `exclude_snippets`
|
40
|
+
|
41
|
+
The `exclude_snippets` (Default: `true`) option determines whether to check for undefined objects in snippets file (as objects _may_ be defined as arguments)
|
42
|
+
|
38
43
|
## When Not To Use It
|
39
44
|
|
40
45
|
It is discouraged to disable this rule.
|
data/lib/theme_check/analyzer.rb
CHANGED
@@ -34,9 +34,11 @@ module ThemeCheck
|
|
34
34
|
|
35
35
|
liquid_visitor = Visitor.new(@liquid_checks, @disabled_checks)
|
36
36
|
html_visitor = HtmlVisitor.new(@html_checks)
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
ThemeCheck.with_liquid_c_disabled do
|
38
|
+
@theme.liquid.each do |template|
|
39
|
+
liquid_visitor.visit_template(template)
|
40
|
+
html_visitor.visit_template(template)
|
41
|
+
end
|
40
42
|
end
|
41
43
|
|
42
44
|
@theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
|
@@ -47,24 +49,26 @@ module ThemeCheck
|
|
47
49
|
def analyze_files(files)
|
48
50
|
reset
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
52
|
+
ThemeCheck.with_liquid_c_disabled do
|
53
|
+
# Call all checks that run on the whole theme
|
54
|
+
liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
|
55
|
+
html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
|
56
|
+
@theme.liquid.each do |template|
|
57
|
+
liquid_visitor.visit_template(template)
|
58
|
+
html_visitor.visit_template(template)
|
59
|
+
end
|
60
|
+
@theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
|
61
|
+
|
62
|
+
# Call checks that run on a single files, only on specified file
|
63
|
+
liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
|
64
|
+
html_visitor = HtmlVisitor.new(@html_checks.single_file)
|
65
|
+
files.each do |file|
|
66
|
+
if file.liquid?
|
67
|
+
liquid_visitor.visit_template(file)
|
68
|
+
html_visitor.visit_template(file)
|
69
|
+
elsif file.json?
|
70
|
+
@json_checks.single_file.call(:on_file, file)
|
71
|
+
end
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
@@ -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)
|
@@ -80,12 +91,23 @@ module ThemeCheck
|
|
80
91
|
@offenses ||= []
|
81
92
|
end
|
82
93
|
|
83
|
-
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
|
84
|
-
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
|
94
|
+
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, node_markup_offset: 0, &block)
|
95
|
+
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, node_markup_offset: node_markup_offset, correction: block)
|
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,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when invalid tags are used in a Theme App
|
4
|
+
# Extension block
|
5
|
+
class AppBlockValidTags < LiquidCheck
|
6
|
+
severity :error
|
7
|
+
category :liquid
|
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
|
+
OFFENSE_MSG = "Theme app extension blocks cannot contain %s tags"
|
15
|
+
|
16
|
+
def on_javascript(node)
|
17
|
+
add_offense(OFFENSE_MSG % 'javascript', node: node)
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_stylesheet(node)
|
21
|
+
add_offense(OFFENSE_MSG % 'stylesheet', node: node)
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_include(node)
|
25
|
+
add_offense(OFFENSE_MSG % 'include', node: node)
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_layout(node)
|
29
|
+
add_offense(OFFENSE_MSG % 'layout', node: node)
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_section(node)
|
33
|
+
add_offense(OFFENSE_MSG % 'section', node: node)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -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'] != "stylesheet"
|
17
|
+
file_size = href_to_file_size(node.attributes['href'])
|
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
|