shopify-cli 2.12.0 → 2.15.0

Sign up to get free protection for your applications and to get access to all the features.
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)