shopify-cli 2.19.0 → 2.21.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +2 -1
  3. data/.github/ISSUE_TEMPLATE/config.yml +9 -0
  4. data/.github/workflows/cla.yml +22 -0
  5. data/CHANGELOG.md +34 -2
  6. data/Gemfile.lock +1 -1
  7. data/README.md +7 -6
  8. data/docs/users/installation.md +1 -1
  9. data/lib/project_types/extension/messages/messages.rb +1 -1
  10. data/lib/project_types/extension/tasks/fetch_specifications.rb +4 -1
  11. data/lib/project_types/theme/commands/push.rb +3 -1
  12. data/lib/project_types/theme/commands/serve.rb +1 -0
  13. data/lib/project_types/theme/messages/messages.rb +39 -2
  14. data/lib/shopify_cli/assets/post_auth_page/index.html.erb +34 -0
  15. data/lib/shopify_cli/assets/post_auth_page/style.css +58 -0
  16. data/lib/shopify_cli/constants.rb +1 -0
  17. data/lib/shopify_cli/context.rb +3 -2
  18. data/lib/shopify_cli/environment.rb +4 -0
  19. data/lib/shopify_cli/identity_auth/servlet.rb +4 -20
  20. data/lib/shopify_cli/messages/messages.rb +6 -8
  21. data/lib/shopify_cli/theme/dev_server/hot-reload-no-script.html +27 -0
  22. data/lib/shopify_cli/theme/dev_server/hot-reload.js +38 -11
  23. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +62 -0
  24. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +25 -1
  25. data/lib/shopify_cli/theme/dev_server/proxy.rb +1 -0
  26. data/lib/shopify_cli/theme/dev_server.rb +3 -2
  27. data/lib/shopify_cli/theme/file.rb +5 -0
  28. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +21 -7
  29. data/lib/shopify_cli/theme/syncer/operation.rb +7 -6
  30. data/lib/shopify_cli/theme/syncer/unsupported_script_warning.rb +90 -0
  31. data/lib/shopify_cli/theme/syncer.rb +86 -32
  32. data/lib/shopify_cli/theme/theme.rb +5 -0
  33. data/lib/shopify_cli/theme/theme_admin_api.rb +16 -11
  34. data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk.rb +102 -0
  35. data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk_job.rb +75 -0
  36. data/lib/shopify_cli/theme/theme_admin_api_throttler/errors.rb +7 -0
  37. data/lib/shopify_cli/theme/theme_admin_api_throttler/put_request.rb +52 -0
  38. data/lib/shopify_cli/theme/theme_admin_api_throttler/request_parser.rb +39 -0
  39. data/lib/shopify_cli/theme/theme_admin_api_throttler/response_parser.rb +21 -0
  40. data/lib/shopify_cli/theme/theme_admin_api_throttler.rb +62 -0
  41. data/lib/shopify_cli/version.rb +1 -1
  42. metadata +16 -3
  43. data/.github/probots.yml +0 -3
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module DevServer
6
+ class HotReload
7
+ class RemoteFileDeleter
8
+ def initialize(ctx, theme:, streams:)
9
+ @ctx = ctx
10
+ @theme = theme
11
+ @streams = streams
12
+ end
13
+
14
+ def delete(file)
15
+ retries = 6
16
+
17
+ until retries.zero?
18
+ retries -= 1
19
+
20
+ _status, body = fetch_asset(file)
21
+ retries = 0 if deleted_file?(body)
22
+
23
+ wait
24
+ end
25
+
26
+ notify(file)
27
+ end
28
+
29
+ private
30
+
31
+ def api_client
32
+ @api_client ||= ThemeAdminAPI.new(@ctx, @theme.shop)
33
+ end
34
+
35
+ def deleted_file?(body)
36
+ remote_checksum = body.dig("asset", "checksum")
37
+
38
+ remote_checksum.nil?
39
+ end
40
+
41
+ def notify(file)
42
+ @streams.broadcast(JSON.generate(deleted: [file]))
43
+ @ctx.debug("[RemoteFileDeleter] Deleted #{file}")
44
+ end
45
+
46
+ def wait
47
+ sleep(1)
48
+ end
49
+
50
+ def fetch_asset(file)
51
+ api_client.get(
52
+ path: "themes/#{@theme.id}/assets.json",
53
+ query: URI.encode_www_form("asset[key]" => file.relative_path),
54
+ )
55
+ rescue ShopifyCLI::API::APIRequestNotFoundError
56
+ [404, {}]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "hot_reload/remote_file_reloader"
4
+ require_relative "hot_reload/remote_file_deleter"
4
5
  require_relative "hot_reload/sections_index"
