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.
- 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
|