shopify-cli 2.15.1 → 2.15.2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +13 -0
  5. data/lib/project_types/extension/messages/messages.rb +1 -1
  6. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  7. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  8. data/lib/project_types/script/messages/messages.rb +3 -0
  9. data/lib/project_types/script/ui/error_handler.rb +11 -0
  10. data/lib/project_types/theme/commands/serve.rb +1 -0
  11. data/lib/project_types/theme/messages/messages.rb +40 -8
  12. data/lib/shopify_cli/git.rb +36 -0
  13. data/lib/shopify_cli/messages/messages.rb +5 -4
  14. data/lib/shopify_cli/release.rb +120 -20
  15. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  16. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  17. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  18. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  19. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  20. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +34 -0
  21. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  22. data/lib/shopify_cli/theme/dev_server/watcher.rb +1 -1
  23. data/lib/shopify_cli/theme/dev_server.rb +15 -3
  24. data/lib/shopify_cli/theme/file.rb +15 -4
  25. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  26. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  27. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  28. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  29. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  30. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  31. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  32. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  33. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  34. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  35. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  36. data/lib/shopify_cli/theme/syncer.rb +79 -63
  37. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  38. data/lib/shopify_cli/thread_pool.rb +15 -3
  39. data/lib/shopify_cli/version.rb +1 -1
  40. metadata +15 -2
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Merger
9
+ class << self
10
+ ##
11
+ # Merge `theme_file` with the `new_content` by relying on the union merge
12
+ #
13
+ def union_merge(theme_file, new_content)
14
+ git_merge(theme_file, new_content, ["--union", "-p"])
15
+ end
16
+
17
+ private
18
+
19
+ ##
20
+ # Merge theme file (`ShopifyCLI::Theme::File`) with a new content (String),
21
+ # by creating a temporary file based on the `new_content`.
22
+ #
23
+ def git_merge(theme_file, new_content, opts)
24
+ remote_file = create_tmp_file(tmp_file_name(theme_file), new_content)
25
+ empty_file = create_tmp_file("empty")
26
+
27
+ ShopifyCLI::Git.merge_file(
28
+ theme_file.absolute_path,
29
+ empty_file.path,
30
+ remote_file.path,
31
+ opts
32
+ )
33
+ ensure
34
+ # Remove temporary files on Windows as well
35
+ remote_file.close!
36
+ empty_file.close!
37
+ end
38
+
39
+ def create_tmp_file(basename, content = "")
40
+ tmp_file = Tempfile.new(basename)
41
+ tmp_file.write(content)
42
+ tmp_file.close # Make it ready to merge
43
+ tmp_file
44
+ end
45
+
46
+ def tmp_file_name(ref_file)
47
+ "shopify-cli-merge-#{ref_file.name(".*")}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -35,7 +35,7 @@ module ShopifyCLI
35
35
  end
36
36
 
37
37
  def file_path
38
- file&.relative_path.to_s
38
+ file&.relative_path
39
39
  end
40
40
 
41
41
  private
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "thread"
3
4
  require "json"
4
5
  require "base64"
5
6
  require "forwardable"
6
7
 
8
+ require_relative "syncer/checksums"
7
9
  require_relative "syncer/error_reporter"
8
- require_relative "syncer/standard_reporter"
10
+ require_relative "syncer/ignore_helper"
11
+ require_relative "syncer/json_delete_handler"
12
+ require_relative "syncer/json_update_handler"
13
+ require_relative "syncer/merger"
9
14
  require_relative "syncer/operation"
15
+ require_relative "syncer/standard_reporter"
10
16
  require_relative "theme_admin_api"
11
17
 
12
18
  module ShopifyCLI
@@ -14,36 +20,46 @@ module ShopifyCLI
14
20
  class Syncer
15
21
  extend Forwardable
16
22
 
17
- attr_reader :checksums
18
- attr_reader :checksums_mutex
19
- attr_accessor :include_filter
20
- attr_accessor :ignore_filter
23
+ include IgnoreHelper
24
+ include JsonDeleteHandler
25
+ include JsonUpdateHandler
26
+
27
+ QUEUEABLE_METHODS = [
28
+ :get, # - Updates the local file with the remote file content
29
+ :update, # - Updates the remote file with the local file content
30
+ :delete, # - Deletes the remote file
31
+ :union_merge, # - Union merges the local file content with the remote file content
32
+ ]
33
+
34
+ attr_reader :theme, :checksums, :error_checksums
35
+ attr_accessor :include_filter, :ignore_filter
21
36
 