5
6
 
6
7
  module ShopifyCLI
@@ -14,6 +15,7 @@ module ShopifyCLI
14
15
  @mode = mode
15
16
  @streams = SSE::Streams.new
16
17
  @remote_file_reloader = RemoteFileReloader.new(ctx, theme: @theme, streams: @streams)
18
+ @remote_file_deleter = RemoteFileDeleter.new(ctx, theme: @theme, streams: @streams)
17
19
  @sections_index = SectionsIndex.new(@theme)
18
20
  @watcher = watcher
19
21
  @watcher.add_observer(self, :notify_streams_of_file_change)
@@ -36,13 +38,20 @@ module ShopifyCLI
36
38
  @streams.close
37
39
  end
38
40
 
39
- def notify_streams_of_file_change(modified, added, _removed)
41
+ def notify_streams_of_file_change(modified, added, removed)
40
42
  files = (modified + added)
41
43
  .reject { |file| @ignore_filter&.ignore?(file) }
42
44
  .map { |file| @theme[file] }
43
45
 
44
46
  files -= liquid_css_files = files.select(&:liquid_css?)
45
47
 
48
+ deleted_files = removed
49
+ .reject { |file| @ignore_filter&.ignore?(file) }
50
+ .map { |file| @theme[file] }
51
+
52
+ remote_delete(deleted_files) unless deleted_files.empty?
53
+ reload_page(removed) unless deleted_files.empty?
54
+
46
55
  hot_reload(files) unless files.empty?
47
56
  remote_reload(liquid_css_files)
48
57
  end
@@ -55,8 +64,21 @@ module ShopifyCLI
55
64
  @ctx.debug("[HotReload] Modified #{paths.join(", ")}")
56
65
  end
57
66
 
67
+ def reload_page(removed)
68
+ @streams.broadcast(JSON.generate(reload_page: true))
69
+ @ctx.debug("[ReloadPage] Deleted #{removed.join(", ")}")
70
+ end
71
+
72
+ def remote_delete(files)
73
+ files.each do |file|
74
+ @ctx.debug("delete file each -> file.relative_path #{file.relative_path}")
75
+ @remote_file_deleter.delete(file)
76
+ end
77
+ end
78
+
58
79
  def remote_reload(files)
59
80
  files.each do |file|
81
+ @ctx.debug("reload file each -> file.relative_path #{file.relative_path}")
60
82
  @remote_file_reloader.reload(file)
61
83
  end
62
84
  end
@@ -67,7 +89,9 @@ module ShopifyCLI
67
89
 
68
90
  def inject_hot_reload_javascript(body)
69
91
  hot_reload_js = ::File.read("#{__dir__}/hot-reload.js")
