theme-check 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94b918e53777cfb70c65dacd9d9bfbdb6f4463597c8f9c81969e1baef89df7a1
4
- data.tar.gz: 7e45af9a3dfeab1eafdff5533df68a9e0d67007d94661a2c974c0c2fece61095
3
+ metadata.gz: adc4aaf3260408a9b52d9afaa59662308f173a5d5dcd0f3663cbcc68da1d7f39
4
+ data.tar.gz: 50cd7e0d42fda99ad3816a846c25868c38b92ea521e0be759c515548dd82f70b
5
5
  SHA512:
6
- metadata.gz: ea1b75ca11a66aefe5eef421f577d53716cb180799863185d36fad21a9033b5cf52579df3e593e41867513da410adc7d16de723f35b3a5435e50098b7af2a348
7
- data.tar.gz: ceab091f6bf7317d899cb5459601b7dfb5294e6e4be962a421b33bad33c74fd730001dea90687f81f754d57e5cdedc5b39d0efb5d95d6c016ed965f7e4e86a9f
6
+ metadata.gz: 7ffad3310e441edd678143de233584a09167ffff1e09b96109cef417d360d797821dc9ef3491af4564608cd3b35aa59be41ebea6bb76030e1544d53bc9db8d02
7
+ data.tar.gz: 30b6d8f3b58b95c833a78bc30ceea6cfcfa35d563e3d184b6aee51817965be87b352c2dbad5ff67a74f2a64479fe1945c11db738c1bfb6e64f610ae15b862cb3
data/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
1
 
2
+ v1.1.0 / 2021-07-06
3
+ ==================
4
+
5
+ * Add `--fail-level` CLI flag to configure exit code
6
+ * Refactor all theme file classes to inherit from `ThemeFile`
7
+ * Fix `undefined method liquid?` error when scanning from LSP
8
+ * Adding asset document links
9
+ * Allow initializing theme app extension configuration files
10
+ * Allow disabling registering mock Liquid tags w/ `ThemeCheck::Tags.register_tags = false`
11
+ * Support Theme App Extensions
12
+ * Add checks for theme app extension block JS/CSS
13
+ * Disable Liquid::C when parsing Liquid templates
14
+
2
15
  v1.0.0 / 2021-06-28
3
16
  ==================
4
17
 
data/README.md CHANGED
@@ -100,6 +100,8 @@ TemplateLength:
100
100
  # Or ignore certain paths
101
101
  ignore:
102
102
  - snippets/icon-*
103
+ # Or change the severity (error|suggestion|style)
104
+ severity: suggestion
103
105
 
104
106
  # Enable a custom check
105
107
  MyCustomCheck
@@ -141,3 +143,40 @@ Disable checks for the _entire document_ by placing the comment on the first lin
141
143
 
142
144
  {%assign x = 1%}