22
37
  def_delegators :@error_reporter, :has_any_error?
23
38
 
24
- def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil)
39
+ def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil, overwrite_json: true)
25
40
  @ctx = ctx
26
41
  @theme = theme
27
42
  @include_filter = include_filter
28
43
  @ignore_filter = ignore_filter
44
+ @overwrite_json = overwrite_json
29
45
  @error_reporter = ErrorReporter.new(ctx)
30
46
  @standard_reporter = StandardReporter.new(ctx)
31
47
  @reporters = [@error_reporter, @standard_reporter]
32
48
 
33
49
  # Queue of `Operation`s waiting to be picked up from a thread for processing.
34
50
  @queue = Queue.new
51
+
35
52
  # `Operation`s will be removed from this Array completed.
36
53
  @pending = []
54
+
37
55
  # Thread making the API requests.
38
56
  @threads = []
57
+
39
58
  # Mutex used to pause all threads when backing-off when hitting API rate limits
40
59
  @backoff_mutex = Mutex.new
41
60
 
42
- # Mutex used to coordinate changes in the checksums (shared accross all threads)
43
- @checksums_mutex = Mutex.new
44
-
45
61
  # Latest theme assets checksums. Updated on each upload.
46
- @checksums = {}
62
+ @checksums = Checksums.new(theme)
47
63
 
48
64
  # Checksums of assets with errors.
49
65
  @error_checksums = []
@@ -73,6 +89,10 @@ module ShopifyCLI
73
89
  files.each { |file| enqueue(:delete, file) }
74
90
  end
75
91
 
92
+ def enqueue_union_merges(files)
93
+ files.each { |file| enqueue(:union_merge, file) }
94
+ end
95
+
76
96
  def size
77
97
  @pending.size
78
98
  end
@@ -86,7 +106,11 @@ module ShopifyCLI
86
106
  end
87
107
 
88
108
  def remote_file?(file)
89
- checksums.key?(@theme[file].relative_path.to_s)
109
+ checksums.has?(file)
110
+ end
111
+
112
+ def broken_file?(file)
113
+ error_checksums.include?(checksums[file.relative_path])
90
114
  end
91
115
 
92
116
  def wait!
@@ -135,20 +159,18 @@ module ShopifyCLI
135
159
  fetch_checksums!
136
160
 
137
161
  if delete
138
- # Delete remote files not present locally
139
- removed_files = checksums.keys - @theme.theme_files.map { |file| file.relative_path.to_s }
162
+ removed_json_files, removed_files = checksums
163
+ .keys
164
+ .-(@theme.theme_files.map(&:relative_path))
165
+ .map { |file| @theme[file] }
166
+ .partition(&:json?)
167
+
140
168
  enqueue_deletes(removed_files)
169
+ enqueue_json_deletes(removed_json_files)
141
170
  end
142
171
 
143
- # Some files must be uploaded after the other ones
144
- delayed_config_files = [
145
- @theme["config/settings_schema.json"],
146
- @theme["config/settings_data.json"],
147
- ]
148
-
149
172
  enqueue_updates(@theme.liquid_files)
150
- enqueue_updates(@theme.json_files - delayed_config_files)
151
- enqueue_updates(delayed_config_files)
173
+ enqueue_json_updates(@theme.json_files)
152
174
 
153
175
  if delay_low_priority_files
154
176
  # Wait for liquid & JSON files to upload, because those are rendered remotely
@@ -171,7 +193,7 @@ module ShopifyCLI
171
193
  if delete
172
194
  # Delete local files not present remotely
173
195
  missing_files = @theme.theme_files
174
- .reject { |file| checksums.key?(file.relative_path.to_s) }.uniq
196
+ .reject { |file| checksums.has?(file) }.uniq
175
197
  .reject { |file| ignore_file?(file) }
176
198
  missing_files.each do |file|
177
199
  @ctx.debug("rm #{file.relative_path}")
@@ -187,12 +209,13 @@ module ShopifyCLI
187
209
  private
188
210
 
189
211
  def report_error(operation, error_suffix = "")
