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