143
145
  ```
146
+
147
+ ## Exit Code and `--fail-level`
148
+
149
+ Use the `--fail-level` (default: `error`) flag to configure the exit code of theme-check. Useful in CI scenarios.
150
+
151
+ Example:
152
+
153
+ ```
154
+ # Make CI fail on styles warnings, suggestions, and errors
155
+ theme-check --fail-level style path_to_theme
156
+
157
+ # Make CI fail on suggestions, and errors
158
+ theme-check --fail-level suggestion path_to_theme
159
+
160
+ # Make CI fail on errors
161
+ theme-check path_to_theme
162
+ ```
163
+
164
+ There are three fail levels:
165
+
166
+ - `error`
167
+ - `suggestion`
168
+ - `style`
169
+
170
+ Exit code meanings:
171
+
172
+ - 0: Success!
173
+ - 1: Your code doesn't pass the checks
174
+ - 2: There's a bug in theme-check
175
+
176
+ If you would like to change the severity of a check, you can do so with the `severity` attribute. Example:
177
+
178
+ ```yaml
179
+ DeprecateLazysizes:
180
+ enabled: true
181
+ severity: error
182
+ ```
data/config/default.yml CHANGED
@@ -1,7 +1,13 @@
1
1
  root: .
2
2
 
3
+ extends: :nothing
4
+
3
5
  require: []
4
6
 
7
+ include_categories: []
8
+
9
+ exclude_categories: []
10
+
5
11
  ignore:
6
12
  - node_modules/*
7
13
 
@@ -161,3 +167,13 @@ ImgLazyLoading:
161
167
  HtmlParsingError:
162
168
  enabled: true
163
169
  ignore: []
170
+
171
+ AssetSizeAppBlockJavaScript:
172
+ enabled: false
173
+ ignore: []
174
+ threshold_in_bytes: 10_000
175
+
176
+ AssetSizeAppBlockCSS:
177
+ enabled: false
178
+ ignore: []
179
+ threshold_in_bytes: 100_000
@@ -0,0 +1,11 @@
1
+ root: .
2
+
3
+ require: []
4
+
5
+ include_categories: []
6
+
7
+ exclude_categories: []
8
+
9
+ ignore:
10
+ - node_modules/*
11
+
@@ -0,0 +1,153 @@
1
+ root: .
2
+
3
+ extends: :nothing
4
+
5
+ require: []
6
+
7
+ include_categories: []
8
+
9
+ exclude_categories: []
10
+
11
+ ignore:
12
+ - node_modules/*
13
+
14
+ ConvertIncludeToRender:
15
+ enabled: true
16
+ ignore: []
17
+
18
+ LiquidTag:
19
+ enabled: true
20
+ ignore: []
21
+ min_consecutive_statements: 4
22
+
23
+ MissingTemplate:
24
+ enabled: true
25
+ ignore: []
26
+
27
+ NestedSnippet:
28
+ enabled: true
29
+ ignore: []
30
+ max_nesting_level: 3
31
+
32
+ RequiredLayoutThemeObject:
33
+ enabled: true
34
+ ignore: []
35
+
36
+ SpaceInsideBraces:
37
+ enabled: true
38
+ ignore: []
39
+
40
+ SyntaxError:
41
+ enabled: true
42
+ ignore: []
43
+
44
+ TemplateLength:
45
+ enabled: true
46
+ ignore: []
47
+ max_length: 200
48
+ # Exclude content of {% schema %} in line count
49
+ exclude_schema: true
50
+
51
+ UnknownFilter:
52
+ enabled: true
53
+ ignore: []
54
+
55
+ UnusedAssign:
56
+ enabled: true
57
+ ignore: []
58
+
59
+ UnusedSnippet:
60
+ enabled: true
61
+ ignore: []
62
+
63
+ MatchingSchemaTranslations:
64
+ enabled: true
65
+ ignore: []
66
+
67
+ MatchingTranslations:
68
+ enabled: true
69
+ ignore: []
70
+
71
+ TranslationKeyExists:
72
+ enabled: true
73
+ ignore: []
74
+
75
+ ValidHTMLTranslation:
76
+ enabled: true
77
+ ignore: []
78
+
79
+ ValidJson:
80
+ enabled: true
81
+ ignore: []
82
+
83
+ ValidSchema:
84
+ enabled: true
85
+ ignore: []
86
+
87
+ UndefinedObject:
88
+ enabled: true
89
+ ignore: []
90
+ exclude_snippets: true
91
+
92
+ RequiredDirectories:
93
+ enabled: false
94
+ ignore: []
95
+
96
+ DeprecatedFilter:
97
+ enabled: true
98
+ ignore: []
99
+
100
+ MissingEnableComment:
101
+ enabled: true
102
+ ignore: []
103
+
104
+ ParserBlockingJavaScript:
105
+ enabled: true
106
+ ignore: []
107
+
108
+ ParserBlockingScriptTag:
109
+ enabled: true
110
+
111
+ AssetSizeJavaScript:
112
+ enabled: true
113
+ ignore: []
114
+ threshold_in_bytes: 10_000
115
+
116
+ AssetSizeCSS:
117
+ enabled: true
118
+ ignore: []
119
+ threshold_in_bytes: 100_000
120
+
121
+ ImgWidthAndHeight:
122
+ enabled: true
123
+ ignore: []
124
+
125
+ RemoteAsset:
126
+ enabled: true
127
+ ignore: []
128
+
129
+ AssetUrlFilters:
130
+ enabled: true
131
+ ignore: []
132
+
133
+ ContentForHeaderModification:
134
+ enabled: true
135
+ ignore: []
136
+
137
+ ImgLazyLoading:
138
+ enabled: true
139
+ ignore: []
140
+
141
+ HtmlParsingError:
142
+ enabled: true
143
+ ignore: []
144
+
145
+ AssetSizeAppBlockJavaScript:
146
+ enabled: true
147
+ ignore: []
148
+ threshold_in_bytes: 10_000
149
+
150
+ AssetSizeAppBlockCSS:
151
+ enabled: true
152
+ ignore: []
153
+ threshold_in_bytes: 100_000
@@ -0,0 +1,52 @@
1
+ # Prevent Large CSS bundles (`AssetSizeAppBlockCSS`)
2
+
3
+ This rule exists to prevent large CSS bundles from being included via Theme App Extensions (for speed).
4
+
5
+ ## Check Details
6
+
7
+ This rule disallows the use of too much CSS in themes, as configured by `threshold_in_bytes`.
8
+
9
+ :-1: Examples of **incorrect** code for this check:
10
+ ```liquid
11
+ <!-- Here, assets/app.css is **greater** than `threshold_in_bytes` compressed. -->
12
+ {% schema %}
13
+ {
14
+ ...
15
+ "stylesheet": "app.css"
16
+ }
17
+ {% endschema %}
18
+ ```
19
+
20
+ ## Check Options
21
+
22
+ The default configuration is the following:
23
+
24
+ ```yaml
25
+ AssetSizeAppBlockCSS:
26
+ enabled: false
27
+ threshold_in_bytes: 100_000
28
+ ```
29
+
30
+ ### `threshold_in_bytes`
31
+
32
+ The `threshold_in_bytes` option (default: `100_000`) determines the maximum allowed compressed size in bytes that a single CSS file can take.
33
+
34
+ This includes theme and remote stylesheets.
35
+
36
+ ## When Not To Use It
37
+
38
+ This rule should not be disabled locally since the check will be enforced when
39
+ promoting new versions of the extension.
40
+
41
+ ## Version
42
+
43
+ This check has been introduced in 1.1.0
44
+
45
+ ## Resources
46
+
47
+ - [The Performance Inequality Gap](https://infrequently.org/2021/03/the-performance-inequality-gap/)
48
+ - [Rule Source][codesource]
49
+ - [Documentation Source][docsource]
50
+
51
+ [codesource]: /lib/theme_check/checks/asset_size_app_block_css.rb
52
+ [docsource]: /docs/checks/asset_size_app_block_css.md
@@ -0,0 +1,57 @@
1
+ # Prevent Abuse on Server Rendered App Blocks (`AssetSizeAppBlockJavaScript`)
2
+
3
+ For server rendered app blocks, it is an anti-pattern to execute large JavaScript bundles on every page load
4
+
5
+ This doesn't mean they don't have a reason to exist. For instance, chat widgets are mini applications embedded inside web pages. Designing such an app with server rendered updates would be absurd. However, if only 10% of the users interact with the chat widget, the other 90% should not have to execute the entire bundle on every page load.
6
+
7
+ The natural solution to this problem is to implement the chat widget using the [Import on Interaction Pattern][ioip].
8
+
9
+ ## Check Details
10
+
11
+ This rule disallows the use of block JavaScript files and external scripts to have a compressed size greater than a configured `threshold_in_bytes`.
12
+
13
+ :-1: Examples of **incorrect** code for this check:
14
+ ```liquid
15
+ <!-- Here assets/chat-widget.js is more than 10KB gzipped. -->
16
+ {% schema %}
17
+ {
18
+ ...
19
+ "javascript": "chat-widget.js"
20
+ }
21
+ {% endschema %}
22
+ ```
23
+
24
+ ## Check Options
25
+
26
+ The default configuration is the following:
27
+
28
+ ```yaml
29
+ AssetSizeAppBlockJavaScript:
30
+ enabled: true
31
+ threshold_in_bytes: 10000
32
+ ```
33
+
34
+ ### `threshold_in_bytes`
35
+
36
+ The `threshold_in_bytes` option (default: `10000`) determines the maximum allowed compressed size in bytes that a single JavaScript file can take.
37
+
38
+ This includes theme and remote scripts.
39
+
40
+ ## When Not To Use It
41
+
42
+ This rule should not be disabled locally since the check will be enforced when
43
+ promoting new versions of the extension.
44
+
45
+ ## Version
46
+
47
+ This check has been introduced in 1.1.0
48
+
49
+ ## Resources
50
+
51
+ - [The Import On Interaction Pattern][ioip]
52
+ - [Rule Source][codesource]
53
+ - [Documentation Source][docsource]
54
+
55
+ [ioip]: https://addyosmani.com/blog/import-on-interaction/
56
+ [codesource]: /lib/theme_check/checks/asset_size_app_block_javascript.rb
57
+ [docsource]: /docs/checks/asset_size_app_block_javascript.md
data/lib/theme_check.rb CHANGED
@@ -4,6 +4,7 @@ require "liquid"
4
4
  require_relative "theme_check/version"
5
5
  require_relative "theme_check/bug"
6
6
  require_relative "theme_check/exceptions"
7
+ require_relative "theme_check/theme_file"
7
8
  require_relative "theme_check/analyzer"
8
9
  require_relative "theme_check/check"
9
10
  require_relative "theme_check/checks_tracking"
@@ -45,3 +46,17 @@ Dir[__dir__ + "/theme_check/checks/*.rb"].each { |file| require file }
45
46
  # UTF-8 is the default internal and external encoding, like in Rails & Shopify.
46
47
  Encoding.default_external = Encoding::UTF_8
47
48
  Encoding.default_internal = Encoding::UTF_8
49
+
50
+ module ThemeCheck
51
+ def self.with_liquid_c_disabled
52
+ if defined?(Liquid::C)
53
+ was_enabled = Liquid::C.enabled
54
+ Liquid::C.enabled = false if was_enabled
55
+ end
56
+ yield
57
+ ensure
58
+ if defined?(Liquid::C) && was_enabled
59
+ Liquid::C.enabled = true
60
+ end
61
+ end
62
+ end
@@ -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
- @theme.liquid.each do |template|
38
- liquid_visitor.visit_template(template)
39
- html_visitor.visit_template(template)
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
- # Call all checks that run on the whole theme
51
- liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
52
- html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
53
- @theme.liquid.each do |template|
54
- liquid_visitor.visit_template(template)
55
- html_visitor.visit_template(template)
56
- end
57
- @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
58
-
59
- # Call checks that run on a single files, only on specified file
60
- liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
61
- html_visitor = HtmlVisitor.new(@html_checks.single_file)
62
- files.each do |file|
63
- if file.liquid?
64
- liquid_visitor.visit_template(file)
65
- html_visitor.visit_template(file)
66
- elsif file.json?
67
- @json_checks.single_file.call(:on_file, file)
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
- @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?
@@ -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
@@ -10,10 +10,11 @@ module ThemeCheck
10
10
  def initialize
11
11
  @path = "."
12
12
  @command = :check
13
- @only_categories = []
13
+ @include_categories = []
14
14
  @exclude_categories = []
15
15
  @auto_correct = false
16
16
  @config_path = nil
17
+ @fail_level = :error
17
18
  end
18
19
 
19
20
  def option_parser(parser = OptionParser.new, help: true)
@@ -25,20 +26,27 @@ module ThemeCheck
25
26
  @option_parser.separator("Basic Options:")
26
27
  @option_parser.on(
27
28
  "-C", "--config PATH",
28
- "Use the config provided, overriding .theme-check.yml if present"
29
+ "Use the config provided, overriding .theme-check.yml if present",
30
+ "Use :theme_app_extension to use default checks for theme app extensions"
29
31
  ) { |path| @config_path = path }
30
32
  @option_parser.on(
31
- "-c", "--category CATEGORY",
32
- "Only run this category of checks"
33
- ) { |category| @only_categories << category.to_sym }
33
+ "-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
34
+ "Runs checks matching all categories when specified more than once"
35
+ ) { |category| @include_categories << category.to_sym }
34
36
  @option_parser.on(
35
- "-x", "--exclude-category CATEGORY",
36
- "Exclude this category of checks"
37
+ "-x", "--exclude-category CATEGORY", Check::CATEGORIES, "Exclude this category of checks",
38
+ "Excludes checks matching any category when specified more than once"
37
39
  ) { |category| @exclude_categories << category.to_sym }
38
40
  @option_parser.on(
39
41
  "-a", "--auto-correct",
40
42
  "Automatically fix offenses"
41
43
  ) { @auto_correct = true }
44
+ @option_parser.on(
45
+ "--fail-level SEVERITY", Check::SEVERITIES,
46
+ "Minimum severity (error|suggestion|style) for exit with error code"
47
+ ) do |severity|
48
+ @fail_level = severity.to_sym
49
+ end
42
50
 
43
51
  @option_parser.separator("")
44
52
  @option_parser.separator("Miscellaneous:")
@@ -77,6 +85,8 @@ module ThemeCheck
77
85
 
78
86
  def parse(argv)
79
87
  @path = option_parser.parse(argv).first || "."
88
+ rescue OptionParser::InvalidArgument => e
89
+ abort(e.message)
80
90
  end
81
91
 
82
92
  def run!
@@ -84,13 +94,13 @@ module ThemeCheck
84
94
  @config = if @config_path
85
95
  ThemeCheck::Config.new(
86
96
  root: @path,
87
- configuration: ThemeCheck::Config.load_file(@config_path)
97
+ configuration: ThemeCheck::Config.load_config(@config_path)
88
98
  )
89
99
  else
90
100
  ThemeCheck::Config.from_path(@path)
91
101
  end
92
- @config.only_categories = @only_categories
93
- @config.exclude_categories = @exclude_categories
102
+ @config.include_categories = @include_categories unless @include_categories.empty?
103
+ @config.exclude_categories = @exclude_categories unless @exclude_categories.empty?
94
104
  @config.auto_correct = @auto_correct
95
105
  end
96
106
 
@@ -99,12 +109,16 @@ module ThemeCheck
99
109
 
100
110
  def run
101
111
  run!
112
+ exit(0)
102
113
  rescue Abort => e
103
114
  if e.message.empty?
104
115
  exit(1)
105
116
  else
106
117
  abort(e.message)
107
118
  end
119
+ rescue ThemeCheckError => e
120
+ STDERR.puts(e.message)
121
+ exit(2)
108
122
  end
109
123
 
110
124
  def self.parse_and_run!(argv)
@@ -130,11 +144,16 @@ module ThemeCheck
130
144
  def init
131
145
  dotfile_path = ThemeCheck::Config.find(@path)
132
146
  if dotfile_path.nil?
133
- File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config::DEFAULT_CONFIG))
147
+ config_name = if @config_path && @config_path[0] == ":"
148
+ "#{@config_path[1..]}.yml"
149
+ else
150
+ "default.yml"
151
+ end
152
+ File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config.bundled_config_path(config_name)))
134
153
 
135
154
  puts "Writing new #{ThemeCheck::Config::DOTFILE} to #{@path}"
136
155
  else
137
- raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}."
156
+ raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}"
138
157
  end
139
158
  end
140
159
 
@@ -157,7 +176,9 @@ module ThemeCheck
157
176
  analyzer.analyze_theme
158
177
  analyzer.correct_offenses
159
178
  ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
160
- raise Abort, "" if analyzer.uncorrectable_offenses.any?
179
+ raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
180
+ offense.check.severity_value <= Check.severity_value(@fail_level)
181
+ end
161
182
  end
162
183
  end
163
184
  end
@@ -3,11 +3,11 @@
3
3
  module ThemeCheck
4
4
  class Config
5
5
  DOTFILE = '.theme-check.yml'
6
- DEFAULT_CONFIG = "#{__dir__}/../../config/default.yml"
6
+ BUNDLED_CONFIGS_DIR = "#{__dir__}/../../config"
7
7
  BOOLEAN = [true, false]
8
8
 
9
9
  attr_reader :root
10
- attr_accessor :only_categories, :exclude_categories, :auto_correct
10
+ attr_accessor :auto_correct
11
11
 
12
12
  class << self
13
13
  attr_reader :last_loaded_config
@@ -42,18 +42,46 @@ module ThemeCheck
42
42
  YAML.load_file(absolute_path)
43
43
  end
44
44
 
45
+ def bundled_config_path(name)
46
+ "#{BUNDLED_CONFIGS_DIR}/#{name}"
47
+ end
48
+
49
+ def load_bundled_config(name)
50
+ load_file(bundled_config_path(name))
51
+ end
52
+
53
+ def load_config(path)
54
+ if path[0] == ":"
55
+ load_bundled_config("#{path[1..]}.yml")
56
+ elsif path.is_a?(Symbol)
57
+ load_bundled_config("#{path}.yml")
58
+ else
59
+ load_file(path)
60
+ end
61
+ end
62
+
45
63
  def default
46
- @default ||= load_file(DEFAULT_CONFIG)
64
+ @default ||= load_config(":default")
47
65
  end
48
66
  end
49
67
 
50
68
  def initialize(root: nil, configuration: nil, should_resolve_requires: true)
51
69
  @configuration = if configuration
70
+ # TODO: Do we need to handle extends here? What base configuration
71
+ # should we validate against once Theme App Extensions has its own
72
+ # checks? :all?
52
73
  validate_configuration(configuration)
53
74
  else
54
75
  {}
55
76
  end
56
- merge_with_default_configuration!(@configuration)
77
+
78
+ # Follow extends
79
+ extends = @configuration["extends"] || ":default"
80
+ while extends
81
+ extended_configuration = self.class.load_config(extends)
82
+ extends = extended_configuration["extends"]
83
+ @configuration = merge_configurations!(@configuration, extended_configuration)
84
+ end
57
85
 
58
86
  @root = if root && @configuration.key?("root")
59
87
  Pathname.new(root).join(@configuration["root"])
@@ -61,8 +89,6 @@ module ThemeCheck
61
89
  Pathname.new(root)
62
90
  end
63
91
 
64
- @only_categories = []
65
- @exclude_categories = []
66
92
  @auto_correct = false
67
93
 
68
94
  resolve_requires if @root && should_resolve_requires
@@ -87,16 +113,18 @@ module ThemeCheck
87
113
  check_class = ThemeCheck.const_get(check_name)
88
114
 
89
115
  next if check_class.categories.any? { |category| exclude_categories.include?(category) }
90
- next if only_categories.any? && check_class.categories.none? { |category| only_categories.include?(category) }
116
+ next if include_categories.any? && !include_categories.all? { |category| check_class.categories.include?(category) }
91
117
 
92
118
  options_for_check = options.transform_keys(&:to_sym)
93
119
  options_for_check.delete(:enabled)
120
+ severity = options_for_check.delete(:severity)
94
121
  ignored_patterns = options_for_check.delete(:ignore) || []
95
122
  check = if options_for_check.empty?
96
123
  check_class.new
97
124
  else
98
125
  check_class.new(**options_for_check)
99
126
  end
127
+ check.severity = severity.to_sym if severity
100
128
  check.ignored_patterns = ignored_patterns
101
129
  check.options = options_for_check
102
130
  check
@@ -107,6 +135,22 @@ module ThemeCheck
107
135
  self["ignore"] || []
108
136
  end
109
137
 
138
+ def include_categories
139
+ self["include_categories"] || []
140
+ end
141
+
142
+ def include_categories=(categories)
143
+ @configuration["include_categories"] = categories
144
+ end
145
+
146
+ def exclude_categories
147
+ self["exclude_categories"] || []
148
+ end
149
+
150
+ def exclude_categories=(categories)
151
+ @configuration["exclude_categories"] = categories
152
+ end
153
+
110
154
  private
111
155
 
112
156
  def check_name?(name)
@@ -133,6 +177,8 @@ module ThemeCheck
133
177
  else
134
178
  warn("bad configuration type for #{name}: expected a Hash, got #{value.inspect}")
135
179
  end
180
+ elsif key == "severity"
181
+ valid_configuration[key] = value
136
182
  elsif default.nil?
137
183
  warn("unknown configuration: #{name}")
138
184
  elsif BOOLEAN.include?(default) && !BOOLEAN.include?(value)
@@ -147,13 +193,13 @@ module ThemeCheck
147
193
  valid_configuration
148
194
  end
149
195
 
150
- def merge_with_default_configuration!(configuration, default_configuration = self.class.default)
151
- default_configuration.each do |key, default|
196
+ def merge_configurations!(configuration, extended_configuration)
197
+ extended_configuration.each do |key, default|
152
198
  value = configuration[key]
153
199
 
154
200
  case value
155
201
  when Hash
156
- merge_with_default_configuration!(value, default)
202
+ merge_configurations!(value, default)
157
203
  when nil
158
204
  configuration[key] = default
159
205
  end
@@ -1,32 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
  require "net/http"
3
3
 
4
- TIMEOUT_EXCEPTIONS = [
5
- Net::ReadTimeout,
6
- Net::OpenTimeout,
7
- Net::WriteTimeout,
8
- Errno::ETIMEDOUT,
9
- Timeout::Error,
10
- ]
4
+ module ThemeCheck
5
+ TIMEOUT_EXCEPTIONS = [
6
+ Net::ReadTimeout,
7
+ Net::OpenTimeout,
8
+ Net::WriteTimeout,
9
+ Errno::ETIMEDOUT,
10
+ Timeout::Error,
11
+ ]
11
12
 
12
- CONNECTION_EXCEPTIONS = [
13
- IOError,
14
- EOFError,
15
- SocketError,
16
- Errno::EINVAL,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- Errno::ECONNREFUSED,
21
- Errno::EAGAIN,
22
- Errno::EHOSTUNREACH,
23
- Errno::ENETUNREACH,
24
- ]
13
+ CONNECTION_EXCEPTIONS = [
14
+ IOError,
15
+ EOFError,
16
+ SocketError,
17
+ Errno::EINVAL,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::ECONNREFUSED,
22
+ Errno::EAGAIN,
23
+ Errno::EHOSTUNREACH,
24
+ Errno::ENETUNREACH,
25
+ ]
25
26
 
26
- NET_HTTP_EXCEPTIONS = [
27
- Net::HTTPBadResponse,
28
- Net::HTTPHeaderSyntaxError,
29
- Net::ProtocolError,
30
- *TIMEOUT_EXCEPTIONS,
31
- *CONNECTION_EXCEPTIONS,
32
- ]
27
+ NET_HTTP_EXCEPTIONS = [
28
+ Net::HTTPBadResponse,
29
+ Net::HTTPHeaderSyntaxError,
30
+ Net::ProtocolError,
31
+ *TIMEOUT_EXCEPTIONS,
32
+ *CONNECTION_EXCEPTIONS,
33
+ ]
34
+ end
@@ -1,29 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
- require "pathname"
4
3
 
5
4
  module ThemeCheck
6
- class JsonFile
5
+ class JsonFile < 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
  @parser_error = nil
13
11
  end
14
12
 
15
- def path
16
- @storage.path(@relative_path)
17
- end
18
-
19
- def relative_path
20
- @relative_pathname ||= Pathname.new(@relative_path)
21
- end
22
-
23
- def source
24
- @source ||= @storage.read(@relative_path)
25
- end
26
-
27
13
  def content
28
14
  load!
29
15
  @content
@@ -34,23 +20,10 @@ module ThemeCheck
34
20
  @parser_error
35
21
  end
36
22
 
37
- def name
38
- relative_path.sub_ext('').to_s
39
- end
40
-
41
23
  def json?
42
24
  true
43
25
  end
44
26
 
45
- def liquid?
46
- false
47
- end
48
-
49
- def ==(other)
50
- other.is_a?(JsonFile) && relative_path == other.relative_path
51
- end
52
- alias_method :eql?, :==
53
-
54
27
  private
55
28
 
56
29
  def load!
@@ -10,5 +10,13 @@ module ThemeCheck
10
10
  ^\s*render\s+'(?<partial>[^']*)'|
11
11
  ^\s*render\s+"(?<partial>[^"]*)"
12
12
  }mix
13
+ ASSET_INCLUDE = %r{
14
+ \{\%-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
15
+ \{\%-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
16
+
17
+ # in liquid tags the whole line is white space until the asset partial
18
+ ^\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
19
+ ^\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
20
+ }mix
13
21
  end
14
22
  end
@@ -13,7 +13,7 @@ module ThemeCheck
13
13
  def document_links(relative_path)
14
14
  buffer = @storage.read(relative_path)
15
15
  return [] unless buffer
16
- matches(buffer, PARTIAL_RENDER).map do |match|
16
+ snippet_matches = matches(buffer, PARTIAL_RENDER).map do |match|
17
17
  start_line, start_character = from_index_to_row_column(
18
18
  buffer,
19
19
  match.begin(:partial),
@@ -25,7 +25,7 @@ module ThemeCheck
25
25
  )
26
26
 
27
27
  {
28
- target: link(match[:partial]),
28
+ target: snippet_link(match[:partial]),
29
29
  range: {
30
30
  start: {
31
31
  line: start_line,
@@ -38,10 +38,46 @@ module ThemeCheck
38
38
  },
39
39
  }
40
40
  end
41
+ asset_matches = matches(buffer, ASSET_INCLUDE).map do |match|
42
+ start_line, start_character = from_index_to_row_column(
43
+ buffer,
44
+ match.begin(:partial),
45
+ )
46
+
47
+ end_line, end_character = from_index_to_row_column(
48
+ buffer,
49
+ match.end(:partial)
50
+ )
51
+
52
+ {
53
+ target: asset_link(match[:partial]),
54
+ range: {
55
+ start: {
56
+ line: start_line,
57
+ character: start_character,
58
+ },
59
+ end: {
60
+ line: end_line,
61
+ character: end_character,
62
+ },
63
+ },
64
+ }
65
+ end
66
+ snippet_matches + asset_matches
67
+ end
68
+
69
+ def snippet_link(partial)
70
+ file_link('snippets', partial, '.liquid')
41
71
  end
42
72
 
43
- def link(partial)
44
- "file://#{@storage.path('snippets/' + partial + '.liquid')}"
73
+ def asset_link(partial)
74
+ file_link('assets', partial, '')
75
+ end
76
+
77
+ private
78
+
79
+ def file_link(directory, partial, extension)
80
+ "file://#{@storage.path(directory + '/' + partial + extension)}"
45
81
  end
46
82
  end
47
83
  end
@@ -169,14 +169,31 @@ module ThemeCheck
169
169
 
170
170
  class Stylesheet < Liquid::Raw; end
171
171
 
172
- Liquid::Template.register_tag('form', Form)
173
- Liquid::Template.register_tag('layout', Layout)
174
- Liquid::Template.register_tag('render', Render)
175
- Liquid::Template.register_tag('paginate', Paginate)
176
- Liquid::Template.register_tag('section', Section)
177
- Liquid::Template.register_tag('style', Style)
178
- Liquid::Template.register_tag('schema', Schema)
179
- Liquid::Template.register_tag('javascript', Javascript)
180
- Liquid::Template.register_tag('stylesheet', Stylesheet)
172
+ class << self
173
+ attr_writer :register_tags
174
+
175
+ def register_tags?
176
+ @register_tags
177
+ end
178
+
179
+ def register_tag(name, klass)
180
+ Liquid::Template.register_tag(name, klass)
181
+ end
182
+
183
+ def register_tags!
184
+ return if !register_tags? || (defined?(@registered_tags) && @registered_tags)
185
+ @registered_tags = true
186
+ register_tag('form', Form)
187
+ register_tag('layout', Layout)
188
+ register_tag('render', Render)
189
+ register_tag('paginate', Paginate)
190
+ register_tag('section', Section)
191
+ register_tag('style', Style)
192
+ register_tag('schema', Schema)
193
+ register_tag('javascript', Javascript)
194
+ register_tag('stylesheet', Stylesheet)
195
+ end
196
+ end
197
+ self.register_tags = true
181
198
  end
182
199
  end
@@ -1,25 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require "pathname"
3
2
 
4
3
  module ThemeCheck
5
- class Template
6
- def initialize(relative_path, storage)
7
- @storage = storage
8
- @relative_path = relative_path
9
- end
10
-
11
- def path
12
- @storage.path(@relative_path)
13
- end
14
-
15
- def relative_path
16
- @relative_pathname ||= Pathname.new(@relative_path)
17
- end
18
-
19
- def source
20
- @source ||= @storage.read(@relative_path)
21
- end
22
-
4
+ class Template < ThemeFile
23
5
  def write
24
6
  content = updated_content
25
7
  if source != content
@@ -28,14 +10,6 @@ module ThemeCheck
28
10
  end
29
11
  end
30
12
 
31
- def name
32
- relative_path.sub_ext('').to_s
33
- end
34
-
35
- def json?
36
- false
37
- end
38
-
39
13
  def liquid?
40
14
  true
41
15
  end
@@ -88,16 +62,13 @@ module ThemeCheck
88
62
  parse.root
89
63
  end
90
64
 
91
- def ==(other)
92
- other.is_a?(Template) && relative_path == other.relative_path
93
- end
94
- alias_method :eql?, :==
95
-
96
65
  def self.parse(source)
66
+ Tags.register_tags!
97
67
  Liquid::Template.parse(
98
68
  source,
99
69
  line_numbers: true,
100
70
  error_mode: :warn,
71
+ disable_liquid_c_nodes: true,
101
72
  )
102
73
  end
103
74
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+
4
+ module ThemeCheck
5
+ class ThemeFile
6
+ def initialize(relative_path, storage)
7
+ @relative_path = relative_path
8
+ @storage = storage
9
+ end
10
+
11
+ def path
12
+ @storage.path(@relative_path)
13
+ end
14
+
15
+ def relative_path
16
+ @relative_pathname ||= Pathname.new(@relative_path)
17
+ end
18
+
19
+ def name
20
+ relative_path.sub_ext('').to_s
21
+ end
22
+
23
+ def source
24
+ @source ||= @storage.read(@relative_path)
25
+ end
26
+
27
+ def json?
28
+ false
29
+ end
30
+
31
+ def liquid?
32
+ false
33
+ end
34
+
35
+ def ==(other)
36
+ other.is_a?(self.class) && relative_path == other.relative_path
37
+ end
38
+ alias_method :eql?, :==
39
+ end
40
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-28 00:00:00.000000000 Z
11
+ date: 2021-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -62,6 +62,8 @@ files:
62
62
  - Rakefile
63
63
  - bin/liquid-server
64
64
  - config/default.yml
65
+ - config/nothing.yml
66
+ - config/theme_app_extension.yml
65
67
  - data/shopify_liquid/deprecated_filters.yml
66
68
  - data/shopify_liquid/filters.yml
67
69
  - data/shopify_liquid/objects.yml
@@ -74,6 +76,8 @@ files:
74
76
  - docs/api/json_check.md
75
77
  - docs/api/liquid_check.md
76
78
  - docs/checks/TEMPLATE.md.erb
79
+ - docs/checks/asset_size_app_block_css.md
80
+ - docs/checks/asset_size_app_block_javascript.md
77
81
  - docs/checks/asset_size_css.md
78
82
  - docs/checks/asset_size_css_stylesheet_tag.md
79
83
  - docs/checks/asset_size_javascript.md
@@ -120,6 +124,8 @@ files:
120
124
  - lib/theme_check/check.rb
121
125
  - lib/theme_check/checks.rb
122
126
  - lib/theme_check/checks/TEMPLATE.rb.erb
127
+ - lib/theme_check/checks/asset_size_app_block_css.rb
128
+ - lib/theme_check/checks/asset_size_app_block_javascript.rb
123
129
  - lib/theme_check/checks/asset_size_css.rb
124
130
  - lib/theme_check/checks/asset_size_css_stylesheet_tag.rb
125
131
  - lib/theme_check/checks/asset_size_javascript.rb
@@ -209,6 +215,7 @@ files:
209
215
  - lib/theme_check/tags.rb
210
216
  - lib/theme_check/template.rb
211
217
  - lib/theme_check/theme.rb
218
+ - lib/theme_check/theme_file.rb
212
219
  - lib/theme_check/version.rb
213
220
  - lib/theme_check/visitor.rb
214
221
  - packaging/homebrew/theme_check.base.rb