190
- @error_checksums << @checksums[operation.file_path]
212
+ @error_checksums << checksums[operation.file_path]
191
213
  @error_reporter.report("#{operation.as_error_message}#{error_suffix}")
192
214
  end
193
215
 
194
216
  def enqueue(method, file)
195
217
  raise ArgumentError, "file required" unless file
218
+ raise ArgumentError, "method '#{method}' cannot be queued" unless QUEUEABLE_METHODS.include?(method)
196
219
 
197
220
  operation = Operation.new(@ctx, method, @theme[file])
198
221
 
@@ -204,10 +227,10 @@ module ShopifyCLI
204
227
  return
205
228
  end
206
229
 
207
- if [:update, :get].include?(method) && operation.file.exist? && !file_has_changed?(operation.file)
230
+ if [:update, :get].include?(method) && operation.file.exist?
208
231
  is_fixed = !!@error_checksums.delete(operation.file.checksum)
209
232
  @standard_reporter.report(operation.as_fix_message) if is_fixed
210
- return
233
+ return unless checksums.file_has_changed?(operation.file)
211
234
  end
212
235
 
213
236
  @pending << operation
@@ -236,7 +259,7 @@ module ShopifyCLI
236
259
  end
237
260
 
238
261
  def update(file)
239
- asset = { key: file.relative_path.to_s }
262
+ asset = { key: file.relative_path }
240
263
  if file.text?
241
264
  asset[:value] = file.read
242
265
  else
@@ -253,32 +276,10 @@ module ShopifyCLI
253
276
  response
254
277
  end
255
278
 
256
- def ignore_operation?(operation)
257
- path = operation.file_path
258
- ignore_path?(path)
259
- end
260
-
261
- def ignore_file?(file)
262
- path = file.path
263
- ignore_path?(path)
264
- end
265
-
266
- def ignore_path?(path)
267
- ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
268
- end
269
-
270
- def ignored_by_ignore_filter?(path)
271
- ignore_filter&.ignore?(path)
272
- end
273
-
274
- def ignored_by_include_filter?(path)
275
- !!include_filter && !include_filter.match?(path)
276
- end
277
-
278
279
  def get(file)
279
280
  _status, body, response = api_client.get(
280
281
  path: "themes/#{@theme.id}/assets.json",
281
- query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
282
+ query: URI.encode_www_form("asset[key]" => file.relative_path),
282
283
  )
283
284
 
284
285
  update_checksums(body)
@@ -297,37 +298,48 @@ module ShopifyCLI
297
298
  _status, _body, response = api_client.delete(
298
299
  path: "themes/#{@theme.id}/assets.json",
299
300
  body: JSON.generate(asset: {
300
- key: file.relative_path.to_s,
301
+ key: file.relative_path,
301
302
  })
302
303
  )
303
304
 
304
305
  response
305
306
  end
306
307
 
308
+ def union_merge(file)
309
+ _status, body, response = api_client.get(
310
+ path: "themes/#{@theme.id}/assets.json",
311
+ query: URI.encode_www_form("asset[key]" => file.relative_path),
312
+ )
313
+
314
+ return response unless file.text?
315
+
316
+ remote_content = body.dig("asset", "value")
317
+
318
+ return response if remote_content.nil?
319
+
320
+ content = Merger.union_merge(file, remote_content)
321
+
322
+ file.write(content)
323
+
324
+ enqueue(:update, file)
325
+
326
+ response
327
+ end
328
+
307
329
  def update_checksums(api_response)
308
330
  api_response.values.flatten.each do |asset|
309
331
  next unless asset["key"]
310
- checksums_mutex.synchronize do
311
- @checksums[asset["key"]] = asset["checksum"]
312
- end
313
- end
314
- # Generate .liquid asset files are reported twice in checksum:
315
- # once of generated, once for .liquid. We only keep the .liquid, that's the one we have
316
- # on disk.
317
- checksums_mutex.synchronize do
318
- @checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
332
+ checksums[asset["key"]] = asset["checksum"]
319
333
  end
320
- end
321
334
 
322
- def file_has_changed?(file)
323
- file.checksum != @checksums[file.relative_path.to_s]
335
+ checksums.reject_duplicated_checksums!
324
336
  end
325
337
 
326
338
  def parse_api_errors(exception)
327
339
  parsed_body = JSON.parse(exception&.response&.body)
328
340
  message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
329
341
  # Truncate to first lines
