shopify-cli 2.14.0 → 2.15.2

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +1 -1
  3. data/.github/workflows/stale.yml +46 -0
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +39 -7
  7. data/Rakefile +40 -0
  8. data/ext/shopify-extensions/version +1 -1
  9. data/lib/project_types/extension/forms/questions/ask_template.rb +5 -8
  10. data/lib/project_types/extension/messages/messages.rb +11 -1
  11. data/lib/project_types/extension/models/development_server_requirements.rb +13 -7
  12. data/lib/project_types/extension/models/server_config/root.rb +2 -0
  13. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +13 -0
  14. data/lib/project_types/script/config/extension_points.yml +18 -0
  15. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  16. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  17. data/lib/project_types/script/messages/messages.rb +3 -0
  18. data/lib/project_types/script/ui/error_handler.rb +11 -0
  19. data/lib/project_types/theme/commands/pull.rb +2 -2
  20. data/lib/project_types/theme/commands/push.rb +2 -2
  21. data/lib/project_types/theme/commands/serve.rb +1 -0
  22. data/lib/project_types/theme/conversions/base_glob.rb +20 -5
  23. data/lib/project_types/theme/messages/messages.rb +47 -8
  24. data/lib/shopify_cli/changelog.rb +76 -0
  25. data/lib/shopify_cli/command.rb +8 -7
  26. data/lib/shopify_cli/environment.rb +19 -11
  27. data/lib/shopify_cli/git.rb +36 -0
  28. data/lib/shopify_cli/messages/messages.rb +16 -9
  29. data/lib/shopify_cli/release.rb +194 -0
  30. data/lib/shopify_cli/sed.rb +19 -0
  31. data/lib/shopify_cli/services/app/create/node_service.rb +2 -14
  32. data/lib/shopify_cli/services/app/create/php_service.rb +1 -6
  33. data/lib/shopify_cli/services/app/create/rails_service.rb +4 -12
  34. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  35. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  36. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  37. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  38. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  39. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +34 -0
  40. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  41. data/lib/shopify_cli/theme/dev_server/watcher.rb +1 -1
  42. data/lib/shopify_cli/theme/dev_server.rb +15 -3
  43. data/lib/shopify_cli/theme/file.rb +15 -4
  44. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  45. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  46. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  47. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  48. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  49. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  50. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  51. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  52. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  53. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  54. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  55. data/lib/shopify_cli/theme/syncer.rb +79 -63
  56. data/lib/shopify_cli/theme/theme.rb +12 -4
  57. data/lib/shopify_cli/theme/theme_admin_api.rb +24 -23
  58. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  59. data/lib/shopify_cli/thread_pool.rb +15 -3
  60. data/lib/shopify_cli/tunnel.rb +9 -0
  61. data/lib/shopify_cli/version.rb +1 -1
  62. data/shopify-cli.gemspec +3 -1
  63. metadata +21 -4
  64. data/lib/project_types/rails/ruby.rb +0 -17
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "forms/apply_to_all"
4
+ require_relative "forms/select_update_strategy"
5
+
6
+ module ShopifyCLI
7
+ module Theme
8
+ class Syncer
9
+ module JsonUpdateHandler
10
+ def enqueue_json_updates(files)
11
+ # Some files must be uploaded after the other ones
12
+ delayed_files = [
13
+ theme["config/settings_schema.json"],
14
+ theme["config/settings_data.json"],
15
+ ]
16
+
17
+ # Update remote JSON files and delays `delayed_files` update
18
+ files = files
19
+ .select { |file| !ignore_file?(file) && file.exist? && checksums.file_has_changed?(file) }
20
+ .sort_by { |file| delayed_files.include?(file) ? 1 : 0 }
21
+
22
+ if overwrite_json?
23
+ enqueue_updates(files)
24
+ else
25
+ # Handle conflicts when JSON files cannot be overwritten
26
+ handle_update_conflicts(files)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def handle_update_conflicts(files)
33
+ to_get = []
34
+ to_delete = []
35
+ to_update = []
36
+ to_union_merge = []
37
+
38
+ apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
39
+
40
+ files.each do |file|
41
+ update_strategy = apply_to_all.value || ask_update_strategy(file)
42
+ apply_to_all.apply?(update_strategy)
43
+
44
+ case update_strategy
45
+ when :keep_remote
46
+ if file_exist_remotely?(file)
47
+ to_get << file
48
+ else
49
+ delete_locally(file)
50
+ end
51
+ when :keep_local
52
+ to_update << file
53
+ when :union_merge
54
+ if file_exist_remotely?(file)
55
+ to_union_merge << file
56
+ else
57
+ to_update << file
58
+ end
59
+ end
60
+ end
61
+
62
+ enqueue_get(to_get)
63
+ enqueue_deletes(to_delete)
64
+ enqueue_updates(to_update)
65
+ enqueue_union_merges(to_union_merge)
66
+ end
67
+
68
+ def file_exist_remotely?(file)
69
+ !checksums[file.relative_path].nil?
70
+ end
71
+
72
+ def delete_locally(file)
73
+ ::File.delete(file.absolute_path)
74
+ end
75
+
76
+ def ask_update_strategy(file)
77
+ Forms::SelectUpdateStrategy.ask(@ctx, [], file: file).strategy
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Merger
9
+ class << self
10
+ ##
11
+ # Merge `theme_file` with the `new_content` by relying on the union merge
12
+ #
13
+ def union_merge(theme_file, new_content)
14
+ git_merge(theme_file, new_content, ["--union", "-p"])
15
+ end
16
+
17
+ private
18
+
19
+ ##
20
+ # Merge theme file (`ShopifyCLI::Theme::File`) with a new content (String),
21
+ # by creating a temporary file based on the `new_content`.
22
+ #
23
+ def git_merge(theme_file, new_content, opts)
24
+ remote_file = create_tmp_file(tmp_file_name(theme_file), new_content)
25
+ empty_file = create_tmp_file("empty")
26
+
27
+ ShopifyCLI::Git.merge_file(
28
+ theme_file.absolute_path,
29
+ empty_file.path,
30
+ remote_file.path,
31
+ opts
32
+ )
33
+ ensure
34
+ # Remove temporary files on Windows as well
35
+ remote_file.close!
36
+ empty_file.close!
37
+ end
38
+
39
+ def create_tmp_file(basename, content = "")
40
+ tmp_file = Tempfile.new(basename)
41
+ tmp_file.write(content)
42
+ tmp_file.close # Make it ready to merge
43
+ tmp_file
44
+ end
45
+
46
+ def tmp_file_name(ref_file)
47
+ "shopify-cli-merge-#{ref_file.name(".*")}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -35,7 +35,7 @@ module ShopifyCLI
35
35
  end