92
+ hot_reload_no_script = ::File.read("#{__dir__}/hot-reload-no-script.html")
70
93
  hot_reload_script = [
94
+ hot_reload_no_script,
71
95
  "<script>",
72
96
  params_js,
73
97
  hot_reload_js,
@@ -94,6 +94,7 @@ module ShopifyCLI
94
94
  end
95
95
 
96
96
  def bearer_token
97
+ Environment.storefront_renderer_auth_token ||
97
98
  ShopifyCLI::DB.get(:storefront_renderer_production_exchange_token) ||
98
99
  raise(KeyError, "storefront_renderer_production_exchange_token missing")
99
100
  end
@@ -28,11 +28,12 @@ module ShopifyCLI
28
28
  attr_accessor :ctx
29
29
 
30
30
  def start(ctx, root, host: "127.0.0.1", theme: nil, port: 9292, poll: false, editor_sync: false,
31
- mode: ReloadMode.default)
31
+ mode: ReloadMode.default, stable: false)
32
32
  @ctx = ctx
33
33
  theme = find_theme(root, theme)
34
34
  ignore_filter = IgnoreFilter.from_path(root)
35
- @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter, overwrite_json: !editor_sync)
35
+ @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter, overwrite_json: !editor_sync,
36
+ stable: stable)
36
37
  watcher = Watcher.new(ctx, theme: theme, ignore_filter: ignore_filter, syncer: @syncer, poll: poll)
37
38
  remote_watcher = RemoteWatcher.to(theme: theme, syncer: @syncer)
38
39
 
@@ -5,6 +5,7 @@ module ShopifyCLI
5
5
  module Theme
6
6
  class File < Struct.new(:path)
7
7
  attr_accessor :remote_checksum
8
+ attr_writer :warnings
8
9
 
9
10
  def initialize(path, root)
10
11
  super(Pathname.new(path))
@@ -104,6 +105,10 @@ module ShopifyCLI
104
105
  @relative_path.to_s
105
106
  end
106
107
 
108
+ def warnings
109
+ @warnings || []
110
+ end
111
+
107
112
  private
108
113
 
109
114
  def normalize_json(content)
@@ -8,16 +8,11 @@ module ShopifyCLI
8
8
  class Syncer
9
9
  module JsonUpdateHandler
10
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
11
  # Update remote JSON files and delays `delayed_files` update
18
12
  files = files
19
- .select { |file| !ignore_file?(file) && file.exist? && checksums.file_has_changed?(file) }
13
+ .select { |file| ready_to_update?(file) }
20
14
  .sort_by { |file| delayed_files.include?(file) ? 1 : 0 }
15
+ .reject { |file| overwrite_json? && delayed_files.include?(file) }
21
16
 
22
17
  if overwrite_json?
23
18
  enqueue_updates(files)
@@ -27,6 +22,21 @@ module ShopifyCLI
27
22
  end
28
23
  end
29
24
 
25
+ def enqueue_delayed_files_updates
26
+ return unless overwrite_json?
27
+ # Update delayed files synchronously
28
+ delayed_files.each do |file|
29
+ update(file) if ready_to_update?(file)
30
+ end
31
+ end
32
+
33
+ def delayed_files
34
+ [
35
+ theme["config/settings_schema.json"],
36
+ theme["config/settings_data.json"],
37
+ ]
38
+ end
39
+
30
40
  private
31
41
 
32
42
  def handle_update_conflicts(files)
@@ -76,6 +86,10 @@ module ShopifyCLI
76
86
  def ask_update_strategy(file)
77
87
  Forms::SelectUpdateStrategy.ask(@ctx, [], file: file, exists_remotely: file_exist_remotely?(file)).strategy
78
88
  end
89
+
90
+ def ready_to_update?(file)
91
+ !ignore_file?(file) && file.exist? && checksums.file_has_changed?(file)
92
+ end
79
93
  end
80
94
  end
81
95
  end
@@ -9,6 +9,7 @@ module ShopifyCLI
9
9
  COLOR_BY_STATUS = {
10
10
  error: :red,
11
11
  synced: :green,
12
+ warning: :yellow,
12
13
  fixed: :cyan,
13
14
  }
14
15
 
@@ -26,8 +27,8 @@ module ShopifyCLI
26
27
  as_message_with(status: :error)
27
28
  end
28
29
 
