shopify-cli 2.14.0 → 2.15.2

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