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