29
- def as_synced_message
30
- as_message_with(status: :synced)
30
+ def as_synced_message(color: :green)
31
+ as_message_with(status: :synced, color: color)
31
32
  end
32
33
 
33
34
  def as_fix_message
@@ -40,11 +41,11 @@ module ShopifyCLI
40
41
 
41
42
  private
42
43
 
43
- def as_message_with(status:)
44
- status_color = COLOR_BY_STATUS[status]
45
- status_text = @ctx.message("theme.serve.operation.status.#{status}").ljust(6)
44
+ def as_message_with(status:, color: nil)
45
+ color ||= COLOR_BY_STATUS[status]
46
+ text = @ctx.message("theme.serve.operation.status.#{status}").ljust(6)
46
47
 
47
- "#{timestamp} {{#{status_color}:#{status_text}}} {{>}} {{blue:#{self}}}"
48
+ "#{timestamp} {{#{color}:#{text}}} {{>}} {{blue:#{self}}}"
48
49
  end
49
50
 
50
51
  def timestamp
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ class UnsupportedScriptWarning
7
+ attr_reader :ctx
8
+
9
+ def initialize(ctx, file)
10
+ @ctx = ctx
11
+ @file = file
12
+ end
13
+
14
+ def to_s
15
+ "\n\n#{occurrences} #{long_text}"
16
+ end
17
+
18
+ private
19
+
20
+ def occurrences
21
+ warnings.map { |w| occurrence(w) }.join("\n")
22
+ end
23
+
24
+ def occurrence(warning)
25
+ line_number = "{{blue: #{warning.line} |}}"
26
+ pointer = pointer_message(warning)
27
+
28
+ <<~OCCURRENCE
29
+ #{line_number} #{warning.line_content}
30
+ #{pointer}
31
+ OCCURRENCE
32
+ end
33
+
34
+ def long_text
35
+ lines_and_columns = warnings.map do |warning|
36
+ message("line_and_column", warning.line, warning.column)
37
+ end
38
+
39
+ message("unsupported_script_text", lines_and_columns.join)
40
+ .split("\n")
41
+ .reduce("") do |text, line|
42
+ # Add indentation in the long text to improve readability
43
+ line = " #{line}"
44
+
45
+ # Inline yellow (otherwise `CLI::UI::Frame` breaks multiline formatting)
46
+ line = "{{yellow:#{line}}}"
47
+
48
+ "#{text}#{line}\n"
49
+ end
50
+ end
51
+
52
+ def pointer_message(warning)
53
+ padding = warning.column + warning.line.to_s.size + 2
54
+ text = message("unsupported_script")
55
+
56
+ "{{yellow:#{" " * padding} ^ {{bold:#{text}}}}}"
57
+ end
58
+
59
+ def message(*args)
60
+ key = args.shift
61
+ @ctx.message("theme.serve.syncer.warnings.#{key}", *args)
62
+ end
63
+
64
+ def warnings
65
+ @warnings ||= @file.warnings.map { |w| Warning.new(@file, w) }
66
+ end
67
+
68
+ class Warning
69
+ attr_reader :line, :column
70
+
71
+ def initialize(file, warning_hash)
72
+ @file = file
73
+ @line = warning_hash["line"].to_i
74
+ @column = warning_hash["column"].to_i
75
+ end
76
+
77
+ def line_content
78
+ file_lines[line - 1]
79
+ end
80
+
81
+ private
82
+
83
+ def file_lines
84
+ @file_lines ||= @file.read.split("\n")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -13,7 +13,9 @@ require_relative "syncer/json_update_handler"
13
13
  require_relative "syncer/merger"
14
14
  require_relative "syncer/operation"
15
15
  require_relative "syncer/standard_reporter"
16
+ require_relative "syncer/unsupported_script_warning"
16
17
  require_relative "theme_admin_api"
18
+ require_relative "theme_admin_api_throttler"
17
19
 
18
20
  module ShopifyCLI