36
36
 
37
37
  def file_path
38
- file&.relative_path.to_s
38
+ file&.relative_path
39
39
  end
40
40
 
41
41
  private
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "thread"
3
4
  require "json"
4
5
  require "base64"
5
6
  require "forwardable"
6
7
 
8
+ require_relative "syncer/checksums"
7
9
  require_relative "syncer/error_reporter"
8
- require_relative "syncer/standard_reporter"
10
+ require_relative "syncer/ignore_helper"
11
+ require_relative "syncer/json_delete_handler"
12
+ require_relative "syncer/json_update_handler"
13
+ require_relative "syncer/merger"
9
14
  require_relative "syncer/operation"
15
+ require_relative "syncer/standard_reporter"
10
16
  require_relative "theme_admin_api"
11
17
 
12
18
  module ShopifyCLI
@@ -14,36 +20,46 @@ module ShopifyCLI
14
20
  class Syncer
15
21
  extend Forwardable
16
22
 
17
- attr_reader :checksums
18
- attr_reader :checksums_mutex
19
- attr_accessor :include_filter
20
- attr_accessor :ignore_filter
23
+ include IgnoreHelper
24
+ include JsonDeleteHandler
25
+ include JsonUpdateHandler
26
+
27
+ QUEUEABLE_METHODS = [
28
+ :get, # - Updates the local file with the remote file content
29
+ :update, # - Updates the remote file with the local file content
30
+ :delete, # - Deletes the remote file
31
+ :union_merge, # - Union merges the local file content with the remote file content
32
+ ]
33
+
34
+ attr_reader :theme, :checksums, :error_checksums
35
+ attr_accessor :include_filter, :ignore_filter
21
36
 
