shopify-cli 2.12.0 → 2.15.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +5 -0
  3. data/.github/CONTRIBUTING.md +1 -1
  4. data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  5. data/.github/workflows/shopify.yml +2 -1
  6. data/.github/workflows/stale.yml +41 -0
  7. data/.rubocop.yml +1 -1
  8. data/.ruby-version +1 -1
  9. data/CHANGELOG.md +36 -0
  10. data/Gemfile.lock +18 -18
  11. data/Rakefile +16 -0
  12. data/bin/shopify +4 -4
  13. data/dev.yml +1 -1
  14. data/ext/javy/hashes/javy-arm-macos-v0.2.0.gz.sha256 +1 -0
  15. data/ext/javy/hashes/javy-arm-macos-v0.2.1.gz.sha256 +1 -0
  16. data/ext/javy/hashes/javy-x86_64-linux-v0.2.0.gz.sha256 +1 -0
  17. data/ext/javy/hashes/javy-x86_64-linux-v0.2.1.gz.sha256 +1 -0
  18. data/ext/javy/hashes/javy-x86_64-macos-v0.2.0.gz.sha256 +1 -0
  19. data/ext/javy/hashes/javy-x86_64-macos-v0.2.1.gz.sha256 +1 -0
  20. data/ext/javy/hashes/javy-x86_64-windows-v0.2.0.gz.sha256 +1 -0
  21. data/ext/javy/hashes/javy-x86_64-windows-v0.2.1.gz.sha256 +1 -0
  22. data/ext/javy/version +1 -1
  23. data/lib/project_types/extension/features/argo_setup_steps.rb +4 -6
  24. data/lib/project_types/extension/models/npm_package.rb +19 -1
  25. data/lib/project_types/extension/models/server_config/development_renderer.rb +4 -3
  26. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +13 -0
  27. data/lib/project_types/extension/tasks/configure_features.rb +15 -2
  28. data/lib/project_types/extension/tasks/convert_server_config.rb +2 -1
  29. data/lib/project_types/script/cli.rb +0 -4
  30. data/lib/project_types/script/commands/create.rb +4 -4
  31. data/lib/project_types/script/config/extension_points.yml +0 -6
  32. data/lib/project_types/script/errors.rb +1 -1
  33. data/lib/project_types/script/forms/create.rb +7 -7
  34. data/lib/project_types/script/layers/application/build_script.rb +9 -26
  35. data/lib/project_types/script/layers/application/create_script.rb +9 -10
  36. data/lib/project_types/script/layers/application/project_dependencies.rb +12 -14
  37. data/lib/project_types/script/layers/application/push_script.rb +14 -10
  38. data/lib/project_types/script/layers/domain/errors.rb +3 -3
  39. data/lib/project_types/script/layers/domain/push_package.rb +6 -0
  40. data/lib/project_types/script/layers/domain/script_config.rb +2 -4
  41. data/lib/project_types/script/layers/domain/script_project.rb +3 -2
  42. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +0 -16
  43. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +0 -1
  44. data/lib/project_types/script/layers/infrastructure/languages/typescript_project_creator.rb +19 -4
  45. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +2 -10
  46. data/lib/project_types/script/layers/infrastructure/languages/wasm_project_creator.rb +0 -3
  47. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +1 -1
  48. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +3 -21
  49. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +14 -26
  50. data/lib/project_types/script/layers/infrastructure/script_service.rb +4 -2
  51. data/lib/project_types/script/messages/messages.rb +9 -9
  52. data/lib/project_types/script/ui/error_handler.rb +4 -4
  53. data/lib/project_types/script/ui/strict_spinner.rb +4 -6
  54. data/lib/project_types/theme/cli.rb +2 -0
  55. data/lib/project_types/theme/commands/common/root_helper.rb +11 -5
  56. data/lib/project_types/theme/commands/list.rb +34 -0
  57. data/lib/project_types/theme/commands/open.rb +65 -0
  58. data/lib/project_types/theme/commands/package.rb +1 -0
  59. data/lib/project_types/theme/commands/pull.rb +4 -4
  60. data/lib/project_types/theme/commands/push.rb +4 -4
  61. data/lib/project_types/theme/conversions/base_glob.rb +20 -5
  62. data/lib/project_types/theme/forms/select.rb +11 -39
  63. data/lib/project_types/theme/messages/messages.rb +33 -2
  64. data/lib/project_types/theme/presenters/theme_presenter.rb +48 -0
  65. data/lib/project_types/theme/presenters/themes_presenter.rb +32 -0
  66. data/lib/shopify_cli/api.rb +1 -1
  67. data/lib/shopify_cli/command.rb +1 -7
  68. data/lib/shopify_cli/commands/app/deploy.rb +0 -1
  69. data/lib/shopify_cli/constants.rb +2 -2
  70. data/lib/shopify_cli/context.rb +13 -15
  71. data/lib/shopify_cli/core/entry_point.rb +1 -1
  72. data/lib/shopify_cli/core/monorail.rb +14 -6
  73. data/lib/shopify_cli/environment.rb +19 -11
  74. data/lib/shopify_cli/exception_reporter.rb +2 -0
  75. data/lib/shopify_cli/messages/messages.rb +5 -5
  76. data/lib/shopify_cli/packager.rb +1 -1
  77. data/lib/shopify_cli/result.rb +14 -0
  78. data/lib/shopify_cli/services/app/create/node_service.rb +2 -14
  79. data/lib/shopify_cli/services/app/create/php_service.rb +1 -6
  80. data/lib/shopify_cli/services/app/create/rails_service.rb +5 -13
  81. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +5 -5
  82. data/lib/shopify_cli/theme/dev_server/watcher.rb +10 -2
  83. data/lib/shopify_cli/theme/development_theme.rb +2 -5
  84. data/lib/shopify_cli/theme/syncer.rb +27 -32
  85. data/lib/shopify_cli/theme/theme.rb +16 -27
  86. data/lib/shopify_cli/theme/theme_admin_api.rb +72 -0
  87. data/lib/shopify_cli/transform_data_structure.rb +3 -2
  88. data/lib/shopify_cli/version.rb +1 -1
  89. data/shipit.yml +3 -0
  90. data/shopify-cli.gemspec +9 -2
  91. data/shopify-dev +9 -11
  92. metadata +21 -9
  93. data/lib/project_types/rails/ruby.rb +0 -17
  94. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +0 -21
  95. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +0 -109