19
21
  module Theme
@@ -31,12 +33,12 @@ module ShopifyCLI
31
33
  :union_merge, # - Union merges the local file content with the remote file content
32
34
  ]
33
35
 
34
- attr_reader :theme, :checksums, :error_checksums
36
+ attr_reader :theme, :checksums, :error_checksums, :api_client
35
37
  attr_accessor :include_filter, :ignore_filter
36
38
 
37
39
  def_delegators :@error_reporter, :has_any_error?
38
40
 
39
- def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true)
41
+ def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true, stable: false)
40
42
  @ctx = ctx
41
43
  @theme = theme
42
44
  @include_filter = include_filter
@@ -58,15 +60,21 @@ module ShopifyCLI
58
60
  # Mutex used to pause all threads when backing-off when hitting API rate limits
59
61
  @backoff_mutex = Mutex.new
60
62
 
63
+ # Mutex used to coordinate changes in the `pending` list
64
+ @pending_mutex = Mutex.new
65
+
61
66
  # Latest theme assets checksums. Updated on each upload.
62
67
  @checksums = Checksums.new(theme)
63
68
 
64
69
  # Checksums of assets with errors.
65
70
  @error_checksums = []
66
- end
67
71
 
68
- def api_client
69
- @api_client ||= ThemeAdminAPI.new(@ctx, @theme.shop)
72
+ # Initialize `api_client` on main thread
73
+ @api_client = ThemeAdminAPIThrottler.new(
74
+ @ctx,
75
+ ThemeAdminAPI.new(@ctx, @theme.shop),
76
+ !stable
77
+ )
70
78
  end
71
79
 
72
80
  def lock_io!
@@ -134,6 +142,7 @@ module ShopifyCLI
134
142
  end
135
143
 
136
144
  def shutdown
145
+ api_client.shutdown
137
146
  @queue.close unless @queue.closed?
138
147
  ensure
139
148
  @threads.each { |thread| thread.join if thread.alive? }
@@ -185,6 +194,9 @@ module ShopifyCLI
185
194
  unless delay_low_priority_files
186
195
  wait!(&block)
187
196
  end
197
+
198
+ api_client.deactivate_throttler!
199
+ enqueue_delayed_files_updates
188
200
  end
189
201
 
190
202
  def download_theme!(delete: true, &block)
@@ -242,38 +254,57 @@ module ShopifyCLI
242
254
  wait_for_backoff!
243
255
  @ctx.debug(operation.to_s)
244
256
 
245
- response = send(operation.method, operation.file)
257
+ send(operation.method, operation.file) do |response|
258
+ raise response if response.is_a?(StandardError)
246
259
 
247
- @standard_reporter.report(operation.as_synced_message)
260
+ file = operation.file
248
261
 
249
- # Check if the API told us we're near the rate limit
250
- if !backingoff? && (limit = response["x-shopify-shop-api-call-limit"])
251
- used, total = limit.split("/").map(&:to_i)
252
- backoff_if_near_limit!(used, total)
262
+ if file.warnings.any?
263
+ warning_message =
264
+ operation.as_synced_message(color: :yellow) +
265
+ UnsupportedScriptWarning.new(@ctx, file).to_s
266
+ @error_reporter.report(warning_message)
267
+ else
268
+ @standard_reporter.report(operation.as_synced_message)
269
+ end
270
+
271
+ # Check if the API told us we're near the rate limit
272
+ if !backingoff? && (limit = response["x-shopify-shop-api-call-limit"])
273
+ used, total = limit.split("/").map(&:to_i)
274
+ backoff_if_near_limit!(used, total)
275
+ end
276
+ rescue StandardError => error
277
+ handle_operation_error(operation, error)
278
+ ensure
279
+ @pending_mutex.synchronize do
280
+ # Avoid abrupt jumps in the progress bar
281
+ wait(0.05)
282
+ @pending.delete(operation)
283
+ end
253
284
  end