22
37
  def_delegators :@error_reporter, :has_any_error?
23
38
 
24
- def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil)
39
+ def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true)
25
40
  @ctx = ctx
26
41
  @theme = theme
27
42
  @include_filter = include_filter
28
43
  @ignore_filter = ignore_filter
44
+ @overwrite_json = overwrite_json
29
45
  @error_reporter = ErrorReporter.new(ctx)
30
46
  @standard_reporter = StandardReporter.new(ctx)
31
47
  @reporters = [@error_reporter, @standard_reporter]
32
48
 
33
49
  # Queue of `Operation`s waiting to be picked up from a thread for processing.
34
50
  @queue = Queue.new
51
+
35
52
  # `Operation`s will be removed from this Array completed.
36
53
  @pending = []
54
+
37
55
  # Thread making the API requests.
38
56
  @threads = []
57
+
39
58
  # Mutex used to pause all threads when backing-off when hitting API rate limits
40
59
  @backoff_mutex = Mutex.new
41
60
 
42
- # Mutex used to coordinate changes in the checksums (shared accross all threads)
43
- @checksums_mutex = Mutex.new
44
-
45
61
  # Latest theme assets checksums. Updated on each upload.
46
- @checksums = {}
62
+ @checksums = Checksums.new(theme)
47
63
 
48
64
  # Checksums of assets with errors.
49
65
  @error_checksums = []
@@ -73,6 +89,10 @@ module ShopifyCLI
73
89
  files.each { |file| enqueue(:delete, file) }
74
90
  end
75
91
 
92
+ def enqueue_union_merges(files)
93
+ files.each { |file| enqueue(:union_merge, file) }
94
+ end
95
+
76
96
  def size
77
97
  @pending.size
78
98
  end
@@ -86,7 +106,11 @@ module ShopifyCLI
86
106
  end
87
107
 
88
108
  def remote_file?(file)
89
- checksums.key?(@theme[file].relative_path.to_s)
109
+ checksums.has?(file)
110
+ end
111
+
112
+ def broken_file?(file)
113
+ error_checksums.include?(checksums[file.relative_path])
90
114
  end
91
115
 
92
116
  def wait!
@@ -135,20 +159,18 @@ module ShopifyCLI
135
159
  fetch_checksums!
136
160
 
137
161
  if delete
138
- # Delete remote files not present locally
139
- removed_files = checksums.keys - @theme.theme_files.map { |file| file.relative_path.to_s }
162
+ removed_json_files, removed_files = checksums
163
+ .keys
164
+ .-(@theme.theme_files.map(&:relative_path))
165
+ .map { |file| @theme[file] }
166
+ .partition(&:json?)
167
+
140
168
  enqueue_deletes(removed_files)
169
+ enqueue_json_deletes(removed_json_files)
141
170
  end
142
171
 
143
- # Some files must be uploaded after the other ones
144
- delayed_config_files = [
145
- @theme["config/settings_schema.json"],
146
- @theme["config/settings_data.json"],
147
- ]
148
-
149
172
  enqueue_updates(@theme.liquid_files)
150
- enqueue_updates(@theme.json_files - delayed_config_files)
151
- enqueue_updates(delayed_config_files)
173
+ enqueue_json_updates(@theme.json_files)
152
174
 
153
175
  if delay_low_priority_files
154
176
  # Wait for liquid & JSON files to upload, because those are rendered remotely
@@ -171,7 +193,7 @@ module ShopifyCLI
171
193
  if delete
172
194
  # Delete local files not present remotely
173
195
  missing_files = @theme.theme_files
174
- .reject { |file| checksums.key?(file.relative_path.to_s) }.uniq
196
+ .reject { |file| checksums.has?(file) }.uniq
175
197
  .reject { |file| ignore_file?(file) }
176
198
  missing_files.each do |file|
177
199
  @ctx.debug("rm #{file.relative_path}")
@@ -187,12 +209,13 @@ module ShopifyCLI
187
209
  private
