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
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCLI
|
4
|
+
module Theme
|
5
|
+
class Syncer
|
6
|
+
class Uploader
|
7
|
+
module Forms
|
8
|
+
class ApplyToAllForm < ShopifyCLI::Form
|
9
|
+
attr_accessor :apply
|
10
|
+
flag_arguments :number_of_files
|
11
|
+
|
12
|
+
def ask
|
13
|
+
title = message("title", number_of_files - 1)
|
14
|
+
|
15
|
+
self.apply = CLI::UI::Prompt.ask(title, allow_empty: false) do |handler|
|
16
|
+
handler.option(message("yes")) { true }
|
17
|
+
handler.option(message("no")) { false }
|
18
|
+
end
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply?
|
24
|
+
apply
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def message(key, *params)
|
30
|
+
ctx.message("theme.serve.syncer.forms.apply_to_all.#{key}", *params)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCLI
|
4
|
+
module Theme
|
5
|
+
class Syncer
|
6
|
+
class Uploader
|
7
|
+
module Forms
|
8
|
+
class BaseStrategyForm < ShopifyCLI::Form
|
9
|
+
attr_accessor :strategy
|
10
|
+
|
11
|
+
def ask
|
12
|
+
ctx.puts(title_context(file))
|
13
|
+
|
14
|
+
self.strategy = CLI::UI::Prompt.ask(title_question, allow_empty: false) do |handler|
|
15
|
+
strategies.each do |strategy|
|
16
|
+
handler.option(as_text(strategy)) { strategy }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
exit_cli if self.strategy == :exit
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
##
|
28
|
+
# List of strategies that populate the form options
|
29
|
+
#
|
30
|
+
def strategies
|
31
|
+
raise "`#{self.class.name}#strategies' must be defined"
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Message prefix for the form title and options (strategies).
|
36
|
+
# See the methods `title` and `as_text`
|
37
|
+
#
|
38
|
+
def prefix
|
39
|
+
raise "`#{self.class.name}#prefix' must be defined"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def exit_cli
|
45
|
+
exit(0)
|
46
|
+
end
|
47
|
+
|
48
|
+
def title_context(file)
|
49
|
+
ctx.message("#{prefix}.title_context", file.relative_path)
|
50
|
+
end
|
51
|
+
|
52
|
+
def title_question
|
53
|
+
ctx.message("#{prefix}.title_question")
|
54
|
+
end
|
55
|
+
|
56
|
+
def as_text(strategy)
|
57
|
+
ctx.message("#{prefix}.#{strategy}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_strategy_form"
|
4
|
+
|
5
|
+
module ShopifyCLI
|
6
|
+
module Theme
|
7
|
+
class Syncer
|
8
|
+
class Uploader
|
9
|
+
module Forms
|
10
|
+
class SelectDeleteStrategy < BaseStrategyForm
|
11
|
+
flag_arguments :file
|
12
|
+
|
13
|
+
def strategies
|
14
|
+
%i[
|
15
|
+
delete
|
16
|
+
restore
|
17
|
+
exit
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def prefix
|
22
|
+
"theme.serve.syncer.forms.delete_strategy"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_strategy_form"
|
4
|
+
|
5
|
+
module ShopifyCLI
|
6
|
+
module Theme
|
7
|
+
class Syncer
|
8
|
+
class Uploader
|
9
|
+
module Forms
|
10
|
+
class SelectUpdateStrategy < BaseStrategyForm
|
11
|
+
flag_arguments :file, :exists_remotely
|
12
|
+
|
13
|
+
def strategies
|
14
|
+
%i[
|
15
|
+
keep_remote
|
16
|
+
keep_local
|
17
|
+
union_merge
|
18
|
+
exit
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def prefix
|
23
|
+
"theme.serve.syncer.forms.#{exists_remotely ? "update_strategy" : "update_remote_deleted_strategy"}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
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
|
+
class Uploader
|
10
|
+
module JsonDeleteHandler
|
11
|
+
def enqueue_json_deletes(files)
|
12
|
+
return enqueue_deletes(files) if overwrite_json?
|
13
|
+
|
14
|
+
# Handle conflicts when JSON files cannot be overwritten
|
15
|
+
handle_delete_conflicts(files)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def handle_delete_conflicts(files)
|
21
|
+
to_delete = []
|
22
|
+
to_get = []
|
23
|
+
|
24
|
+
apply_to_all = Forms::ApplyToAll.new(ctx, files.size)
|
25
|
+
|
26
|
+
files.each do |file|
|
27
|
+
delete_strategy = apply_to_all.value || ask_delete_strategy(file)
|
28
|
+
apply_to_all.apply?(delete_strategy)
|
29
|
+
|
30
|
+
case delete_strategy
|
31
|
+
when :delete
|
32
|
+
to_delete << file
|
33
|
+
when :restore
|
34
|
+
to_get << file
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
enqueue_deletes(to_delete)
|
39
|
+
enqueue_get(to_get)
|
40
|
+
end
|
41
|
+
|
42
|
+
def ask_delete_strategy(file)
|
43
|
+
Forms::SelectDeleteStrategy.ask(ctx, [], file: file).strategy
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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
|
+
class Uploader
|
10
|
+
module JsonUpdateHandler
|
11
|
+
def enqueue_json_updates(files)
|
12
|
+
return enqueue_updates(files) if overwrite_json?
|
13
|
+
|
14
|
+
# Handle conflicts when JSON files cannot be overwritten
|
15
|
+
handle_update_conflicts(files)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def handle_update_conflicts(files)
|
21
|
+
to_get = []
|
22
|
+
to_delete = []
|
23
|
+
to_update = []
|
24
|
+
to_union_merge = []
|
25
|
+
|
26
|
+
apply_to_all = Forms::ApplyToAll.new(ctx, files.size)
|
27
|
+
|
28
|
+
files.each do |file|
|
29
|
+
update_strategy = apply_to_all.value || ask_update_strategy(file)
|
30
|
+
apply_to_all.apply?(update_strategy)
|
31
|
+
|
32
|
+
case update_strategy
|
33
|
+
when :keep_remote
|
34
|
+
if file_exist_remotely?(file)
|
35
|
+
to_get << file
|
36
|
+
else
|
37
|
+
delete_locally(file)
|
38
|
+
end
|
39
|
+
when :keep_local
|
40
|
+
to_update << file
|
41
|
+
when :union_merge
|
42
|
+
if file_exist_remotely?(file)
|
43
|
+
to_union_merge << file
|
44
|
+
else
|
45
|
+
to_update << file
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
enqueue_get(to_get)
|
51
|
+
enqueue_deletes(to_delete)
|
52
|
+
enqueue_updates(to_update)
|
53
|
+
enqueue_union_merges(to_union_merge)
|
54
|
+
end
|
55
|
+
|
56
|
+
def file_exist_remotely?(file)
|
57
|
+
!checksums[file.relative_path].nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete_locally(file)
|
61
|
+
::File.delete(file.absolute_path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def ask_update_strategy(file)
|
65
|
+
Forms::SelectUpdateStrategy.ask(ctx, [], file: file, exists_remotely: file_exist_remotely?(file)).strategy
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
require_relative "uploader/bulk_item"
|
6
|
+
require_relative "uploader/bulk"
|
7
|
+
require_relative "uploader/json_delete_handler"
|
8
|
+
require_relative "uploader/json_update_handler"
|
9
|
+
|
10
|
+
module ShopifyCLI
|
11
|
+
module Theme
|
12
|
+
class Syncer
|
13
|
+
class Uploader
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
include JsonDeleteHandler
|
17
|
+
include JsonUpdateHandler
|
18
|
+
|
19
|
+
attr_reader :syncer
|
20
|
+
|
21
|
+
def_delegators :syncer,
|
22
|
+
# helpers
|
23
|
+
:ctx,
|
24
|
+
:api_client,
|
25
|
+
:theme,
|
26
|
+
:ignore_file?,
|
27
|
+
:overwrite_json?,
|
28
|
+
:bulk_updates_activated?,
|
29
|
+
|
30
|
+
# enqueue
|
31
|
+
:enqueue_deletes,
|
32
|
+
:enqueue_get,
|
33
|
+
:enqueue_union_merges,
|
34
|
+
:enqueue_updates,
|
35
|
+
|
36
|
+
# checksums
|
37
|
+
:checksums,
|
38
|
+
:update_checksums,
|
39
|
+
:fetch_checksums!,
|
40
|
+
:wait!
|
41
|
+
|
42
|
+
def initialize(syncer, delete, delay_low_priority_files, &update_progress_bar_block)
|
43
|
+
@syncer = syncer
|
44
|
+
@delete = delete
|
45
|
+
@delay_low_priority_files = delay_low_priority_files
|
46
|
+
@update_progress_bar_block = update_progress_bar_block
|
47
|
+
@progress_bar_mutex = Mutex.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def upload!
|
51
|
+
fetch_checksums!
|
52
|
+
delete_files!
|
53
|
+
|
54
|
+
if bulk_updates_activated? && overwrite_json?
|
55
|
+
bulk_upload!
|
56
|
+
else
|
57
|
+
async_upload!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete_files!
|
62
|
+
return unless delete?
|
63
|
+
|
64
|
+
files_present_remotely = checksums.keys
|
65
|
+
files_present_locally = theme.theme_files.map(&:relative_path)
|
66
|
+
|
67
|
+
json_files, other_files = (files_present_remotely - files_present_locally)
|
68
|
+
.map { |file| theme[file] }
|
69
|
+
.reject { |file| ignore_file?(file) }
|
70
|
+
.partition(&:json?)
|
71
|
+
|
72
|
+
enqueue_deletes(other_files)
|
73
|
+
enqueue_json_deletes(json_files)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def bulk_upload!
|
79
|
+
update_progress_bar!
|
80
|
+
|
81
|
+
enqueue_bulk_updates(liquid_files)
|
82
|
+
enqueue_bulk_updates(json_files)
|
83
|
+
enqueue_bulk_updates(config_files)
|
84
|
+
|
85
|
+
if delay_low_priority_files?
|
86
|
+
# Process lower-priority files (assets) in the background, as they
|
87
|
+
# are served locally
|
88
|
+
enqueue_updates(static_asset_files)
|
89
|
+
else
|
90
|
+
enqueue_bulk_updates(static_asset_files)
|
91
|
+
end
|
92
|
+
|
93
|
+
wait!(&@update_progress_bar_block) unless delay_low_priority_files?
|
94
|
+
end
|
95
|
+
|
96
|
+
def async_upload!
|
97
|
+
enqueue_updates(liquid_files)
|
98
|
+
enqueue_json_updates(json_files)
|
99
|
+
enqueue_updates(config_files)
|
100
|
+
|
101
|
+
# Wait upload of Liquid & JSON files, as they are rendered remotely
|
102
|
+
wait!(&@update_progress_bar_block) if delay_low_priority_files?
|
103
|
+
|
104
|
+
# Process lower-priority files (assets) in the background, as they
|
105
|
+
# are served locally
|
106
|
+
enqueue_updates(static_asset_files)
|
107
|
+
|
108
|
+
wait!(&@update_progress_bar_block) unless delay_low_priority_files?
|
109
|
+
end
|
110
|
+
|
111
|
+
def enqueue_bulk_updates(files)
|
112
|
+
retries = 0
|
113
|
+
pending_items = files.map { |file| bulk_item(file) }
|
114
|
+
|
115
|
+
while pending_items.any? && retries < 4
|
116
|
+
bulk = Bulk.new(ctx, theme, api_client)
|
117
|
+
|
118
|
+
files
|
119
|
+
.map { |file| bulk_item(file) }
|
120
|
+
.each { |request| bulk.enqueue(request) }
|
121
|
+
|
122
|
+
bulk.shutdown
|
123
|
+
|
124
|
+
retries += 1
|
125
|
+
pending_items = bulk.remaining_items
|
126
|
+
end
|
127
|
+
|
128
|
+
return unless pending_items.any?
|
129
|
+
|
130
|
+
# Remaining items are handled in the background when the bulk timeout
|
131
|
+
# is exceeded
|
132
|
+
pending_items.size.times { update_progress_bar! }
|
133
|
+
|
134
|
+
syncer.enqueue_updates(pending_items.map(&:file))
|
135
|
+
syncer.wait!
|
136
|
+
end
|
137
|
+
|
138
|
+
def bulk_item(file)
|
139
|
+
BulkItem.new(file) do |_s, body, response|
|
140
|
+
if response.is_a?(StandardError)
|
141
|
+
report(file, response)
|
142
|
+
else
|
143
|
+
update_checksums(body)
|
144
|
+
end
|
145
|
+
ensure
|
146
|
+
update_progress_bar!
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def delete?
|
151
|
+
@delete
|
152
|
+
end
|
153
|
+
|
154
|
+
def delay_low_priority_files?
|
155
|
+
@delay_low_priority_files
|
156
|
+
end
|
157
|
+
|
158
|
+
# Files
|
159
|
+
|
160
|
+
def number_of_bulk_items
|
161
|
+
@number_of_files ||= [
|
162
|
+
json_files.size,
|
163
|
+
liquid_files.size,
|
164
|
+
config_files.size,
|
165
|
+
delay_low_priority_files? ? 0 : static_asset_files.size,
|
166
|
+
].reduce(:+)
|
167
|
+
end
|
168
|
+
|
169
|
+
def json_files
|
170
|
+
@json_files ||= uploadable(theme.json_files) - config_files
|
171
|
+
end
|
172
|
+
|
173
|
+
def liquid_files
|
174
|
+
@liquid_files ||= uploadable(theme.liquid_files)
|
175
|
+
end
|
176
|
+
|
177
|
+
def static_asset_files
|
178
|
+
@static_asset_files ||= uploadable(theme.static_asset_files)
|
179
|
+
end
|
180
|
+
|
181
|
+
def config_files
|
182
|
+
@config_files ||= uploadable(
|
183
|
+
[
|
184
|
+
theme["config/settings_schema.json"],
|
185
|
+
theme["config/settings_data.json"],
|
186
|
+
]
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
def uploadable(files)
|
191
|
+
files.select { |file| uploadable?(file) }
|
192
|
+
end
|
193
|
+
|
194
|
+
def uploadable?(file)
|
195
|
+
return false unless file.exist?
|
196
|
+
return false if ignore_file?(file)
|
197
|
+
|
198
|
+
checksums.file_has_changed?(file)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Handle prorgress bar
|
202
|
+
|
203
|
+
def update_progress_bar!
|
204
|
+
@pending_files ||= number_of_bulk_items
|
205
|
+
@pending_files -= 1
|
206
|
+
|
207
|
+
# Avoid abrupt updates in the progress bar
|
208
|
+
@progress_bar_mutex.synchronize do
|
209
|
+
sleep(0.02)
|
210
|
+
update_progress_bar(@pending_files, number_of_bulk_items)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def update_progress_bar(size, total)
|
215
|
+
@update_progress_bar_block.call(size, total)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Handler errors
|
219
|
+
|
220
|
+
def report(file, _error)
|
221
|
+
error_message = "The asset #{file.relative_path} could not be uploaded.\n#{e.inspect}"
|
222
|
+
syncer.report_file_error(file, error_message)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|