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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +1 -1
- data/lib/project_types/theme/commands/package.rb +20 -5
- data/lib/project_types/theme/messages/messages.rb +4 -2
- data/lib/shopify_cli/packager.rb +5 -14
- data/lib/shopify_cli/theme/backoff_helper.rb +47 -0
- data/lib/shopify_cli/theme/ignore_helper.rb +7 -1
- data/lib/shopify_cli/theme/syncer/downloader.rb +63 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk.rb +133 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk_item.rb +64 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk_job.rb +139 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk_request.rb +30 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all.rb +41 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all_form.rb +37 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/base_strategy_form.rb +64 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/select_delete_strategy.rb +29 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/select_update_strategy.rb +30 -0
- data/lib/shopify_cli/theme/syncer/uploader/json_delete_handler.rb +49 -0
- data/lib/shopify_cli/theme/syncer/uploader/json_update_handler.rb +71 -0
- data/lib/shopify_cli/theme/syncer/uploader.rb +227 -0
- data/lib/shopify_cli/theme/syncer.rb +91 -144
- data/lib/shopify_cli/version.rb +1 -1
- metadata +16 -16
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +0 -39
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +0 -35
- data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +0 -62
- data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +0 -27
- data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +0 -28
- data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +0 -51
- data/lib/shopify_cli/theme/syncer/json_update_handler.rb +0 -96
- data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk.rb +0 -102
- data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk_job.rb +0 -75
- data/lib/shopify_cli/theme/theme_admin_api_throttler/errors.rb +0 -7
- data/lib/shopify_cli/theme/theme_admin_api_throttler/put_request.rb +0 -52
- data/lib/shopify_cli/theme/theme_admin_api_throttler/request_parser.rb +0 -39
- data/lib/shopify_cli/theme/theme_admin_api_throttler/response_parser.rb +0 -21
- 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,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
|