188
210
 
189
211
  def report_error(operation, error_suffix = "")
190
- @error_checksums << @checksums[operation.file_path]
212
+ @error_checksums << checksums[operation.file_path]
191
213
  @error_reporter.report("#{operation.as_error_message}#{error_suffix}")
192
214
  end
193
215
 
194
216
  def enqueue(method, file)
195
217
  raise ArgumentError, "file required" unless file
218
+ raise ArgumentError, "method '#{method}' cannot be queued" unless QUEUEABLE_METHODS.include?(method)
196
219
 
197
220
  operation = Operation.new(@ctx, method, @theme[file])
198
221
 
@@ -204,10 +227,10 @@ module ShopifyCLI
204
227
  return
205
228
  end
206
229
 
207
- if [:update, :get].include?(method) && operation.file.exist? && !file_has_changed?(operation.file)
230
+ if [:update, :get].include?(method) && operation.file.exist?
208
231
  is_fixed = !!@error_checksums.delete(operation.file.checksum)
209
232
  @standard_reporter.report(operation.as_fix_message) if is_fixed
210
- return
233
+ return unless checksums.file_has_changed?(operation.file)
211
234
  end
212
235
 
213
236
  @pending << operation
@@ -236,7 +259,7 @@ module ShopifyCLI
236
259
  end
237
260
 
238
261
  def update(file)
239
- asset = { key: file.relative_path.to_s }
262
+ asset = { key: file.relative_path }
240
263
  if file.text?
241
264
  asset[:value] = file.read
242
265
  else
@@ -253,32 +276,10 @@ module ShopifyCLI
253
276
  response
254
277
  end
255
278
 
256
- def ignore_operation?(operation)
257
- path = operation.file_path
258
- ignore_path?(path)
259
- end
260
-
261
- def ignore_file?(file)
262
- path = file.path
263
- ignore_path?(path)
264
- end
265
-
266
- def ignore_path?(path)
267
- ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
268
- end
269
-
270
- def ignored_by_ignore_filter?(path)
271
- ignore_filter&.ignore?(path)
272
- end
273
-
274
- def ignored_by_include_filter?(path)
275
- !!include_filter && !include_filter.match?(path)
276
- end
277
-
278
279
  def get(file)
279
280
  _status, body, response = api_client.get(
280
281
  path: "themes/#{@theme.id}/assets.json",
281
- query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
282
+ query: URI.encode_www_form("asset[key]" => file.relative_path),
282
283
  )
283
284
 
284
285
  update_checksums(body)
@@ -297,37 +298,48 @@ module ShopifyCLI
297
298
  _status, _body, response = api_client.delete(
298
299
  path: "themes/#{@theme.id}/assets.json",
299
300
  body: JSON.generate(asset: {
300
- key: file.relative_path.to_s,
301
+ key: file.relative_path,
301
302
  })
302
303
  )
303
304
 
304
305
  response
305
306
  end
306
307
 
308
+ def union_merge(file)
309
+ _status, body, response = api_client.get(
310
+ path: "themes/#{@theme.id}/assets.json",
311
+ query: URI.encode_www_form("asset[key]" => file.relative_path),
312
+ )
313
+
314
+ return response unless file.text?
315
+
316
+ remote_content = body.dig("asset", "value")
317
+
318
+ return response if remote_content.nil?
319
+
320
+ content = Merger.union_merge(file, remote_content)
321
+
322
+ file.write(content)
323
+
324
+ enqueue(:update, file)
325
+
326
+ response
327
+ end
328
+
307
329
  def update_checksums(api_response)
308
330
  api_response.values.flatten.each do |asset|
309
331
  next unless asset["key"]
310
- checksums_mutex.synchronize do
311
- @checksums[asset["key"]] = asset["checksum"]
312
- end
313
- end
314
- # Generate .liquid asset files are reported twice in checksum:
315
- # once of generated, once for .liquid. We only keep the .liquid, that's the one we have
316
- # on disk.
317
- checksums_mutex.synchronize do
318
- @checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
332
+ checksums[asset["key"]] = asset["checksum"]
319
333
  end
320
- end
321
334
 
