shopify-cli 2.29.0 → 2.30.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/project_types/theme/commands/package.rb +20 -5
  5. data/lib/project_types/theme/messages/messages.rb +4 -2
  6. data/lib/shopify_cli/packager.rb +5 -14
  7. data/lib/shopify_cli/theme/backoff_helper.rb +47 -0
  8. data/lib/shopify_cli/theme/ignore_helper.rb +7 -1
  9. data/lib/shopify_cli/theme/syncer/downloader.rb +63 -0
  10. data/lib/shopify_cli/theme/syncer/uploader/bulk.rb +133 -0
  11. data/lib/shopify_cli/theme/syncer/uploader/bulk_item.rb +64 -0
  12. data/lib/shopify_cli/theme/syncer/uploader/bulk_job.rb +139 -0
  13. data/lib/shopify_cli/theme/syncer/uploader/bulk_request.rb +30 -0
  14. data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all.rb +41 -0
  15. data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all_form.rb +37 -0
  16. data/lib/shopify_cli/theme/syncer/uploader/forms/base_strategy_form.rb +64 -0
  17. data/lib/shopify_cli/theme/syncer/uploader/forms/select_delete_strategy.rb +29 -0
  18. data/lib/shopify_cli/theme/syncer/uploader/forms/select_update_strategy.rb +30 -0
  19. data/lib/shopify_cli/theme/syncer/uploader/json_delete_handler.rb +49 -0
  20. data/lib/shopify_cli/theme/syncer/uploader/json_update_handler.rb +71 -0
  21. data/lib/shopify_cli/theme/syncer/uploader.rb +227 -0
  22. data/lib/shopify_cli/theme/syncer.rb +91 -144
  23. data/lib/shopify_cli/version.rb +1 -1
  24. metadata +16 -16
  25. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +0 -39
  26. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +0 -35
  27. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +0 -62
  28. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +0 -27
  29. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +0 -28
  30. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +0 -51
  31. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +0 -96
  32. data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk.rb +0 -102
  33. data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk_job.rb +0 -75
  34. data/lib/shopify_cli/theme/theme_admin_api_throttler/errors.rb +0 -7
  35. data/lib/shopify_cli/theme/theme_admin_api_throttler/put_request.rb +0 -52
  36. data/lib/shopify_cli/theme/theme_admin_api_throttler/request_parser.rb +0 -39
  37. data/lib/shopify_cli/theme/theme_admin_api_throttler/response_parser.rb +0 -21
  38. data/lib/shopify_cli/theme/theme_admin_api_throttler.rb +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f07c45a6fc9d6b93d9f2826a7ad9eb139842d8b7f35717ca5fb17291285c330
4
- data.tar.gz: 774e222e76f9a8d30b68e8b0756ed4cecbc8281d302d94c131d072c123b19f8c
3
+ metadata.gz: 3050c5be253ff04fbd3fa04ddd60389c0e6c0c2c0a3ae3fdcc6b059dc428c193
4
+ data.tar.gz: 2d373f27b01e11cd4df467114ba00849c978ae7cca1d8529749be5ba5d5d663d
5
5
  SHA512:
