shopify-cli 2.29.0 → 2.30.0

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