322
- def file_has_changed?(file)
323
- file.checksum != @checksums[file.relative_path.to_s]
335
+ checksums.reject_duplicated_checksums!
324
336
  end
325
337
 
326
338
  def parse_api_errors(exception)
327
339
  parsed_body = JSON.parse(exception&.response&.body)
328
340
  message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
329
341
  # Truncate to first lines
330
- [message].flatten.map { |mess| mess.split("\n", 2).first }
342
+ [message].flatten.map { |m| m.split("\n", 2).first }
331
343
  rescue JSON::ParserError
332
344
  [exception.message]
333
345
  end
@@ -339,6 +351,10 @@ module ShopifyCLI
339
351
  end
340
352
  end
341
353
 
354
+ def overwrite_json?
355
+ @overwrite_json
356
+ end
357
+
342
358
  def backingoff?
343
359
  @backoff_mutex.locked?
344
360
  end
@@ -21,11 +21,11 @@ module ShopifyCLI
21
21
  end
22
22
 
23
23
  def theme_files
24
- glob(["**/*.liquid", "**/*.json", "assets/*"]).uniq
24
+ (glob(["**/*.liquid", "**/*.json"]) + static_asset_files).uniq
25
25
  end
26
26
 
27
27
  def static_asset_files
28
- glob("assets/*").reject(&:liquid?)
28
+ glob("assets/*", raise_on_dir: true).reject(&:liquid?)
29
29
  end
30
30
 
31
31
  def liquid_files
@@ -36,8 +36,11 @@ module ShopifyCLI
36
36
  glob("**/*.json")
37
37
  end
38
38
 
39
- def glob(pattern)
40
- root.glob(pattern).map { |path| File.new(path, root) }
39
+ def glob(pattern, raise_on_dir: false)
40
+ root.glob(pattern).map do |path|
41
+ abort_if_directory!(path) if raise_on_dir
42
+ File.new(path, root)
43
+ end
41
44
  end
42
45
 
43
46
  def theme_file?(file)
@@ -218,6 +221,11 @@ module ShopifyCLI
218
221
 
219
222
  self
220
223
  end
224
+
225
+ def abort_if_directory!(path)
226
+ return unless ::File.directory?(path)
227
+ @ctx.abort(@ctx.message("theme.serve.error.invalid_subdirectory", path.to_s))
228
+ end
221
229
  end
222
230
  end
223
231
  end
@@ -11,35 +11,19 @@ module ShopifyCLI
11
11
  end
12
12
 
13
13
  def get(path:, **args)
14
- rest_request(
15
- method: "GET",
16
- path: path,
17
- **args
18
- )
14
+ rest_request(method: "GET", path: path, **args)
19
15
  end
20
16
 
21
17
  def put(path:, **args)
22
- rest_request(
23
- method: "PUT",
24
- path: path,
25
- **args
26
- )
18
+ rest_request(method: "PUT", path: path, **args)
27
19
  end
28
20
 
29
21
  def post(path:, **args)
30
- rest_request(
31
- method: "POST",
32
- path: path,
33
- **args
34
- )
22
+ rest_request(method: "POST", path: path, **args)
35
23
  end
36
24
 
37
25
  def delete(path:, **args)
38
- rest_request(
39
- method: "DELETE",
40
- path: path,
41
- **args
42
- )
26
+ rest_request(method: "DELETE", path: path, **args)
43
27
  end
44
28
 
45
29
  def get_shop_or_abort # rubocop:disable Naming/AccessorMethodName
@@ -56,16 +40,33 @@ module ShopifyCLI
56
40
  **args.compact
57
41
  )
58
42
  rescue ShopifyCLI::API::APIRequestForbiddenError,
59
- ShopifyCLI::API::APIRequestUnauthorizedError
60
- handle_permissions_error
43
+ ShopifyCLI::API::APIRequestUnauthorizedError => error
44
+ # The Admin API returns 403 Forbidden responses on different
45
+ # scenarios:
46
+ #
47
+ # * when a user doesn't have permissions for a request:
48
+ # <APIRequestForbiddenError: 403 {}>
49
+ #
50
+ # * when an asset operation cannot be performed:
51
+ # <APIRequestForbiddenError: 403 {"message":"templates/gift_card.liquid could not be deleted"}>
52
+ if empty_response_error?(error)
53
+ return handle_permissions_error
54
+ end
55
+
56
+ raise error
61
57
  end