254
- rescue ShopifyCLI::API::APIRequestError => e
255
- error_suffix = ":\n " + parse_api_errors(e).join("\n ")
256
- report_error(operation, error_suffix)
257
- ensure
258
- @pending.delete(operation)
285
+ rescue StandardError => error
286
+ handle_operation_error(operation, error)
259
287
  end
260
288
 
261
289
  def update(file)
262
290
  asset = { key: file.relative_path }
291
+
263
292
  if file.text?
264
293
  asset[:value] = file.read
265
294
  else
266
295
  asset[:attachment] = Base64.encode64(file.read)
267
296
  end
268
297
 
269
- _status, body, response = api_client.put(
270
- path: "themes/#{@theme.id}/assets.json",
271
- body: JSON.generate(asset: asset)
272
- )
298
+ path = "themes/#{@theme.id}/assets.json"
299
+ req_body = JSON.generate(asset: asset)
273
300
 
274
- update_checksums(body)
301
+ api_client.put(path: path, body: req_body) do |_status, resp_body, response|
302
+ update_checksums(resp_body)
275
303
 
276
- response
304
+ file.warnings = resp_body.dig("asset", "warnings")
305
+
306
+ yield(response) if block_given?
307
+ end
277
308
  end
278
309
 
279
310
  def get(file)
@@ -291,7 +322,7 @@ module ShopifyCLI
291
322
  file.write(body.dig("asset", "value"))
292
323
  end
293
324
 
294
- response
325
+ yield(response)
295
326
  end
296
327
 
297
328
  def delete(file)
@@ -302,7 +333,7 @@ module ShopifyCLI
302
333
  })
303
334
  )
304
335
 
305
- response
336
+ yield(response)
306
337
  end
307
338
 
308
339
  def union_merge(file)
@@ -311,11 +342,11 @@ module ShopifyCLI
311
342
  query: URI.encode_www_form("asset[key]" => file.relative_path),
312
343
  )
313
344
 
314
- return response unless file.text?
345
+ return yield(response) unless file.text?
315
346
 
316
347
  remote_content = body.dig("asset", "value")
317
348
 
318
- return response if remote_content.nil?
349
+ return yield(response) if remote_content.nil?
319
350
 
320
351
  content = Merger.union_merge(file, remote_content)
321
352
 
@@ -323,7 +354,7 @@ module ShopifyCLI
323
354
 
324
355
  enqueue(:update, file)
325
356
 
326
- response
357
+ yield(response)
327
358
  end
328
359
 
329
360
  def update_checksums(api_response)
@@ -335,24 +366,38 @@ module ShopifyCLI
335
366
  checksums.reject_duplicated_checksums!
336
367
  end
337
368
 
338
- def parse_api_errors(exception)
339
- parsed_body = JSON.parse(exception&.response&.body)
340
- message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
369
+ def parse_api_errors(operation, exception)
370
+ parsed_body = if exception&.response&.is_a?(Hash)
371
+ exception&.response&.[](:body)
372
+ else
373
+ JSON.parse(exception&.response&.body)
374
+ end
375
+
376
+ errors = parsed_body.dig("errors") # either nil or another type
377
+ errors = errors.dig("asset") if errors&.is_a?(Hash)
378
+
379
+ message = errors || parsed_body["message"] || exception.message
341
380
  # Truncate to first lines
342
381
  [message].flatten.map { |m| m.split("\n", 2).first }
343
382
  rescue JSON::ParserError
344
383
  [exception.message]
384
+ rescue StandardError => e
385
+ ["The asset #{operation.file} is could not be synced (cause: #{e.message})."]
345
386
  end
346
387
 
347
388
  def backoff_if_near_limit!(used, limit)
348
389
  if used > limit - @threads.size
349
390
  @ctx.debug("Near API call limit, waiting 2 sec…")
350
- @backoff_mutex.synchronize { sleep(2) }
391
+ @backoff_mutex.synchronize { wait(2) }
351
392
  end