6
- metadata.gz: eab29689be4de3a23b0b9dd14401a130d6f37cc1064c02f16bbce0656d378d487666bd964dfa673b8fcc48344b01acf8d10899e4fb3755c7f5f46c5dd8c46385
7
- data.tar.gz: 01d9830606768a70de1de312527875d997dce26f1913c44fd6ffd82407afc7962931fe38dd7568e49c899438e33bf3f1b465b6b5d73bb0585d25643c0d26d5a2
6
+ metadata.gz: 8427e448b97accd935d7b6b16452070139a70139d53cfd3e4d7f4efff495873f4d1372e9e08bf4b75fae7150c95ddb431363b7d970dbcfffc1fbeb1b22f7c0af
7
+ data.tar.gz: be22490e0dea78184b7bcd58c4bca6a1a52c2963c45063e9a7ad6703ecd295c6e31d37a02716f741a3a7682d0fc12f86a167b0d78d1ef9bd973894bc5b7b4ba1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@ From version 2.6.0, the sections in this file adhere to the [keep a changelog](h
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## Version 2.30.0 - 2022-11-01
6
+
7
+ ### Fixed
8
+ * [#2668](https://github.com/Shopify/shopify-cli/pull/2668): Introduce `--only/--ignore` in the `shopify theme serve` help message
9
+ * [#2667](https://github.com/Shopify/shopify-cli/pull/2667): Fix for "X zip is required for packaging a theme" on Windows
10
+
5
11
  ## Version 2.29.0 - 2022-10-19
6
12
 
7
13
  ### Added
@@ -11,6 +17,7 @@ From version 2.6.0, the sections in this file adhere to the [keep a changelog](h
11
17
 
12
18
  ### Fixed
13
19
  * [#2646](https://github.com/Shopify/shopify-cli/pull/2646): Demo themes shouldn't appear in the `shopify theme pull/push/list/open` commands
20
+ * [#2650](https://github.com/Shopify/shopify-cli/pull/2650): The `shopify theme push`/`shopify theme serve` commands no longer freeze in some scenarios
14
21
 
15
22
  ### Changed
16
23
  * [#2648](https://github.com/Shopify/shopify-cli/pull/2648): Do not warn users when the CLI 2.x is running as a subprocess
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shopify-cli (2.29.0)
4
+ shopify-cli (2.30.0)
5
5
  bugsnag (~> 6.22)
6
6
  listen (~> 3.7.0)
7
7
  theme-check (~> 1.11.0)
@@ -18,11 +18,15 @@ module Theme
18
18
  release-notes.md
19
19
  ]
20
20
 
21
+ ZIP = "zip"
22
+ SEVEN_ZIP = "7z"
23
+
21
24
  def call(args, _name)
22
25
  path = args.first || "."
23
26
 
24
- check_prereq_command("zip")
27
+ check_prereq_command
25
28
  zip_name = theme_name(path) + ".zip"
29
+
26
30
  zip(zip_name, path, THEME_DIRECTORIES)
27
31
  @ctx.done(@ctx.message("theme.package.done", zip_name))
28
32
  end
@@ -33,13 +37,12 @@ module Theme
33
37
 
34
38
  private
35
39
 
36
- def check_prereq_command(command)
37
- cmd_path = @ctx.which(command)
38
- @ctx.abort(@ctx.message("theme.package.error.prereq_command_required", command)) if cmd_path.nil?
40
+ def check_prereq_command
41
+ @ctx.abort(@ctx.message("theme.package.error.prereq_command_required")) if command.nil?
39
42
  end
40
43
 
41
44
  def zip(zip_name, path, files)
42
- @ctx.system("zip", "-r", zip_name, *files, chdir: path)
45
+ @ctx.system(command, command_flags, zip_name, *files, chdir: path)
43
46
  end
44
47
 
45
48
  def theme_name(path)
@@ -53,6 +56,18 @@ module Theme
53
56
 
54
57
  [theme_name, theme_info["theme_version"]].compact.join("-")
55
58
  end
59
+
60
+ def command
61
+ @command ||= if @ctx.which(ZIP)
62
+ ZIP
63
+ elsif @ctx.which(SEVEN_ZIP)
64
+ SEVEN_ZIP
65
+ end
66
+ end
67
+
68
+ def command_flags
69
+ @command_flags ||= command == ZIP ? "-r" : "a"
70
+ end
56
71
  end
57
72
  end
58
73
  end
@@ -124,6 +124,8 @@ module Theme
124
124
 
125
125
  Options:
126
126
  {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
127
+ {{command:-o, --only}} Hot reload only files that match the specified pattern.
128
+ {{command:-x, --ignore}} Skip hot reloading any files that match the specified pattern.
127
129
  {{command:--port=PORT}} Local port to serve theme preview from.
128
130
  {{command:--poll}} Force polling to detect file changes.
129
131
  {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
@@ -300,8 +302,8 @@ module Theme
300
302
  Usage: {{command:%s theme package [ ROOT ]}}
301
303
  HELP
302
304
  error: {
303
- prereq_command_required: "%1$s is required for packaging a theme. Please install %1$s "\
304
- "using the appropriate package manager for your system.",
305
+ prereq_command_required: "zip or 7zip is required for packaging a theme. Please install "\
306
+ "zip or 7zip using the appropriate package manager for your system.",
305
307
  missing_config: "Provide a config/settings_schema.json to package your theme",
306
308
  missing_theme_name: "Provide a theme_info.theme_name configuration in config/settings_schema.json",
307
309
  },
@@ -69,14 +69,13 @@ module ShopifyCLI
69
69
  def build_homebrew
70
70
  root_dir = File.join(PACKAGING_DIR, "homebrew")
71
71
 
72
- build_path = File.join(BUILDS_DIR, "shopify-cli.rb")
73
- build_path_2 = File.join(BUILDS_DIR, "shopify-cli@2.rb")
74
- puts "\nBuilding Homebrew packages"
72
+ build_path = File.join(BUILDS_DIR, "shopify-cli@2.rb")
73
+ puts "\nBuilding Homebrew package"
75
74
 
76
- puts "Generating formulae…"
75
+ puts "Generating formula…"
77
76
  File.delete(build_path) if File.exist?(build_path)
78
77
 
79
- spec_contents = File.read(File.join(root_dir, "shopify-cli.base.rb"))
78
+ spec_contents = File.read(File.join(root_dir, "shopify-cli@2.base.rb"))
80
79
  spec_contents = spec_contents.gsub("SHOPIFY_CLI_VERSION", ShopifyCLI::VERSION)
81
80
 
82
81
  puts "Grabbing sha256 checksum from Rubygems.org"
@@ -90,15 +89,7 @@ module ShopifyCLI
90
89
  spec_contents = spec_contents.gsub("SHOPIFY_CLI_GEM_CHECKSUM", gem_checksum)
91
90
 
92
91
  puts "Writing generated formula\n To: #{build_path}\n\n"
93
- File.write(build_path, spec_contents.gsub("SHOPIFY_CLI_BINSTUB_SUFFIX", ""))
94
-
95
- puts "Writing generated formula\n To: #{build_path_2}\n\n"
96
- File.write(
97
- build_path_2,
98
- spec_contents
99
- .sub("class ShopifyCli < Formula", "class ShopifyCliAT2 < Formula")
100
- .sub("SHOPIFY_CLI_BINSTUB_SUFFIX", "2")
101
- )
92
+ File.write(build_path, spec_contents)
102
93
  end
103
94
 
104
95
  private
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module BackoffHelper
6
+ def initialize_backoff_helper!(margin: 2, backoff_delay: 2)
7
+ @margin = margin
8
+ @backoff_delay = backoff_delay
9
+ @backoff_mutex = Mutex.new
10
+ end
11
+
12
+ def backoff_if_near_limit!(response)
13
+ # Check if the API told us we're near the rate limit
14
+ return if backingoff? || !response
15
+
16
+ limit = response.fetch("x-shopify-shop-api-call-limit", "0/999")
17
+ used, total = limit.split("/").map(&:to_i)
18
+
19
+ backoff! if used > total - @margin
20
+ end
21
+
22
+ def wait_for_backoff!
23
+ # Sleeping in the mutex in another thread. Wait for unlock
24
+ backoff_mutex.synchronize {} if backingoff?
25
+ end
26
+
27
+ def backoff!
28
+ ctx.debug("Near API call limit, waiting #{@backoff_delay} seconds")
29
+ backoff_mutex.synchronize { wait(@backoff_delay) }
30
+ end
31
+
32
+ def backingoff?
33
+ backoff_mutex.locked?
34
+ end
35
+
36
+ def backoff_mutex
37
+ @backoff_mutex || raise("Backoff helper must be initialized")
38
+ end
39
+
40
+ private
41
+
42
+ def wait(seconds)
43
+ sleep(seconds)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -14,7 +14,13 @@ module ShopifyCLI
14
14
  end
15
15
 
16
16
  def ignore_path?(path)
17
- ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
17
+ is_ignored = ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
18
+
19
+ if is_ignored && @ctx
20
+ @ctx.debug("ignore #{path}")
21
+ end
22
+
23
+ is_ignored
18
24
  end
19
25
 
20
26
  private
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Downloader
9
+ extend Forwardable
10
+
11
+ attr_reader :syncer, :ctx
12
+
13
+ def_delegators :syncer,
14
+ :ctx,
15
+ :checksums,
16
+ :enqueue_get,
17
+ :ignore_file?,
18
+ :fetch_checksums!,
19
+ :wait!
20
+
21
+ def initialize(syncer, delete, &update_progress_bar_block)
22
+ @syncer = syncer
23
+ @delete = delete
24
+ @update_progress_bar_block = update_progress_bar_block
25
+ end
26
+
27
+ def download!
28
+ fetch_checksums!
29
+
30
+ if delete_local_files?
31
+ to_be_deleted.each { |file| delete(file) }
32
+ end
33
+
34
+ enqueue_get(checksums.keys)
35
+ wait!(&@update_progress_bar_block)
36
+ end
37
+
38
+ private
39
+
40
+ def delete(file)
41
+ ctx.debug("[#{self.class}] rm #{file.relative_path}")
42
+ file.delete
43
+ end
44
+
45
+ def delete_local_files?
46
+ @delete
47
+ end
48
+
49
+ def to_be_deleted
50
+ @to_be_deleted ||= syncer
51
+ .theme
52
+ .theme_files
53
+ .reject { |file| present_remotely?(file) }.uniq
54
+ .reject { |file| ignore_file?(file) }
55
+ end
56
+
57
+ def present_remotely?(file)
58
+ checksums.has?(file)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool"
4
+ require "shopify_cli/theme/backoff_helper"
5
+
6
+ require_relative "bulk_job"
7
+
8
+ module ShopifyCLI
9
+ module Theme
10
+ class Syncer
11
+ class Uploader
12
+ class Bulk
13
+ include ShopifyCLI::Theme::BackoffHelper
14
+
15
+ MAX_BULK_BYTESIZE = 102_400 # 100KB
16
+ MAX_BULK_FILES = 10 # 10 files
17
+ SHUTDOWN_TIMEOUT = 20 # 20 seconds
18
+ SHUTDOWN_RETRY_INTERVAL = 0.2 # 200 milliseconds
19
+
20
+ attr_reader :ctx, :theme, :admin_api
21
+
22
+ def initialize(ctx, theme, admin_api, pool_size: 15)
23
+ @ctx = ctx
24
+ @theme = theme
25
+ @admin_api = admin_api
26
+ @pending_items = []
27
+ @in_progress_items = []
28
+
29
+ # Mutex used to coordinate changes in the `@pending_items` shared
30
+ # accross the `@thread_pool` threads
31
+ @pending_items_mutex = Mutex.new
32
+
33
+ # Mutex used to coordinate changes in the `@in_progress_items`
34
+ # shared accross the `@thread_pool` threads
35
+ @in_progress_items_mutex = Mutex.new
36
+
37
+ # Initialize thread pool and the jobs
38
+ @thread_pool = initialize_thread_pool!(pool_size)
39
+
40
+ # Initialize backoff helper on main thread to pause all jobs when
41
+ # requests are reaching API rate limits
42
+ initialize_backoff_helper!(margin: pool_size, backoff_delay: 5)
43
+ end
44
+
45
+ def enqueue(bulk_item)
46
+ @pending_items_mutex.synchronize { @pending_items << bulk_item }
47
+ end
48
+
49
+ def shutdown
50
+ wait_bulk_items
51
+ @thread_pool.shutdown
52
+ end
53
+
54
+ def consume_bulk_items
55
+ items = []
56
+ items_bytesize = 0
57
+
58
+ @pending_items_mutex.synchronize do
59
+ has_enough_items = false
60
+ has_enough_bytesize = false
61
+
62
+ until has_enough_items || has_enough_bytesize || @pending_items.empty?
63
+ bulk_item = @pending_items.first
64
+
65
+ has_enough_items = items.size + 1 > MAX_BULK_FILES
66
+ has_enough_bytesize = items_bytesize + bulk_item.size > MAX_BULK_BYTESIZE
67
+
68
+ break if items.any? && (has_enough_items || has_enough_bytesize)
69
+
70
+ items << bulk_item
71
+ items_bytesize += bulk_item.size
72
+
73
+ @pending_items.shift
74
+ end
75
+ end
76
+
77
+ @in_progress_items_mutex.synchronize do
78
+ @in_progress_items += items
79
+ end
80
+
81
+ [items, items_bytesize]
82
+ end
83
+
84
+ def clean_in_progress_items(items)
85
+ @in_progress_items_mutex.synchronize do
86
+ @in_progress_items -= items
87
+ end
88
+ end
89
+
90
+ def remaining_items
91
+ @pending_items + @in_progress_items
92
+ end
93
+
94
+ private
95
+
96
+ def wait_bulk_items
97
+ start_time = Time.now
98
+
99
+ wait(SHUTDOWN_RETRY_INTERVAL) while remaining_items? && start_time - Time.now < SHUTDOWN_TIMEOUT
100
+
101
+ files = remaining_items.map { |item| "- #{item.key}" }.join("\n")
102
+
103
+ log("shutdown, remaining_items=#{remaining_items.size}\n#{files}")
104
+ end
105
+
106
+ def remaining_items?
107
+ !@pending_items.empty? || !@in_progress_items.empty?
108
+ end
109
+
110
+ def initialize_thread_pool!(pool_size)
111
+ ShopifyCLI::ThreadPool
112
+ .new(pool_size: pool_size)
113
+ .tap do |thread_pool|
114
+ pool_size.times { thread_pool.schedule(spawn_job) }
115
+ end
116
+ end
117
+
118
+ def spawn_job
119
+ BulkJob.new(ctx, self)
120
+ end
121
+
122
+ def log(message)
123
+ ctx.debug("[Bulk] #{message}")
124
+ end
125
+
126
+ def wait(seconds)
127
+ sleep(seconds)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool/job"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Uploader
9
+ class BulkItem
10
+ attr_reader :file, :block
11
+ attr_accessor :retries
12
+
13
+ def initialize(file, &block)
14
+ @file = file
15
+ @block = block
16
+ @retries = 0
17
+ end
18
+
19
+ def to_h
20
+ { body: body }
21
+ end
22
+
23
+ def to_s
24
+ "#<#{self.class.name} key=#{key}, retries=#{retries}>"
25
+ end
26
+
27
+ def liquid?
28
+ file.liquid?
29
+ end
30
+
31
+ def key
32
+ file.relative_path
33
+ end
34
+
35
+ def size
36
+ @size ||= body.bytesize
37
+ end
38
+
39
+ def body
40
+ @body ||= JSON.generate(asset: asset)
41
+ end
42
+
43
+ def asset
44
+ @asset ||= asset_hash
45
+ end
46
+
47
+ private
48
+
49
+ def asset_hash
50
+ asset = { key: file.relative_path }
51
+
52
+ if file.text?
53
+ asset[:value] = file.read
54
+ else
55
+ asset[:attachment] = Base64.encode64(file.read)
56
+ end
57
+
58
+ asset
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool/job"
4
+
5
+ require_relative "bulk_request"
6
+
7
+ module ShopifyCLI
8
+ module Theme
9
+ class Syncer
10
+ class Uploader
11
+ class BulkJob < ShopifyCLI::ThreadPool::Job
12
+ INTERVAL = 0.2 # 200ms
13
+ MAX_RETRIES = 10
14
+
15
+ attr_reader :ctx, :bulk, :admin_api
16
+
17
+ def initialize(ctx, bulk)
18
+ super(INTERVAL)
19
+
20
+ @ctx = ctx
21
+ @bulk = bulk
22
+ @admin_api = bulk.admin_api
23
+
24
+ # Mutex used to coordinate changes in the bulk items
25
+ @bulk_item_mutex = Mutex.new
26
+
27
+ # Mutex used to coordinate changes in the bulk items
28
+ @bulk_item_mutex = Mutex.new
29
+ end
30
+
31
+ def perform!
32
+ bulk.wait_for_backoff!
33
+
34
+ # Fetch bulk items
35
+ bulk_items, bulk_size = bulk.consume_bulk_items
36
+ return if bulk_items.empty?
37
+
38
+ # Perform bulk request
39
+ log("job request: size=#{bulk_items.size}, bytesize=#{bulk_size}")
40
+ bulk_status, bulk_body, response = rest_request(bulk_items)
41
+ bulk.backoff_if_near_limit!(response)
42
+ log("job response: http_status=#{bulk_status}")
43
+
44
+ # Abort execution when a fatal error happens
45
+ return stable_flag_suggestion! if bulk_status != 207
46
+
47
+ # Handle item reponses
48
+ responses = parse_responses(bulk_body)
49
+ responses
50
+ .each_with_index do |tuple, index|
51
+ status, body = tuple
52
+ bulk_item = bulk_items[index]
53
+ handle_item_response(bulk_item, status, body, response)
54
+ end
55
+ ensure
56
+ bulk.clean_in_progress_items(bulk_items)
57
+ end
58
+
59
+ private
60
+
61
+ def handle_item_response(bulk_item, status, body, response)
62
+ if status == 200
63
+ return handle_success(bulk_item, status, body, response)
64
+ end
65
+
66
+ if bulk_item.retries < MAX_RETRIES
67
+ return handle_retry(bulk_item, status, body, response)
68
+ end
69
+
70
+ handle_error(bulk_item, status, body)
71
+ end
72
+
73
+ def handle_success(bulk_item, status, body, response)
74
+ log("bulk item success (item=#{bulk_item.key})")
75
+
76
+ @bulk_item_mutex.synchronize do
77
+ bulk_item.block.call(status, body, response)
78
+ end
79
+ end
80
+
81
+ def handle_retry(bulk_item, status, _body, _response)
82
+ key = bulk_item.key
83
+ retries = bulk_item.retries
84
+
85
+ log("bulk item error (item=#{key}, status=#{status}, retries=#{retries})")
86
+
87
+ @bulk_item_mutex.synchronize do
88
+ bulk_item.retries += 1
89
+ bulk.enqueue(bulk_item)
90
+ end
91
+ end
92
+
93
+ def handle_error(bulk_item, status, body)
94
+ log("bulk item fatal error (item=#{bulk_item.key}, status=#{status})")
95
+
96
+ @bulk_item_mutex.synchronize do
97
+ bulk_item.block.call(status, body, error_response(body))
98
+ end
99
+ end
100
+
101
+ def stable_flag_suggestion!
102
+ ctx.abort(ctx.message("theme.stable_flag_suggestion"))
103
+ end
104
+
105
+ def rest_request(bulk_items)
106
+ theme = bulk.theme
107
+ args = BulkRequest.new(theme, bulk_items).to_h
108
+
109
+ measure("bulk rest_request") { admin_api.rest_request(**args) }
110
+ end
111
+
112
+ def measure(subject)
113
+ return yield unless ctx.debug?
114
+
115
+ start_time = Time.now
116
+ result = yield
117
+ time_elapsed = (Time.now - start_time) * 1000
118
+
119
+ log("#{subject} time: #{time_elapsed}ms")
120
+
121
+ result
122
+ end
123
+
124
+ def parse_responses(body)
125
+ body["results"]&.map { |r| [r["code"], r["body"]] } || []
126
+ end
127
+
128
+ def error_response(body)
129
+ ShopifyCLI::API::APIRequestError.new(body, response: { body: body })
130
+ end
131
+
132
+ def log(message)
133
+ ctx.debug("[BulkJob ##{object_id}] #{message}")
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ class Uploader
7
+ class BulkRequest
8
+ def initialize(theme, bulk_items)
9
+ @theme = theme
10
+ @bulk_items = bulk_items
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ path: "themes/#{@theme.id}/assets/bulk.json",
16
+ method: "PUT",
17
+ body: JSON.generate({ assets: assets }),
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def assets
24
+ @bulk_items.map(&:asset)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "apply_to_all_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Uploader
9
+ module Forms
10
+ class ApplyToAll
11
+ attr_reader :value
12
+
13
+ def initialize(ctx, number_of_files)
14
+ @ctx = ctx
15
+ @number_of_files = number_of_files
16
+ @value = nil
17
+ @apply = nil
18
+ end
19
+
20
+ def apply?(value)
21
+ return unless @number_of_files > 1
22
+
23
+ if @apply.nil?
24
+ @apply = ask.apply?
25
+ @value = value if @apply
26
+ end
27
+
28
+ @apply
29
+ end
30
+
31
+ private
32
+
33
+ def ask
34
+ ApplyToAllForm.ask(@ctx, [], number_of_files: @number_of_files)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end