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
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "forms/apply_to_all"
4
- require_relative "forms/select_delete_strategy"
5
-
6
- module ShopifyCLI
7
- module Theme
8
- class Syncer
9
- module JsonDeleteHandler
10
- def enqueue_json_deletes(files)
11
- files = files.select { |file| !ignore_file?(file) }
12
-
13
- if overwrite_json?
14
- enqueue_deletes(files)
15
- else
16
- # Handle conflicts when JSON files cannot be overwritten
17
- handle_delete_conflicts(files)
18
- end
19
- end
20
-
21
- private
22
-
23
- def handle_delete_conflicts(files)
24
- to_delete = []
25
- to_get = []
26
-
27
- apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
28
-
29
- files.each do |file|
30
- delete_strategy = apply_to_all.value || ask_delete_strategy(file)
31
- apply_to_all.apply?(delete_strategy)
32
-
33
- case delete_strategy
34
- when :delete
35
- to_delete << file
36
- when :restore
37
- to_get << file
38
- end
39
- end
40
-
41
- enqueue_deletes(to_delete)
42
- enqueue_get(to_get)
43
- end
44
-
45
- def ask_delete_strategy(file)
46
- Forms::SelectDeleteStrategy.ask(@ctx, [], file: file).strategy
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,96 +0,0 @@
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
- # Update remote JSON files and delays `delayed_files` update
12
- files = files
13
- .select { |file| ready_to_update?(file) }
14
- .sort_by { |file| delayed_files.include?(file) ? 1 : 0 }
15
- .reject { |file| overwrite_json? && delayed_files.include?(file) }
16
-
17
- if overwrite_json?
18
- enqueue_updates(files)
19
- else
20
- # Handle conflicts when JSON files cannot be overwritten
21
- handle_update_conflicts(files)
22
- end
23
- end
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
-
40
- private
41
-
42
- def handle_update_conflicts(files)
43
- to_get = []
44
- to_delete = []
45
- to_update = []
46
- to_union_merge = []
47
-
48
- apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
49
-
50
- files.each do |file|
51
- update_strategy = apply_to_all.value || ask_update_strategy(file)
52
- apply_to_all.apply?(update_strategy)
53
-
54
- case update_strategy
55
- when :keep_remote
56
- if file_exist_remotely?(file)
57
- to_get << file
58
- else
59
- delete_locally(file)
60
- end
61
- when :keep_local
62
- to_update << file
63
- when :union_merge
64
- if file_exist_remotely?(file)
65
- to_union_merge << file
66
- else
67
- to_update << file
68
- end
69
- end
70
- end
71
-
72
- enqueue_get(to_get)
73
- enqueue_deletes(to_delete)
74
- enqueue_updates(to_update)
75
- enqueue_union_merges(to_union_merge)
76
- end
77
-
78
- def file_exist_remotely?(file)
79
- !checksums[file.relative_path].nil?
80
- end
81
-
82
- def delete_locally(file)
83
- ::File.delete(file.absolute_path)
84
- end
85
-
86
- def ask_update_strategy(file)
87
- Forms::SelectUpdateStrategy.ask(@ctx, [], file: file, exists_remotely: file_exist_remotely?(file)).strategy
88
- end
89
-
90
- def ready_to_update?(file)
91
- !ignore_file?(file) && file.exist? && checksums.file_has_changed?(file)
92
- end
93
- end
94
- end
95
- end
96
- end
@@ -1,102 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "bulk_job"
4
- require "shopify_cli/thread_pool"
5
-
6
- module ShopifyCLI
7
- module Theme
8
- class ThemeAdminAPIThrottler
9
- class Bulk
10
- MAX_BULK_BYTESIZE = 10_485_760 # 10MB
11
- MAX_BULK_FILES = 20 # files
12
- QUEUE_TIMEOUT = 0.2 # 200ms
13
-
14
- attr_accessor :admin_api
15
-
16
- def initialize(ctx, admin_api, pool_size: 20)
17
- @ctx = ctx
18
- @admin_api = admin_api
19
- @latest_enqueued_at = now
20
-
21
- @thread_pool = ShopifyCLI::ThreadPool.new(pool_size: pool_size)
22
-
23
- pool_size.times do
24
- @thread_pool.schedule(
25
- BulkJob.new(ctx, self)
26
- )
27
- end
28
-
29
- @put_requests = []
30
- @mut = Mutex.new
31
- end
32
-
33
- def enqueue(put_request)
34
- @mut.synchronize do
35
- @latest_enqueued_at = now
36
- @put_requests << put_request
37
- end
38
- end
39
-
40
- def shutdown
41
- wait_put_requests
42
- @thread_pool.shutdown
43
- end
44
-
45
- def consume_put_requests
46
- to_batch = []
47
- to_batch_size_bytes = 0
48
- @mut.synchronize do
49
- # sort requests to perform less retries at the `bulk_job` level
50
- @put_requests.sort_by! { |r| r.liquid? ? 0 : 1 }
51
-
52
- is_ready = false
53
- until is_ready || @put_requests.empty?
54
- request = @put_requests.first
55
- if to_batch.empty? && request.size > MAX_BULK_BYTESIZE
56
- is_ready = true
57
- to_batch << request
58
- to_batch_size_bytes += request.size
59
- @put_requests.shift
60
- elsif to_batch.size + 1 > MAX_BULK_FILES || to_batch_size_bytes + request.size > MAX_BULK_BYTESIZE
61
- is_ready = true
62
- else
63
- to_batch << request
64
- to_batch_size_bytes += request.size
65
- @put_requests.shift
66
- end
67
- end
68
- end
69
- [to_batch, to_batch_size_bytes]
70
- end
71
-
72
- def ready?
73
- queue_timeout? || bulk_size >= MAX_BULK_FILES || bulk_bytesize >= MAX_BULK_BYTESIZE
74
- end
75
-
76
- def bulk_bytesize
77
- @put_requests.map(&:size).reduce(:+).to_i
78
- end
79
-
80
- private
81
-
82
- def bulk_size
83
- @put_requests.size
84
- end
85
-
86
- def queue_timeout?
87
- return false if bulk_size.zero?
88
- elapsed_time = now - @latest_enqueued_at
89
- elapsed_time > QUEUE_TIMEOUT
90
- end
91
-
92
- def wait_put_requests
93
- sleep(0.2) until @put_requests.empty?
94
- end
95
-
96
- def now
97
- Time.now.to_f
98
- end
99
- end
100
- end
101
- end
102
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shopify_cli/thread_pool/job"
4
- require_relative "request_parser"
5
- require_relative "response_parser"
6
- require_relative "errors"
7
-
8
- module ShopifyCLI
9
- module Theme
10
- class ThemeAdminAPIThrottler
11
- class BulkJob < ShopifyCLI::ThreadPool::Job
12
- JOB_TIMEOUT = 0.2 # 200ms
13
- MAX_RETRIES = 10
14
-
15
- attr_reader :bulk
16
-
17
- def initialize(ctx, bulk)
18
- super(JOB_TIMEOUT)
19
- @ctx = ctx
20
- @bulk = bulk
21
-
22
- # Mutex used to coordinate changes performed by the bulk item block
23
- @block_mutex = Mutex.new
24
- end
25
-
26
- def perform!
27
- return unless bulk.ready?
28
- put_requests, bulk_size = bulk.consume_put_requests
29
- return if put_requests.empty?
30
-
31
- @ctx.debug("[BulkJob] size: #{put_requests.size}, bytesize: #{bulk_size}")
32
- bulk_status, bulk_body, response = rest_request(put_requests)
33
-
34
- if bulk_status == 207
35
- responses(bulk_body).each_with_index do |tuple, index|
36
- status, body = tuple
37
- put_request = put_requests[index]
38
- if status == 200 || put_request.retries >= MAX_RETRIES
39
- @block_mutex.synchronize do
40
- if status == 200
41
- @ctx.debug("[BulkJob] asset saved: #{put_request.key}")
42
- put_request.block.call(status, body, response)
43
- else
44
- @ctx.debug("[BulkJob] asset continuing with error: #{put_request.key}")
45
- err = AssetUploadError.new(body, response: { body: body })
46
- put_request.block.call(status, {}, err)
47
- end
48
- end
49
- else
50
- @ctx.debug("[BulkJob] asset error: #{put_request.key}")
51
- @block_mutex.synchronize do
52
- put_request.retries += 1
53
- bulk.enqueue(put_request)
54
- end
55
- end
56
- end
57
- else
58
- @ctx.puts(@ctx.message("theme.stable_flag_suggestion"))
59
- end
60
- end
61
-
62
- private
63
-
64
- def rest_request(put_requests)
65
- request = RequestParser.new(put_requests).parse
66
- bulk.admin_api.rest_request(**request)
67
- end
68
-
69
- def responses(response_body)
70
- ResponseParser.new(response_body).parse
71
- end
72
- end
73
- end
74
- end
75
- end
@@ -1,7 +0,0 @@
1
- module ShopifyCLI
2
- module Theme
3
- class ThemeAdminAPIThrottler
4
- class AssetUploadError < ShopifyCLI::API::APIRequestError; end
5
- end
6
- end
7
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shopify_cli/thread_pool/job"
4
- require_relative "request_parser"
5
- require_relative "response_parser"
6
-
7
- module ShopifyCLI
8
- module Theme
9
- class ThemeAdminAPIThrottler
10
- class PutRequest
11
- attr_reader :method, :body, :path, :block
12
- attr_accessor :retries
13
-
14
- def initialize(path, body, &block)
15
- @method = "PUT"
16
- @path = path
17
- @body = body
18
- @block = block
19
- @retries = 0
20
- end
21
-
22
- def to_h
23
- {
24
- method: method,
25
- path: path,
26
- body: body,
27
- }
28
- end
29
-
30
- def to_s
31
- "#{key}, retries: #{retries}"
32
- end
33
-
34
- def liquid?
35
- key.end_with?(".liquid")
36
- end
37
-
38
- def key
39
- @key ||= JSON.parse(body)["asset"]["key"]
40
- end
41
-
42
- def bulk_path
43
- path.gsub(/.json$/, "/bulk.json")
44
- end
45
-
46
- def size
47
- @size ||= body.bytesize
48
- end
49
- end
50
- end
51
- end
52
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ShopifyCLI
4
- module Theme
5
- class ThemeAdminAPIThrottler
6
- class RequestParser
7
- def initialize(requests)
8
- @requests = requests
9
- end
10
-
11
- def parse
12
- {
13
- path: path,
14
- method: method,
15
- body: JSON.generate({ assets: assets }),
16
- }
17
- end
18
-
19
- private
20
-
21
- def method
22
- @requests.sample.method
23
- end
24
-
25
- def path
26
- @requests.sample.bulk_path
27
- end
28
-
29
- def assets
30
- @requests.map do |request|
31
- body = JSON.parse(request.body)
32
- body = body.is_a?(Hash) ? body : JSON.parse(body)
33
- body["asset"]
34
- end
35
- end
36
- end
37
- end
38
- end
39
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ShopifyCLI
4
- module Theme
5
- class ThemeAdminAPIThrottler
6
- class ResponseParser
7
- def initialize(response_body)
8
- @response_body = response_body
9
- end
10
-
11
- def parse
12
- result = []
13
- @response_body["results"]&.each do |resp|
14
- result << [resp["code"], resp["body"]]
15
- end
16
- result
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "forwardable"
4
-
5
- require_relative "theme_admin_api_throttler/bulk"
6
- require_relative "theme_admin_api_throttler/put_request"
7
-
8
- module ShopifyCLI
9
- module Theme
10
- class ThemeAdminAPIThrottler
11
- extend Forwardable
12
-
13
- attr_reader :bulk, :admin_api
14
-
15
- def_delegators :@admin_api, :get, :post, :delete
16
-
17
- def initialize(ctx, admin_api, active = true)
18
- @ctx = ctx
19
- @admin_api = admin_api
20
- @active = active
21
- @bulk = Bulk.new(ctx, admin_api)
22
- end
23
-
24
- def put(path:, **args, &block)
25
- request = PutRequest.new(path, args[:body], &block)
26
- if active?
27
- bulk_request(request)
28
- else
29
- rest_request(request)
30
- end
31
- end
32
-
33
- def activate_throttler!
34
- @active = true
35
- end
36
-
37
- def deactivate_throttler!
38
- @active = false
39
- end
40
-
41
- def active?
42
- @active
43
- end
44
-
45
- def shutdown
46
- bulk.shutdown
47
- end
48
-
49
- private
50
-
51
- def rest_request(request)
52
- request.block.call(admin_api.rest_request(**request.to_h))
53
- rescue StandardError => error
54
- request.block.call(500, {}, error)
55
- end
56
-
57
- def bulk_request(request)
58
- bulk.enqueue(request)
59
- end
60
- end
61
- end
62
- end