62
58
 
63
59
  def handle_permissions_error
64
- ensure_user_error = @ctx.message("theme.ensure_user_error", get_shop_or_abort)
60
+ ensure_user_error = @ctx.message("theme.ensure_user_error", shop)
65
61
  ensure_user_try_this = @ctx.message("theme.ensure_user_try_this")
66
62
 
67
63
  @ctx.abort(ensure_user_error, ensure_user_try_this)
68
64
  end
65
+
66
+ def empty_response_error?(error)
67
+ error_message = error&.response&.body.to_s
68
+ error_message.empty?
69
+ end
69
70
  end
70
71
  end
71
72
  end
@@ -3,10 +3,14 @@
3
3
  module ShopifyCLI
4
4
  class ThreadPool
5
5
  class Job
6
- attr_reader :error
6
+ attr_reader :error, :interval
7
+
8
+ def initialize(interval = 0)
9
+ @interval = interval
10
+ end
7
11
 
8
12
  def perform!
9
- raise "`#{self.class.name}#perform!` must be defined"
13
+ raise "`#{self.class.name}#perform!' must be defined"
10
14
  end
11
15
 
12
16
  def call
@@ -22,6 +26,10 @@ module ShopifyCLI
22
26
  def error?
23
27
  !!@error
24
28
  end
29
+
30
+ def recurring?
31
+ !interval.zero?
32
+ end
25
33
  end
26
34
  end
27
35
  end
@@ -27,11 +27,23 @@ module ShopifyCLI
27
27
  def spawn_thread
28
28
  Thread.new do
29
29
  catch(:stop_thread) do
30
- loop do
31
- @jobs.pop.call
32
- end
30
+ loop { perform(@jobs.pop) }
33
31
  end
34
32
  end
35
33
  end
34
+
35
+ def perform(job)
36
+ job.call
37
+ reschedule(job) if job.recurring?
38
+ end
39
+
40
+ def reschedule(job)
41
+ wait(job.interval)
42
+ schedule(job)
43
+ end
44
+
45
+ def wait(seconds)
46
+ sleep(seconds)
47
+ end
36
48
  end
37
49
  end
@@ -178,6 +178,15 @@ module ShopifyCLI
178
178
 
179
179
  def fetch_url(ctx, log_path)
180
180
  LogParser.new(log_path)
181
+ rescue NgrokError => e
182
+ # Full error messages/descriptions: https://ngrok.com/docs/errors
183
+ case e.message
184
+ when /ERR_NGROK_107/
185
+ ctx.abort(ctx.message("tunnel.invalid_token", e.message))
186
+ when /ERR_NGROK_108/
187
+ ctx.abort(ctx.message("tunnel.duplicate_session", e.message))
188
+ end
189
+ raise e.class, e.message
181
190
  rescue RuntimeError => e
182
191
  stop(ctx)
183
192
  raise e.class, e.message
@@ -1,3 +1,3 @@
1
1
  module ShopifyCLI
2
- VERSION = "2.14.0"
2
+ VERSION = "2.15.2"
3
3
  end
data/shopify-cli.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
29
29
  %x(git ls-files -z).split("\x0").reject do |f|
30
30
  f.match(%r{^(test|spec|features|packaging)/}) ||
31
- f.match(%r{^bin/(update-deps|shopify.bat)$})
31
+ f.match(%r{^bin/(update-deps|shopify.bat)$})
32
32
  end
33
33
  end
34
34
  spec.bindir = "bin"
@@ -49,4 +49,6 @@ Gem::Specification.new do |spec|
49
49
  # Both shopify-cli and theme-check gems are owned and developed by Shopify.
50
50
  # These gems are currently being actively developed and it's easiest to update them together.
51
51
  spec.add_dependency("theme-check", "~> 1.10.1")
52
+
53
+ spec.extensions = ["ext/shopify-extensions/extconf.rb"]
52
54
  end