@@ -15,7 +15,9 @@ module ShopifyCLI
15
15
  },
16
16
  core: {
17
17
  errors: {
18
- missing_node: "Node is required to continue. Install node here: https://nodejs.org/en/download.",
18
+ missing_node: "Node.js is required to continue. Install Node.js here: https://nodejs.org/en/download.",
19
+ missing_npm: "npm is required to continue. Install npm here: https://www.npmjs.com/get-npm.",
20
+ missing_ruby: "Ruby is required to continue. Install Ruby here: https://www.ruby-lang.org/en/downloads.",
19
21
  option_parser: {
20
22
  invalid_option: "The option {{command:%s}} is not supported.",
21
23
  missing_argument: "The required argument {{command:%s}} is missing.",
@@ -95,8 +97,6 @@ module ShopifyCLI
95
97
  HELP
96
98
  error: {
97
99
  node_required: "node is required to create an app project. Download at https://nodejs.org/en/download.",
98
- node_version_failure: "Failed to get the current node version. Please make sure it is installed as " \
99
- "per the instructions at https://nodejs.org/en.",
100
100
  npm_required: "npm is required to create an app project. Download at https://www.npmjs.com/get-npm.",
101
101
  npm_version_failure: "Failed to get the current npm version. Please make sure it is installed as per " \
102
102
  "the instructions at https://www.npmjs.com/get-npm.",
@@ -131,8 +131,6 @@ module ShopifyCLI
131
131
  {{underline:https://getcomposer.org/download/}}
132
132
  COMPOSER
133
133
  npm_required: "npm is required to create an app project. Download at https://www.npmjs.com/get-npm.",
134
- npm_version_failure: "Failed to get the current npm version. Please make sure it is installed as per " \
135
- "the instructions at https://www.npmjs.com/get-npm.",
136
134
  app_setup: "Failed to set up the app",
137
135
  },
138
136
 
@@ -283,6 +281,8 @@ module ShopifyCLI
283
281
  localization: {
284
282
  error: {
285
283
  bundle_too_large: "Total size of all locale files must be less than %s.",
284
+ duplicate_locale_code: "Duplicate locale found: `%s`; locale codes"\
285
+ " should be unique and are case insensitive.",
286
286
  file_empty: "Locale file `%s` is empty.",
287
287
  file_too_large: "Locale file `%s` too large; size must be less than %s.",
288
288
  invalid_file_extension: "Invalid locale filename: `%s`; only .json files are allowed.",
@@ -81,7 +81,7 @@ module ShopifyCLI
81
81
  puts "Grabbing sha256 checksum from Rubygems.org"
82
82
  require "digest/sha2"
83
83
  require "open-uri"
84
- gem_checksum = open("https://rubygems.org/downloads/shopify-cli-#{ShopifyCLI::VERSION}.gem") do |io|
84
+ gem_checksum = URI.open("https://rubygems.org/downloads/shopify-cli-#{ShopifyCLI::VERSION}.gem") do |io|
85
85
  Digest::SHA256.new.hexdigest(io.read)
86
86
  end
87
87
 
@@ -181,6 +181,13 @@ module ShopifyCLI
181
181
  self
182
182
  end
183
183
 
184
+ ##
185
+ # returns the value this success represents
186
+ #
187
+ def unwrap!
188
+ value
189
+ end
190
+
184
191
  ##
185
192
  # returns the success value and ignores the fallback value that was either
186
193
  # provided as a method argument or by passing a block. However, the caller
@@ -339,6 +346,13 @@ module ShopifyCLI
339
346
  raise ArgumentError, "expected either a fallback value or a block" unless (args.length == 1) ^ block
340
347
  block ? block.call(@error) : args.pop
341
348
  end
349
+
350
+ ##
351
+ # raises the error this failure represents
352
+ #
353
+ def unwrap!
354
+ raise error
355
+ end
342
356
  end
343
357
 
344
358
  ##
@@ -88,24 +88,12 @@ module ShopifyCLI
88
88
  end
89
89
 
90
90
  def check_node
91
- cmd_path = context.which("node")
92
- context.abort(context.message("core.app.create.node.error.node_required")) if cmd_path.nil?
93
-
94
- version, stat = context.capture2e("node", "-v")
95
- unless stat.success?
96
- context.abort(context.message("core.app.create.node.error.node_version_failure"))
97
- end
98
-
91
+ version = ShopifyCLI::Environment.node_version(context: context)
99
92
  context.done(context.message("core.app.create.node.node_version", version))
100
93
  end
101
94
 
102
95
  def check_npm
103
- cmd_path = context.which("npm")
104
- context.abort(context.message("core.app.create.node.error.npm_required")) if cmd_path.nil?
105
-
106
- version, stat = context.capture2e("npm", "-v")
107
- context.abort(context.message("core.app.create.node.error.npm_version_failure")) unless stat.success?
108
-
96
+ version = ShopifyCLI::Environment.npm_version(context: context)
109
97
  context.done(context.message("core.app.create.node.npm_version", version))
110
98
  end
111
99
 
@@ -69,12 +69,7 @@ module ShopifyCLI
69
69
  end
70
70
 
71
71
  def check_npm
72
- cmd_path = context.which("npm")
73
- context.abort(context.message("core.app.create.php.error.npm_required")) if cmd_path.nil?
74
-
75
- version, stat = context.capture2e("npm", "-v")
76
- context.abort(context.message("core.app.create.php.error.npm_version_failure")) unless stat.success?
77
-
72
+ version = ShopifyCLI::Environment.npm_version(context: context)
78
73
  context.done(context.message("core.app.create.php.npm_version", version))
79
74
  end
80
75
 
@@ -108,8 +108,8 @@ module ShopifyCLI
108
108
  end
109
109
 
110
110
  def check_ruby
111
- ruby_version = Rails::Ruby.version(context)
112
- return if ruby_version.satisfies?("~>2.5") || ruby_version.satisfies?("~>3.0.0")
111
+ ruby_version = Environment.ruby_version(context)
112
+ return if ruby_version.satisfies?("~>2.5") || ruby_version.satisfies?("~>3.1.0")
113
113
  context.abort(context.message("core.app.create.rails.error.invalid_ruby_version"))
114
114
  end
115
115
 
@@ -219,18 +219,10 @@ module ShopifyCLI
219
219
  end
220
220
 
221
221
  def install_webpacker?
222
- rails_version < ::Semantic::Version.new("7.0.0") &&
223
- !File.exist?(File.join(context.root, "config/webpacker.yml"))
224
- end
225
-
226
- def rails_version
227
- output, status = context.capture2e("rails", "--version")
228
- unless status.success?
229
- context.abort(context.message("core.app.create.rails.error.install_failure", "rails"))
230
- end
222
+ rails_version = Environment.rails_version(context: context)
223
+ webpacker_config = File.exist?(File.join(context.root, "config/webpacker.yml"))
231
224
 
232
- version = output.scan(/Rails \d+\.\d+\.\d+/).first.split(" ").last
233
- ::Semantic::Version.new(version)
225
+ rails_version < ::Semantic::Version.new("7.0.0") && !webpacker_config
234
226
  end
235
227
  end
236
228
  end
@@ -28,6 +28,10 @@ module ShopifyCLI
28
28
 
29
29
  private
30
30
 
31
+ def api_client
32
+ @api_client ||= ThemeAdminAPI.new(@ctx, @theme.shop)
33
+ end
34
+
31
35
  def updated_file?(body, file)
32
36
  remote_checksum = body.dig("asset", "checksum")
33
37
  local_checksum = file.checksum
@@ -45,12 +49,8 @@ module ShopifyCLI
45
49
  end
46
50
 
47
51
  def fetch_asset(file)
48
- ShopifyCLI::AdminAPI.rest_request(
49
- @ctx,
50
- shop: @theme.shop,
52
+ api_client.get(
51
53
  path: "themes/#{@theme.id}/assets.json",
52
- method: "GET",
53
- api_version: "unstable",
54
54
  query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
55
55
  )
56
56
  rescue ShopifyCLI::API::APIRequestNotFoundError
@@ -45,13 +45,21 @@ module ShopifyCLI
45
45
  def filter_theme_files(files)
46
46
  files
47
47
  .select { |file| @theme.theme_file?(file) }
48
- .reject { |file| @ignore_filter&.ignore?(file) }
48
+ .map { |file| @theme[file] }
49
+ .reject { |file| ignore_file?(file) }
49
50
  end
50
51
 
51
52
  def filter_remote_files(files)
52
53
  files
53
54
  .select { |file| @syncer.remote_file?(file) }
54
- .reject { |file| @ignore_filter&.ignore?(file) }
55
+ .map { |file| @theme[file] }
56
+ .reject { |file| ignore_file?(file) }
57
+ end
58
+
59
+ private
60
+
61
+ def ignore_file?(file)
62
+ @ignore_filter&.ignore?(file.relative_path.to_s)
55
63
  end
56
64
  end
57
65
  end
@@ -45,11 +45,8 @@ module ShopifyCLI
45
45
  def exists?
46
46
  return false unless id
47
47
 
48
- ShopifyCLI::AdminAPI.rest_request(
49
- @ctx,
50
- shop: shop,
51
- path: "themes/#{id}.json",
52
- api_version: "unstable",
48
+ api_client.get(
49
+ path: "themes/#{id}.json"
53
50
  )
54
51
  rescue ShopifyCLI::API::APIRequestNotFoundError
55
52
  false
@@ -7,15 +7,15 @@ require "forwardable"
7
7
  require_relative "syncer/error_reporter"
8
8
  require_relative "syncer/standard_reporter"
9
9
  require_relative "syncer/operation"
10
+ require_relative "theme_admin_api"
10
11
 
11
12
  module ShopifyCLI
12
13
  module Theme
13
14
  class Syncer
14
15
  extend Forwardable
15
16
 
16
- API_VERSION = "unstable"
17
-
18
17
  attr_reader :checksums
18
+ attr_reader :checksums_mutex
19
19
  attr_accessor :include_filter
20
20
  attr_accessor :ignore_filter
21
21
 
@@ -39,6 +39,9 @@ module ShopifyCLI
39
39
  # Mutex used to pause all threads when backing-off when hitting API rate limits
40
40
  @backoff_mutex = Mutex.new
41
41
 
42
+ # Mutex used to coordinate changes in the checksums (shared accross all threads)
43
+ @checksums_mutex = Mutex.new
44
+
42
45
  # Latest theme assets checksums. Updated on each upload.
43
46
  @checksums = {}
44
47
 
@@ -46,12 +49,16 @@ module ShopifyCLI
46
49
  @error_checksums = []
47
50
  end
48
51
 
52
+ def api_client
53
+ @api_client ||= ThemeAdminAPI.new(@ctx, @theme.shop)
54
+ end
55
+
49
56
  def lock_io!
50
- @reporters.each { |reporter| reporter.disable! }
57
+ @reporters.each(&:disable!)
51
58
  end
52
59
 
53
60
  def unlock_io!
54
- @reporters.each { |reporter| reporter.enable! }
61
+ @reporters.each(&:enable!)
55
62
  end
56
63
 
57
64
  def enqueue_updates(files)
@@ -96,11 +103,8 @@ module ShopifyCLI
96
103
  end
97
104
 
98
105
  def fetch_checksums!
99
- _status, response = ShopifyCLI::AdminAPI.rest_request(
100
- @ctx,
101
- shop: @theme.shop,
102
- path: "themes/#{@theme.id}/assets.json",
103
- api_version: API_VERSION,
106
+ _status, response = api_client.get(
107
+ path: "themes/#{@theme.id}/assets.json"
104
108
  )
105
109
  update_checksums(response)
106
110
  end
@@ -118,7 +122,7 @@ module ShopifyCLI
118
122
  operation = @queue.pop
119
123
  break if operation.nil? # shutdown was called
120
124
  perform(operation)
121
- rescue Exception => e
125
+ rescue Exception => e # rubocop:disable Lint/RescueException
122
126
  error_suffix = ": #{e}"
123
127
  error_suffix += + "\n\t#{e.backtrace.join("\n\t")}" if @ctx.debug?
124
128
  report_error(operation, error_suffix)
@@ -239,12 +243,8 @@ module ShopifyCLI
239
243
  asset[:attachment] = Base64.encode64(file.read)
240
244
  end
241
245
 
242
- _status, body, response = ShopifyCLI::AdminAPI.rest_request(
243
- @ctx,
244
- shop: @theme.shop,
246
+ _status, body, response = api_client.put(
245
247
  path: "themes/#{@theme.id}/assets.json",
246
- method: "PUT",
247
- api_version: API_VERSION,
248
248
  body: JSON.generate(asset: asset)
249
249
  )
250
250
 
@@ -272,23 +272,19 @@ module ShopifyCLI
272
272
  end
273
273
 
274
274
  def ignored_by_include_filter?(path)
275
- include_filter && !include_filter.match?(path)
275
+ !!include_filter && !include_filter.match?(path)
276
276
  end
277
277
 
278
278
  def get(file)
279
- _status, body, response = ShopifyCLI::AdminAPI.rest_request(
280
- @ctx,
281
- shop: @theme.shop,
279
+ _status, body, response = api_client.get(
282
280
  path: "themes/#{@theme.id}/assets.json",
283
- method: "GET",
284
- api_version: API_VERSION,
285
281
  query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
286
282
  )
287
283
 
288
284
  update_checksums(body)
289
285
 
290
286
  attachment = body.dig("asset", "attachment")
291
- value = if attachment
287
+ if attachment
292
288
  file.write(Base64.decode64(attachment))
293
289
  else
294
290
  file.write(body.dig("asset", "value"))
@@ -298,14 +294,10 @@ module ShopifyCLI
298
294
  end
299
295
 
300
296
  def delete(file)
301
- _status, _body, response = ShopifyCLI::AdminAPI.rest_request(
302
- @ctx,
303
- shop: @theme.shop,
297
+ _status, _body, response = api_client.delete(
304
298
  path: "themes/#{@theme.id}/assets.json",
305
- method: "DELETE",
306
- api_version: API_VERSION,
307
299
  body: JSON.generate(asset: {
308
- key: file.relative_path.to_s
300
+ key: file.relative_path.to_s,
309
301
  })
310
302
  )
311
303
 
@@ -314,14 +306,17 @@ module ShopifyCLI
314
306
 
315
307
  def update_checksums(api_response)
316
308
  api_response.values.flatten.each do |asset|
317
- if asset["key"]
309
+ next unless asset["key"]
310
+ checksums_mutex.synchronize do
318
311
  @checksums[asset["key"]] = asset["checksum"]
319
312
  end
320
313
  end
321
314
  # Generate .liquid asset files are reported twice in checksum:
322
315
  # once of generated, once for .liquid. We only keep the .liquid, that's the one we have
323
316
  # on disk.
324
- @checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
317
+ checksums_mutex.synchronize do
318
+ @checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
319
+ end
325
320
  end
326
321
 
327
322
  def file_has_changed?(file)
@@ -332,7 +327,7 @@ module ShopifyCLI
332
327
  parsed_body = JSON.parse(exception&.response&.body)
333
328
  message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
334
329
  # Truncate to first lines
335
- [message].flatten.map { |message| message.split("\n", 2).first }
330
+ [message].flatten.map { |mess| mess.split("\n", 2).first }
336
331
  rescue JSON::ParserError
337
332
  [exception.message]
338
333
  end
@@ -340,7 +335,7 @@ module ShopifyCLI
340
335
  def backoff_if_near_limit!(used, limit)
341
336
  if used > limit - @threads.size
342
337
  @ctx.debug("Near API call limit, waiting 2 sec…")
343
- @backoff_mutex.synchronize { sleep 2 }
338
+ @backoff_mutex.synchronize { sleep(2) }
344
339
  end
345
340
  end
346
341
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "file"
3
+ require_relative "theme_admin_api"
3
4
 
4
5
  require "pathname"
5
6
  require "time"
@@ -59,7 +60,7 @@ module ShopifyCLI
59
60
  end
60
61
 
61
62
  def shop
62
- AdminAPI.get_shop_or_abort(@ctx)
63
+ api_client.get_shop_or_abort
63
64
  end
64
65
 
65
66
  def editor_url
@@ -101,9 +102,7 @@ module ShopifyCLI
101
102
  def create
102
103
  raise InvalidThemeRole, "Can't create live theme. Use publish." if live?
103
104
 
104
- _status, body = ShopifyCLI::AdminAPI.rest_request(
105
- @ctx,
106
- shop: shop,
105
+ _status, body = api_client.post(
107
106
  path: "themes.json",
108
107
  body: JSON.generate({
109
108
  theme: {
@@ -111,31 +110,21 @@ module ShopifyCLI
111
110
  role: role,
112
111
  },
113
112
  }),
114
- method: "POST",
115
- api_version: "unstable",
116
113
  )
117
114
 
118
115
  @id = body["theme"]["id"]
119
116
  end
120
117
 
121
118
  def delete
122
- AdminAPI.rest_request(
123
- @ctx,
124
- shop: shop,
125
- method: "DELETE",
126
- path: "themes/#{id}.json",
127
- api_version: "unstable",
119
+ api_client.delete(
120
+ path: "themes/#{id}.json"
128
121
  )
129
122
  end
130
123
 
131
124
  def publish
132
125
  return if live?
133
- AdminAPI.rest_request(
134
- @ctx,
135
- shop: shop,
136
- method: "PUT",
126
+ api_client.put(
137
127
  path: "themes/#{id}.json",
138
- api_version: "unstable",
139
128
  body: JSON.generate(theme: {
140
129
  role: "main",
141
130
  })
@@ -205,23 +194,23 @@ module ShopifyCLI
205
194
  end
206
195
 
207
196
  def fetch_themes(ctx)
208
- AdminAPI.rest_request(
209
- ctx,
210
- shop: AdminAPI.get_shop_or_abort(ctx),
211
- path: "themes.json",
212
- api_version: "unstable",
197
+ api_client = ThemeAdminAPI.new(ctx)
198
+
199
+ api_client.get(
200
+ path: "themes.json"
213
201
  )
214
202
  end
215
203
  end
216
204
 
217
205
  private
218
206
 
207
+ def api_client
208
+ @api_client ||= ThemeAdminAPI.new(@ctx)
209
+ end
210
+
219
211
  def load_info_from_api
220
- _status, body = AdminAPI.rest_request(
221
- @ctx,
222
- shop: shop,
223
- path: "themes/#{id}.json",
224
- api_version: "unstable",
212
+ _status, body = api_client.get(
213
+ path: "themes/#{id}.json"
225
214
  )
226
215
 
227
216
  @name = body.dig("theme", "name")
@@ -0,0 +1,72 @@
1
+ module ShopifyCLI
2
+ module Theme
3
+ class ThemeAdminAPI
4
+ API_VERSION = "unstable"
5
+
6
+ attr_reader :shop
7
+
8
+ def initialize(ctx, shop = nil)
9
+ @ctx = ctx
10
+ @shop = shop || get_shop_or_abort
11
+ end
12
+
13
+ def get(path:, **args)
14
+ rest_request(method: "GET", path: path, **args)
15
+ end
16
+
17
+ def put(path:, **args)
18
+ rest_request(method: "PUT", path: path, **args)
19
+ end
20
+
21
+ def post(path:, **args)
22
+ rest_request(method: "POST", path: path, **args)
23
+ end
24
+
25
+ def delete(path:, **args)
26
+ rest_request(method: "DELETE", path: path, **args)
27
+ end
28
+
29
+ def get_shop_or_abort # rubocop:disable Naming/AccessorMethodName
30
+ ShopifyCLI::AdminAPI.get_shop_or_abort(@ctx)
31
+ end
32
+
33
+ private
34
+
35
+ def rest_request(**args)
36
+ ShopifyCLI::AdminAPI.rest_request(
37
+ @ctx,
38
+ shop: @shop,
39
+ api_version: API_VERSION,
40
+ **args.compact
41
+ )
42
+ rescue ShopifyCLI::API::APIRequestForbiddenError,
43
+ ShopifyCLI::API::APIRequestUnauthorizedError => error
44
+ # The Admin API returns 403 Forbidden responses on different
45
+ # scenarios:
46
+ #
47
+ # * when a user doesn't have permissions for a request:
48
+ # <APIRequestForbiddenError: 403 {}>
49
+ #
50
+ # * when an asset operation cannot be performed:
51
+ # <APIRequestForbiddenError: 403 {"message":"templates/gift_card.liquid could not be deleted"}>
52
+ if empty_response_error?(error)
53
+ return handle_permissions_error
54
+ end
55
+
56
+ raise error
57
+ end
58
+
59
+ def handle_permissions_error
60
+ ensure_user_error = @ctx.message("theme.ensure_user_error", shop)
61
+ ensure_user_try_this = @ctx.message("theme.ensure_user_try_this")
62
+
63
+ @ctx.abort(ensure_user_error, ensure_user_try_this)
64
+ end
65
+
66
+ def empty_response_error?(error)
67
+ error_message = error&.response&.body.to_s
68
+ error_message.empty?
69
+ end
70
+ end
71
+ end
72
+ end
@@ -40,6 +40,7 @@ module ShopifyCLI
40
40
 
41
41
  property! :underscore_keys, accepts: [true, false], default: false, reader: :underscore_keys?
42
42
  property! :symbolize_keys, accepts: [true, false], default: false, reader: :symbolize_keys?
43
+ property! :shallow, accepts: [true, false], default: false, reader: :shallow?
43
44
  property! :associative_array_container,
44
45
  accepts: ->(c) { c.respond_to?(:new) && c.method_defined?(:[]=) },
45
46
  default: -> { Hash }
@@ -47,10 +48,10 @@ module ShopifyCLI
47
48
  def call(object)
48
49
  case object
49
50
  when Array
50
- object.map(&self).map(&:value)
51
+ shallow? ? object.dup : object.map(&self).map(&:value)
51
52
  when Hash
52
53
  object.each.with_object(associative_array_container.new) do |(key, value), result|
53
- result[transform_key(key)] = call(value).value
54
+ result[transform_key(key)] = shallow? ? value : call(value).value
54
55
  end
55
56
  else
56
57
  ShopifyCLI::Result.success(object)
@@ -1,3 +1,3 @@
1
1
  module ShopifyCLI
2
- VERSION = "2.12.0"
2
+ VERSION = "2.15.0"
3
3
  end
data/shipit.yml ADDED
@@ -0,0 +1,3 @@
1
+ deploy:
2
+ post:
3
+ - bundle exec rake notify_version_to_bugsnag
data/shopify-cli.gemspec CHANGED
@@ -35,11 +35,18 @@ Gem::Specification.new do |spec|
35
35
  spec.require_paths = ["lib", "vendor"]
36
36
  spec.executables << "shopify"
37
37
 
38
- spec.add_development_dependency("bundler", "~> 2.2.2")
38
+ spec.add_development_dependency("bundler", "~> 2.3.8")
39
39
  spec.add_development_dependency("rake", "~> 12.3", ">= 12.3.3")
40
40
  spec.add_development_dependency("minitest", "~> 5.0")
41
41
 
42
42
  spec.add_dependency("bugsnag", "~> 6.22")
43
43
  spec.add_dependency("listen", "~> 3.7.0")
44
- spec.add_dependency("theme-check", "~> 1.9.0")
44
+
45
+ # We prefer being more strict here with the version range to have a more deterministic build.
46
+ # The added benefit is that, if the user upgrades the CLI, and we have "~> 1.10.1" version range,
47
+ # they will get a theme-check update.
48
+ # Whereas if we were to have "~> 1.9", that version would still be satisfied and thus not upgraded.
49
+ # Both shopify-cli and theme-check gems are owned and developed by Shopify.
50
+ # These gems are currently being actively developed and it's easiest to update them together.
51
+ spec.add_dependency("theme-check", "~> 1.10.1")
45
52
  end
data/shopify-dev CHANGED
@@ -3,16 +3,14 @@
3
3
  require_relative "./bin/load_shopify"
4
4
 
5
5
  exit(proc do
6
- begin
7
- ShopifyCLI::ErrorHandler.call do
8
- ShopifyCLI::Core::EntryPoint.call(ARGV.dup)
9
- end
10
- rescue StandardError => error
11
- ShopifyCLI::ErrorHandler.exception = error
12
- if ShopifyCLI::Environment.print_stacktrace?
13
- raise error
14
- else
15
- 1
16
- end
6
+ ShopifyCLI::ErrorHandler.call do
7
+ ShopifyCLI::Core::EntryPoint.call(ARGV.dup)
17
8
  end
9
+ rescue StandardError => error
10
+ ShopifyCLI::ErrorHandler.exception = error
11
+ if ShopifyCLI::Environment.print_stacktrace?
12
+ raise error
13
+ else
14
+ 1
15
+ end
18
16
  end.call)