330
- [message].flatten.map { |mess| mess.split("\n", 2).first }
342
+ [message].flatten.map { |m| m.split("\n", 2).first }
331
343
  rescue JSON::ParserError
332
344
  [exception.message]
333
345
  end
@@ -339,6 +351,10 @@ module ShopifyCLI
339
351
  end
340
352
  end
341
353
 
354
+ def overwrite_json?
355
+ @overwrite_json
356
+ end
357
+
342
358
  def backingoff?
343
359
  @backoff_mutex.locked?
344
360
  end
@@ -3,10 +3,14 @@
3
3
  module ShopifyCLI
4
4
  class ThreadPool
5
5
  class Job
6
- attr_reader :error
6
+ attr_reader :error, :interval
7
+
8
+ def initialize(interval = 0)
9
+ @interval = interval
10
+ end
7
11
 
8
12
  def perform!
9
- raise "`#{self.class.name}#perform!` must be defined"
13
+ raise "`#{self.class.name}#perform!' must be defined"
10
14
  end
11
15
 
12
16
  def call
@@ -22,6 +26,10 @@ module ShopifyCLI
22
26
  def error?
23
27
  !!@error
24
28
  end
29
+
30
+ def recurring?
31
+ !interval.zero?
32
+ end
25
33
  end
26
34
  end
27
35
  end
@@ -27,11 +27,23 @@ module ShopifyCLI
27
27
  def spawn_thread
28
28
  Thread.new do
29
29
  catch(:stop_thread) do
30
- loop do
31
- @jobs.pop.call
32
- end
30
+ loop { perform(@jobs.pop) }
33
31
  end
34
32
  end
35
33
  end
34
+
35
+ def perform(job)
36
+ job.call
37
+ reschedule(job) if job.recurring?
38
+ end
39
+
40
+ def reschedule(job)
41
+ wait(job.interval)
42
+ schedule(job)
43
+ end
44
+
45
+ def wait(seconds)
46
+ sleep(seconds)
47
+ end
36
48
  end
37
49
  end
@@ -1,3 +1,3 @@
1
1
  module ShopifyCLI
2
- VERSION = "2.15.1"
2
+ VERSION = "2.15.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.1
4
+ version: 2.15.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-24 00:00:00.000000000 Z
11
+ date: 2022-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -490,10 +490,13 @@ files:
490
490
  - lib/shopify_cli/theme/dev_server/hot-reload.js
491
491
  - lib/shopify_cli/theme/dev_server/hot_reload.rb
492
492
  - lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb
493
+ - lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb
493
494
  - lib/shopify_cli/theme/dev_server/local_assets.rb
494
495
  - lib/shopify_cli/theme/dev_server/proxy.rb
495
496
  - lib/shopify_cli/theme/dev_server/proxy/template_param_builder.rb
496
497
  - lib/shopify_cli/theme/dev_server/reload_mode.rb
498
+ - lib/shopify_cli/theme/dev_server/remote_watcher.rb
499
+ - lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb
497
500
  - lib/shopify_cli/theme/dev_server/sse.rb
498
501
  - lib/shopify_cli/theme/dev_server/watcher.rb
499
502
  - lib/shopify_cli/theme/dev_server/web_server.rb
@@ -504,7 +507,17 @@ files:
504
507
  - lib/shopify_cli/theme/include_filter.rb
505
508
  - lib/shopify_cli/theme/mime_type.rb
506
509
  - lib/shopify_cli/theme/syncer.rb
510
+ - lib/shopify_cli/theme/syncer/checksums.rb
507
511
  - lib/shopify_cli/theme/syncer/error_reporter.rb
512
+ - lib/shopify_cli/theme/syncer/forms/apply_to_all.rb
513
+ - lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb
514
+ - lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb
515
+ - lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb
516
+ - lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb
517
+ - lib/shopify_cli/theme/syncer/ignore_helper.rb
518
+ - lib/shopify_cli/theme/syncer/json_delete_handler.rb
519
+ - lib/shopify_cli/theme/syncer/json_update_handler.rb
520
+ - lib/shopify_cli/theme/syncer/merger.rb
508
521
  - lib/shopify_cli/theme/syncer/operation.rb
509
522
  - lib/shopify_cli/theme/syncer/standard_reporter.rb
510
523
  - lib/shopify_cli/theme/theme.rb