352
393
  end
353
394
 
354
395
  def overwrite_json?
355
- @overwrite_json
396
+ theme_created_at_runtime? || @overwrite_json
397
+ end
398
+
399
+ def theme_created_at_runtime?
400
+ @theme.created_at_runtime?
356
401
  end
357
402
 
358
403
  def backingoff?
@@ -363,6 +408,15 @@ module ShopifyCLI
363
408
  # Sleeping in the mutex in another thread. Wait for unlock
364
409
  @backoff_mutex.synchronize {} if backingoff?
365
410
  end
411
+
412
+ def handle_operation_error(operation, error)
413
+ error_suffix = ":\n " + parse_api_errors(operation, error).join("\n ")
414
+ report_error(operation, error_suffix)
415
+ end
416
+
417
+ def wait(duration)
418
+ sleep(duration)
419
+ end
366
420
  end
367
421
  end
368
422
  end
@@ -120,6 +120,7 @@ module ShopifyCLI
120
120
  )
121
121
 
122
122
  @id = body["theme"]["id"]
123
+ @created_at_runtime = true
123
124
  end
124
125
 
125
126
  def delete
@@ -147,6 +148,10 @@ module ShopifyCLI
147
148
  development? && id != ShopifyCLI::DB.get(:development_theme_id)
148
149
  end
149
150
 
151
+ def created_at_runtime?
152
+ @created_at_runtime ||= false
153
+ end
154
+
150
155
  def to_h
151
156
  {
152
157
  id: id,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ShopifyCLI
2
4
  module Theme
3
5
  class ThemeAdminAPI
@@ -10,35 +12,36 @@ module ShopifyCLI
10
12
  @shop = shop || get_shop_or_abort
11
13
  end
12
14
 
13
- def get(path:, **args)
14
- rest_request(method: "GET", path: path, **args)
15
+ def get(path:, **args, &block)
16
+ rest_request(method: "GET", path: path, **args, &block)
15
17
  end
16
18
 
17
- def put(path:, **args)
18
- rest_request(method: "PUT", path: path, **args)
19
+ def put(path:, **args, &block)
20
+ rest_request(method: "PUT", path: path, **args, &block)
19
21
  end
20
22
 
21
- def post(path:, **args)
22
- rest_request(method: "POST", path: path, **args)
23
+ def post(path:, **args, &block)
24
+ rest_request(method: "POST", path: path, **args, &block)
23
25
  end
24
26
 
25
- def delete(path:, **args)
26
- rest_request(method: "DELETE", path: path, **args)
27
+ def delete(path:, **args, &block)
28
+ rest_request(method: "DELETE", path: path, **args, &block)
27
29
  end
28
30
 
29
31
  def get_shop_or_abort # rubocop:disable Naming/AccessorMethodName
30
32
  ShopifyCLI::AdminAPI.get_shop_or_abort(@ctx)
31
33
  end
32
34
 
33
- private
34
-
35
35
  def rest_request(**args)
36
- ShopifyCLI::AdminAPI.rest_request(
36
+ status, body, response = ShopifyCLI::AdminAPI.rest_request(
37
37
  @ctx,
38
38
  shop: @shop,
39
39
  api_version: API_VERSION,
40
40
  **args.compact
41
41
  )
42
+ return yield(status, body, response) if block_given?
43
+
44
+ [status, body, response]
42
45
  rescue ShopifyCLI::API::APIRequestForbiddenError,
43
46
  ShopifyCLI::API::APIRequestUnauthorizedError => error
44
47
 
@@ -62,6 +65,8 @@ module ShopifyCLI
62
65
  raise error
63
66
  end
64
67
 
68
+ private
69
+
65
70
  def permission_error
66
71
  ensure_user_error = @ctx.message("theme.ensure_user_error", shop)
67
72
  ensure_user_try_this = @ctx.message("theme.ensure_user_try_this")