shopify-cli 2.10.1 → 2.11.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +117 -0
  3. data/.github/ISSUE_TEMPLATE/enhancement.yaml +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature.yaml +47 -0
  5. data/.github/ISSUE_TEMPLATE.md +18 -0
  6. data/CHANGELOG.md +38 -3
  7. data/Gemfile.lock +1 -1
  8. data/bin/shopify +9 -0
  9. data/dev.yml +3 -0
  10. data/lib/project_types/extension/commands/check.rb +2 -0
  11. data/lib/project_types/extension/commands/create.rb +2 -0
  12. data/lib/project_types/extension/commands/push.rb +15 -0
  13. data/lib/project_types/extension/commands/serve.rb +2 -0
  14. data/lib/project_types/extension/loaders/project.rb +28 -8
  15. data/lib/project_types/extension/messages/messages.rb +10 -2
  16. data/lib/project_types/extension/models/specification_handlers/default.rb +1 -1
  17. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +7 -1
  18. data/lib/project_types/extension/tasks/convert_server_config.rb +3 -1
  19. data/lib/project_types/script/cli.rb +5 -0
  20. data/lib/project_types/script/commands/connect.rb +3 -1
  21. data/lib/project_types/script/commands/create.rb +2 -0
  22. data/lib/project_types/script/commands/push.rb +6 -0
  23. data/lib/project_types/script/config/extension_points.yml +12 -0
  24. data/lib/project_types/script/graphql/module_upload_url_generate.graphql +5 -1
  25. data/lib/project_types/script/layers/application/build_script.rb +6 -2
  26. data/lib/project_types/script/layers/application/create_script.rb +1 -1
  27. data/lib/project_types/script/layers/application/project_dependencies.rb +1 -1
  28. data/lib/project_types/script/layers/application/push_script.rb +39 -31
  29. data/lib/project_types/script/layers/domain/errors.rb +7 -1
  30. data/lib/project_types/script/layers/domain/extension_point.rb +2 -2
  31. data/lib/project_types/script/layers/infrastructure/errors.rb +13 -3
  32. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +3 -16
  33. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +1 -0
  34. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +35 -8
  35. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +3 -16
  36. data/lib/project_types/script/layers/infrastructure/languages/wasm_project_creator.rb +15 -0
  37. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +32 -0
  38. data/lib/project_types/script/layers/infrastructure/metadata_repository.rb +18 -0
  39. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +1 -1
  40. data/lib/project_types/script/layers/infrastructure/script_service.rb +12 -8
  41. data/lib/project_types/script/layers/infrastructure/script_uploader.rb +22 -9
  42. data/lib/project_types/script/loaders/project.rb +2 -1
  43. data/lib/project_types/script/messages/messages.rb +92 -84
  44. data/lib/project_types/script/ui/error_handler.rb +39 -14
  45. data/lib/project_types/theme/commands/check.rb +2 -0
  46. data/lib/project_types/theme/commands/delete.rb +2 -0
  47. data/lib/project_types/theme/commands/init.rb +2 -0
  48. data/lib/project_types/theme/commands/language_server.rb +2 -0
  49. data/lib/project_types/theme/commands/package.rb +2 -0
  50. data/lib/project_types/theme/commands/publish.rb +2 -0
  51. data/lib/project_types/theme/commands/pull.rb +9 -2
  52. data/lib/project_types/theme/commands/push.rb +7 -4
  53. data/lib/project_types/theme/commands/serve.rb +2 -0
  54. data/lib/shopify_cli/command/sub_command.rb +2 -0
  55. data/lib/shopify_cli/command.rb +74 -0
  56. data/lib/shopify_cli/commands/app/create/node.rb +3 -0
  57. data/lib/shopify_cli/commands/app/create/rails.rb +3 -0
  58. data/lib/shopify_cli/commands/app/deploy.rb +2 -0
  59. data/lib/shopify_cli/commands/app/serve.rb +2 -0
  60. data/lib/shopify_cli/constants.rb +13 -1
  61. data/lib/shopify_cli/environment.rb +55 -35
  62. data/lib/shopify_cli/exception_reporter.rb +9 -0
  63. data/lib/shopify_cli/github/issue_url_generator.rb +19 -8
  64. data/lib/shopify_cli/identity_auth/env_auth_token.rb +34 -0
  65. data/lib/shopify_cli/identity_auth.rb +33 -15
  66. data/lib/shopify_cli/messages/messages.rb +3 -2
  67. data/lib/shopify_cli/partners_api.rb +7 -2
  68. data/lib/shopify_cli/services/app/create/rails_service.rb +37 -13
  69. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +63 -0
  70. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +22 -6
  71. data/lib/shopify_cli/theme/dev_server/proxy.rb +4 -5
  72. data/lib/shopify_cli/theme/dev_server.rb +1 -3
  73. data/lib/shopify_cli/theme/development_theme.rb +11 -0
  74. data/lib/shopify_cli/theme/file.rb +4 -0
  75. data/lib/shopify_cli/theme/include_filter.rb +39 -17
  76. data/lib/shopify_cli/theme/theme.rb +0 -4
  77. data/lib/shopify_cli/utilities.rb +7 -0
  78. data/lib/shopify_cli/version.rb +1 -1
  79. data/lib/shopify_cli.rb +1 -0
  80. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +11 -6
  81. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +5 -1
  82. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +6 -4
  83. data/vendor/lib/semantic/version.rb +0 -1
  84. metadata +11 -3
  85. data/lib/project_types/rails/commands/create.rb +0 -210
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "shopify_cli"
3
+ require "semantic/semantic"
3
4
 
4
5
  module ShopifyCLI
5
6
  class Command < CLI::Kit::BaseCommand
@@ -7,6 +8,8 @@ module ShopifyCLI
7
8
  autoload :AppSubCommand, "shopify_cli/command/app_sub_command"
8
9
  autoload :ProjectCommand, "shopify_cli/command/project_command"
9
10
 
11
+ VersionRange = Struct.new(:from, :to, keyword_init: true)
12
+
10
13
  extend Feature::Set
11
14
 
12
15
  attr_writer :ctx
@@ -26,6 +29,8 @@ module ShopifyCLI
26
29
  cmd = new(@ctx)
27
30
  cmd.options.parse(@_options, args)
28
31
  return call_help(command_name) if cmd.options.help
32
+ check_ruby_version
33
+ check_node_version
29
34
  run_prerequisites
30
35
  cmd.call(args, command_name)
31
36
  end
@@ -58,6 +63,75 @@ module ShopifyCLI
58
63
  )
59
64
  end
60
65
 
66
+ def recommend_ruby(from:, to:)
67
+ @compatible_ruby_range = VersionRange.new(
68
+ from: Semantic::Version.new(from),
69
+ to: Semantic::Version.new(to)
70
+ )
71
+ end
72
+
73
+ def recommend_default_ruby_range
74
+ recommend_ruby(
75
+ from: Constants::SupportedVersions::Ruby::FROM,
76
+ to: Constants::SupportedVersions::Ruby::TO
77
+ )
78
+ end
79
+
80
+ def check_ruby_version
81
+ check_version(
82
+ Environment.ruby_version,
83
+ range: @compatible_ruby_range,
84
+ runtime: "Ruby"
85
+ )
86
+ end
87
+
88
+ def recommend_node(from:, to:)
89
+ @compatible_node_range = VersionRange.new(
90
+ from: Semantic::Version.new(from),
91
+ to: Semantic::Version.new(to)
92
+ )
93
+ end
94
+
95
+ def recommend_default_node_range
96
+ recommend_node(
97
+ from: Constants::SupportedVersions::Node::FROM,
98
+ to: Constants::SupportedVersions::Node::TO
99
+ )
100
+ end
101
+
102
+ def check_node_version
103
+ return unless @compatible_node_range
104
+
105
+ context = Context.new
106
+ if context.which("node").nil?
107
+ raise ShopifyCLI::Abort, context.message("core.errors.missing_node")
108
+ end
109
+
110
+ check_version(
111
+ Environment.node_version,
112
+ range: @compatible_node_range,
113
+ runtime: "Node",
114
+ context: context
115
+ )
116
+ end
117
+
118
+ def check_version(version, range:, runtime:, context: Context.new)
119
+ return if Environment.test?
120
+ return if range.nil?
121
+
122
+ version_without_pre_nor_build = Utilities.version_dropping_pre_and_build(version)
123
+ is_higher_than_bottom = version_without_pre_nor_build >= Utilities.version_dropping_pre_and_build(range.from)
124
+ is_lower_than_top = version_without_pre_nor_build < Utilities.version_dropping_pre_and_build(range.to)
125
+ return if is_higher_than_bottom && is_lower_than_top
126
+
127
+ context.warn("Your environment #{runtime} version, #{version},"\
128
+ " is outside of the range supported by the CLI,"\
129
+ " #{range.from}..<#{range.to},"\
130
+ " and might cause incompatibility issues.")
131
+ rescue StandardError => error
132
+ ExceptionReporter.report_error_silently(error)
133
+ end
134
+
61
135
  def prerequisite_task(*tasks_without_args, **tasks_with_args)
62
136
  @prerequisite_tasks ||= []
63
137
  @prerequisite_tasks += tasks_without_args.map { |t| PrerequisiteTask.new(t) }
@@ -5,6 +5,9 @@ module ShopifyCLI
5
5
  class Node < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
7
 
8
+ recommend_default_node_range
9
+ recommend_default_ruby_range
10
+
8
11
  options do |parser, flags|
9
12
  parser.on("--name=NAME") { |t| flags[:name] = t }
10
13
  parser.on("--organization-id=ID") { |id| flags[:organization_id] = id }
@@ -5,6 +5,9 @@ module ShopifyCLI
5
5
  class Rails < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
7
 
8
+ recommend_default_ruby_range
9
+ recommend_default_node_range
10
+
8
11
  options do |parser, flags|
9
12
  parser.on("--name=NAME") { |t| flags[:name] = t }
10
13
  parser.on("--organization-id=ID") { |id| flags[:organization_id] = id }
@@ -4,6 +4,8 @@ module ShopifyCLI
4
4
  class Deploy < ShopifyCLI::Command::AppSubCommand
5
5
  subcommand :Heroku, "heroku", "shopify_cli/commands/app/deploy/heroku"
6
6
 
7
+ recommend_default_ruby_range
8
+
7
9
  def call(args, _name)
8
10
  platform = args.shift
9
11
  case platform
@@ -6,6 +6,8 @@ module ShopifyCLI
6
6
 
7
7
  prerequisite_task :ensure_env, :ensure_dev_store
8
8
 
9
+ recommend_default_ruby_range
10
+
9
11
  options do |parser, flags|
10
12
  parser.on("--host=HOST") do |h|
11
13
  flags[:host] = h.gsub('"', "")
@@ -38,7 +38,7 @@ module ShopifyCLI
38
38
 
39
39
  # When true the CLI points to spin instances of services
40
40
  SPIN = "SPIN"
41
- INFER_SPIN = "INFER_SPIN"
41
+ SPIN_INSTANCE = "SPIN_INSTANCE"
42
42
  SPIN_WORKSPACE = "SPIN_WORKSPACE"
43
43
  SPIN_NAMESPACE = "SPIN_NAMESPACE"
44
44
  SPIN_HOST = "SPIN_HOST"
@@ -58,6 +58,18 @@ module ShopifyCLI
58
58
  MONORAIL_REAL_EVENTS = "MONORAIL_REAL_EVENTS"
59
59
  end
60
60
 
61
+ module SupportedVersions
62
+ module Ruby
63
+ FROM = "2.6.6"
64
+ TO = "3.1.0"
65
+ end
66
+
67
+ module Node
68
+ FROM = "14.5.0"
69
+ TO = "17.0.0"
70
+ end
71
+ end
72
+
61
73
  module Identity
62
74
  CLIENT_ID_DEV = "e5380e02-312a-7408-5718-e07017e9cf52"
63
75
  CLIENT_ID = "fbdb2649-e327-4907-8f67-908d24cfd7e3"
@@ -1,8 +1,29 @@
1
+ require "semantic/semantic"
2
+
1
3
  module ShopifyCLI
2
4
  # The environment module provides an interface to get information from
3
5
  # the environment in which the CLI runs
4
6
  module Environment
5
7
  TRUTHY_ENV_VARIABLE_VALUES = ["1", "true", "TRUE", "yes", "YES"]
8
+ SPIN_OVERRIDE_ENV_NAMES = [
9
+ Constants::EnvironmentVariables::SPIN_WORKSPACE,
10
+ Constants::EnvironmentVariables::SPIN_NAMESPACE,
11
+ Constants::EnvironmentVariables::SPIN_HOST,
12
+ ]
13
+
14
+ def self.ruby_version(context: Context.new)
15
+ out, err, stat = context.capture3('ruby -e "puts RUBY_VERSION"')
16
+ raise ShopifyCLI::Abort, err unless stat.success?
17
+ out = out.gsub('"', "")
18
+ ::Semantic::Version.new(out.chomp)
19
+ end
20
+
21
+ def self.node_version(context: Context.new)
22
+ out, err, stat = context.capture3("node", "--version")
23
+ raise ShopifyCLI::Abort, err unless stat.success?
24
+ out = out.gsub("v", "")
25
+ ::Semantic::Version.new(out.chomp)
26
+ end
6
27
 
7
28
  def self.interactive=(interactive)
8
29
  @interactive = interactive
@@ -71,6 +92,20 @@ module ShopifyCLI
71
92
  end
72
93
  end
73
94
 
95
+ def self.spin_url_override(env_variables: ENV)
96
+ tokens = SPIN_OVERRIDE_ENV_NAMES.map do |name|
97
+ env_variables[name]
98
+ end
99
+
100
+ return if tokens.all?(&:nil?)
101
+
102
+ if tokens.any?(&:nil?)
103
+ raise "To manually target a spin instance, you must set #{SPIN_OVERRIDE_ENV_NAMES}"
104
+ else
105
+ tokens.join(".")
106
+ end
107
+ end
108
+
74
109
  def self.use_spin?(env_variables: ENV)
75
110
  env_variable_truthy?(
76
111
  Constants::EnvironmentVariables::SPIN,
@@ -81,24 +116,31 @@ module ShopifyCLI
81
116
  )
82
117
  end
83
118
 
84
- def self.infer_spin?(env_variables: ENV)
85
- env_variable_truthy?(
86
- Constants::EnvironmentVariables::INFER_SPIN,
87
- env_variables: env_variables
88
- )
89
- end
90
-
91
119
  def self.spin_url(env_variables: ENV)
92
- if infer_spin?(env_variables: env_variables)
93
- %x(spin info fqdn 2> /dev/null).strip
120
+ override = spin_url_override(env_variables: env_variables)
121
+ return override unless override.nil?
122
+
123
+ spin_response = if env_variables.key?(
124
+ Constants::EnvironmentVariables::SPIN_INSTANCE
125
+ )
126
+ spin_show
94
127
  else
95
- spin_workspace = spin_workspace(env_variables: env_variables)
96
- spin_namespace = spin_namespace(env_variables: env_variables)
97
- spin_host = spin_host(env_variables: env_variables)
98
- "#{spin_workspace}.#{spin_namespace}.#{spin_host}"
128
+ spin_show(latest: true)
129
+ end
130
+
131
+ begin
132
+ instance = JSON.parse(spin_response)
133
+ raise "Missing key 'fqdn' from spin show. Actual response: #{instance}" unless instance.include?("fqdn")
134
+ instance["fqdn"]
135
+ rescue => e
136
+ raise "Failed to infer spin environment from spin show response #{spin_response}: #{e}"
99
137
  end
100
138
  end
101
139
 
140
+ def self.spin_show(latest: false)
141
+ latest ? %x(spin show --latest --json) : %x(spin show --json)
142
+ end
143
+
102
144
  def self.send_monorail_events?(env_variables: ENV)
103
145
  env_variable_truthy?(
104
146
  Constants::EnvironmentVariables::MONORAIL_REAL_EVENTS,
@@ -113,27 +155,5 @@ module ShopifyCLI
113
155
  def self.env_variable_truthy?(variable_name, env_variables: ENV)
114
156
  TRUTHY_ENV_VARIABLE_VALUES.include?(env_variables[variable_name.to_s])
115
157
  end
116
-
117
- def self.spin_workspace(env_variables: ENV)
118
- env_value = env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE]
119
- return env_value unless env_value.nil?
120
-
121
- if env_value.nil?
122
- raise "No value set for #{Constants::EnvironmentVariables::SPIN_WORKSPACE}"
123
- end
124
- end
125
-
126
- def self.spin_namespace(env_variables: ENV)
127
- env_value = env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE]
128
- return env_value unless env_value.nil?
129
-
130
- if env_value.nil?
131
- raise "No value set for #{Constants::EnvironmentVariables::SPIN_NAMESPACE}"
132
- end
133
- end
134
-
135
- def self.spin_host(env_variables: ENV)
136
- env_variables[Constants::EnvironmentVariables::SPIN_HOST] || "us.spin.dev"
137
- end
138
158
  end
139
159
  end
@@ -1,5 +1,10 @@
1
1
  module ShopifyCLI
2
2
  module ExceptionReporter
3
+ def self.report_error_silently(error)
4
+ return unless ReportingConfigurationController.reporting_enabled?
5
+ report_to_bugsnag(error: error)
6
+ end
7
+
3
8
  def self.report(error, _logs = nil, _api_key = nil, custom_metadata = {})
4
9
  context = ShopifyCLI::Context.new
5
10
  unless ShopifyCLI::Environment.development?
@@ -19,6 +24,10 @@ module ShopifyCLI
19
24
  return unless reportable_error?(error)
20
25
 
21
26
  return unless report?(context: context)
27
+ report_to_bugsnag(error: error, custom_metadata: custom_metadata)
28
+ end
29
+
30
+ def self.report_to_bugsnag(error:, custom_metadata: {})
22
31
  ENV["BUGSNAG_DISABLE_AUTOCONFIGURE"] = "1"
23
32
  require "bugsnag"
24
33
 
@@ -2,17 +2,28 @@ module ShopifyCLI
2
2
  module GitHub
3
3
  module IssueURLGenerator
4
4
  def self.error_url(error)
5
- title = "#{error.class}: #{error.message}"
5
+ title = "[Bug]: #{error.class}: #{error.message}"
6
6
  labels = "type:bug"
7
- content = File.read(File.join(ShopifyCLI::ROOT, ".github/ISSUE_TEMPLATE.md"))
8
7
 
9
8
  # take at most 5 lines from backtrace
10
- stacktrace = error.backtrace.length < 5 ? error.backtrace : error.backtrace[0..4]
11
- body = stacktrace.join("\n").to_s
12
- output = content.gsub(/<!--Stacktrace(.|\n)*-->/, body)
13
- query = URI.encode_www_form({ title: title, body: output, labels: labels })
14
- url = "#{ShopifyCLI::Constants::Links::NEW_ISSUE}?#{query}"
15
- url
9
+ stacktrace_text =
10
+ if error.backtrace # Sometimes errors seem to appear without backtrace, see https://github.com/Shopify/shopify-cli/issues/1972#issuecomment-1028013630
11
+ stacktrace = error.backtrace.length < 5 ? error.backtrace : error.backtrace[0..4]
12
+ stacktrace.join("\n").to_s
13
+ else
14
+ ""
15
+ end
16
+ query = URI.encode_www_form({
17
+ title: title,
18
+ labels: labels,
19
+ template: "bug_report.yaml",
20
+ stack_trace: stacktrace_text,
21
+ os: RUBY_PLATFORM,
22
+ cli_version: ShopifyCLI::VERSION,
23
+ ruby_version: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}",
24
+ shell: ENV["SHELL"],
25
+ })
26
+ "#{ShopifyCLI::Constants::Links::NEW_ISSUE}?#{query}"
16
27
  end
17
28
  end
18
29
  end
@@ -0,0 +1,34 @@
1
+ module ShopifyCLI
2
+ class IdentityAuth
3
+ class EnvAuthToken
4
+ Token = Struct.new(:token, :expires_at, keyword_init: true)
5
+
6
+ class << self
7
+ attr_accessor :exchanged_partners_token
8
+
9
+ def partners_token_present?
10
+ Environment.auth_token
11
+ end
12
+
13
+ def fetch_exchanged_partners_token
14
+ current_time = Time.now.to_i
15
+
16
+ # If we have an in-memory token that hasn't expired yet, we reuse it.
17
+ if exchanged_partners_token && current_time < exchanged_partners_token.expires_at.to_i
18
+ return exchanged_partners_token.token
19
+ end
20
+
21
+ new_exchanged_token = yield(Environment.auth_token)
22
+ token = new_exchanged_token["access_token"]
23
+ expires_in = new_exchanged_token["expires_in"].to_i
24
+ expires_at = Time.at(current_time + expires_in)
25
+
26
+ token = Token.new(token: token, expires_at: expires_at)
27
+
28
+ self.exchanged_partners_token = token
29
+ token.token
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -13,6 +13,7 @@ module ShopifyCLI
13
13
  include SmartProperties
14
14
 
15
15
  autoload :Servlet, "shopify_cli/identity_auth/servlet"
16
+ autoload :EnvAuthToken, "shopify_cli/identity_auth/env_auth_token"
16
17
 
17
18
  class Error < StandardError; end
18
19
  class Timeout < StandardError; end
@@ -68,9 +69,12 @@ module ShopifyCLI
68
69
  request_exchange_tokens
69
70
  end
70
71
 
71
- def self.fetch_or_auth_partners_token(ctx:)
72
- env_var_auth_token = Environment.auth_token
73
- return env_var_auth_token if env_var_auth_token
72
+ def fetch_or_auth_partners_token
73
+ if EnvAuthToken.partners_token_present?
74
+ return EnvAuthToken.fetch_exchanged_partners_token do |env_token|
75
+ exchange_partners_auth_token(env_token)
76
+ end
77
+ end
74
78
 
75
79
  ShopifyCLI::DB.get(:partners_exchange_token) do
76
80
  IdentityAuth.new(ctx: ctx).authenticate
@@ -78,6 +82,15 @@ module ShopifyCLI
78
82
  end
79
83
  end
80
84
 
85
+ def exchange_partners_auth_token(subject_token)
86
+ application = "partners"
87
+ request_exchange_token(
88
+ audience: client_id_for_application(application),
89
+ scopes: APPLICATION_SCOPES[application],
90
+ subject_token: subject_token,
91
+ )
92
+ end
93
+
81
94
  def self.environment_auth_token?
82
95
  !!Environment.auth_token
83
96
  end
@@ -195,30 +208,35 @@ module ShopifyCLI
195
208
 
196
209
  def request_exchange_tokens
197
210
  APPLICATION_SCOPES.each do |key, scopes|
198
- request_exchange_token(key, client_id_for_application(key), scopes)
211
+ request_and_save_exchange_token(key, client_id_for_application(key), scopes)
199
212
  end
200
213
  end
201
214
 
202
- def request_exchange_token(name, audience, additional_scopes)
215
+ def request_and_save_exchange_token(name, audience, additional_scopes)
203
216
  return if name == "shopify" && !store.exists?(:shop)
217
+ access_token = request_exchange_token(
218
+ audience: audience,
219
+ scopes: scopes(additional_scopes),
220
+ subject_token: store.get(:identity_access_token),
221
+ destination: name == "shopify" ? "https://#{store.get(:shop)}/admin" : nil
222
+ )["access_token"]
223
+ store.set("#{name}_exchange_token".to_sym => access_token)
224
+ ctx.debug("#{name}_exchange_token: " + access_token)
225
+ end
204
226
 
227
+ def request_exchange_token(audience:, scopes:, subject_token:, destination: nil)
205
228
  params = {
206
229
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
207
230
  requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
208
231
  subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
209
232
  client_id: client_id,
210
233
  audience: audience,
211
- scope: scopes(additional_scopes),
212
- subject_token: store.get(:identity_access_token),
213
- }.tap do |result|
214
- if name == "shopify"
215
- result[:destination] = "https://#{store.get(:shop)}/admin"
216
- end
217
- end
234
+ scope: scopes,
235
+ subject_token: subject_token,
236
+ destination: destination,
237
+ }.compact
218
238
  # ctx.debug(params)
219
- resp = post_token_request(params)
220
- store.set("#{name}_exchange_token".to_sym => resp["access_token"])
221
- ctx.debug("#{name}_exchange_token: " + resp["access_token"])
239
+ post_token_request(params)
222
240
  end
223
241
 
224
242
  def post_token_request(params)
@@ -15,6 +15,7 @@ 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
19
  option_parser: {
19
20
  invalid_option: "The option {{command:%s}} is not supported.",
20
21
  missing_argument: "The required argument {{command:%s}} is missing.",
@@ -41,7 +42,7 @@ module ShopifyCLI
41
42
  invalid_type: "The type %s is not supported. The only supported types are"\
42
43
  " {{command:[ rails | node | php ]}}",
43
44
  help: <<~HELP,
44
- {{command:%s app create}}: Creates a ruby on rails app.
45
+ {{command:%s app create}}: Creates a new project in a subdirectory.
45
46
  Usage: {{command:%s app create [ rails | node | php ]}}
46
47
  HELP
47
48
  rails: {
@@ -53,7 +54,7 @@ module ShopifyCLI
53
54
  {{command:--organization-id=ID}} Partner organization ID. Must be an existing organization.
54
55
  {{command:--store-domain=MYSHOPIFYDOMAIN }} Development store URL. Must be an existing development store.
55
56
  {{command:--db=DB}} Database type. Must be one of: mysql, postgresql, sqlite3, oracle, frontbase, ibm_db, sqlserver, jdbcmysql, jdbcsqlite3, jdbcpostgresql, jdbc.
56
- {{command:--rails-opts=RAILSOPTS}} Additional options. Must be string containing one or more valid Rails options, separated by spaces.
57
+ {{command:--rails-opts=RAILSOPTS}} Additional options. Must be a string containing one or more valid Rails options, separated by spaces.
57
58
  HELP
58
59
 
59
60
  error: {
@@ -38,7 +38,11 @@ module ShopifyCLI
38
38
  def query(ctx, query_name, **variables)
39
39
  CLI::Kit::Util.begin do
40
40
  api_client(ctx).query(query_name, variables: variables)
41
- end.retry_after(API::APIRequestUnauthorizedError, retries: 1) do
41
+ end.retry_after(
42
+ API::APIRequestUnauthorizedError,
43
+ retries: 1,
44
+ only: -> { !IdentityAuth::EnvAuthToken.partners_token_present? }
45
+ ) do
42
46
  ShopifyCLI::IdentityAuth.new(ctx: ctx).reauthenticate
43
47
  end
44
48
  rescue API::APIRequestUnauthorizedError => e
@@ -60,9 +64,10 @@ module ShopifyCLI
60
64
  private
61
65
 
62
66
  def api_client(ctx)
67
+ identity_auth = ShopifyCLI::IdentityAuth.new(ctx: ctx)
63
68
  new(
64
69
  ctx: ctx,
65
- token: IdentityAuth.fetch_or_auth_partners_token(ctx: ctx),
70
+ token: identity_auth.fetch_or_auth_partners_token,
66
71
  url: "https://#{Environment.partners_domain}/api/cli/graphql",
67
72
  )
68
73
  end
@@ -41,12 +41,7 @@ module ShopifyCLI
41
41
 
42
42
  raise ShopifyCLI::AbortSilent if form.nil?
43
43
 
44
- ruby_version = Rails::Ruby.version(context)
45
- context.abort(context.message("core.app.create.rails.error.invalid_ruby_version")) unless
46
- ruby_version.satisfies?("~>2.5") || ruby_version.satisfies?("~>3.0.0")
47
-
48
- check_node
49
- check_yarn
44
+ check_dependencies
50
45
 
51
46
  build(form.name, form.db)
52
47
  set_custom_ua
@@ -106,6 +101,18 @@ module ShopifyCLI
106
101
  end
107
102
  end
108
103
 
104
+ def check_dependencies
105
+ check_ruby
106
+ check_node
107
+ check_yarn
108
+ end
109
+
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")
113
+ context.abort(context.message("core.app.create.rails.error.invalid_ruby_version"))
114
+ end
115
+
109
116
  def check_node
110
117
  cmd_path = context.which("node")
111
118
  if cmd_path.nil?
@@ -148,11 +155,13 @@ module ShopifyCLI
148
155
  end
149
156
 
150
157
  def build(name, db)
151
- context.abort(context.message("core.app.create.rails.error.install_failure",
152
- "rails")) unless install_gem("rails",
153
- "<6.1")
154
- context.abort(context.message("core.app.create.rails.error.install_failure", "bundler ~>2.0")) unless
155
- install_gem("bundler", "~>2.0")
158
+ unless install_gem("rails")
159
+ context.abort(context.message("core.app.create.rails.error.install_failure", "rails"))
160
+ end
161
+
162
+ unless install_gem("bundler", "~>2.0")
163
+ context.abort(context.message("core.app.create.rails.error.install_failure", "bundler ~>2.0"))
164
+ end
156
165
 
157
166
  full_path = File.join(context.root, name)
158
167
  context.abort(context.message("core.app.create.rails.error.dir_exists", name)) if Dir.exist?(full_path)
@@ -173,7 +182,7 @@ module ShopifyCLI
173
182
 
174
183
  context.puts(context.message("core.app.create.rails.adding_shopify_gem"))
175
184
  File.open(File.join(context.root, "Gemfile"), "a") do |f|
176
- f.puts "\ngem 'shopify_app', '>=17.0.3'"
185
+ f.puts "\ngem 'shopify_app', '>=18.1.0'"
177
186
  end
178
187
  CLI::UI::Frame.open(context.message("core.app.create.rails.running_bundle_install")) do
179
188
  syscall(%w(bundle install))
@@ -188,7 +197,7 @@ module ShopifyCLI
188
197
  syscall(%w(rails db:migrate RAILS_ENV=development))
189
198
  end
190
199
 
191
- unless File.exist?(File.join(context.root, "config/webpacker.yml"))
200
+ if install_webpacker?
192
201
  CLI::UI::Frame.open(context.message("core.app.create.rails.running_webpacker_install")) do
193
202
  syscall(%w(rails webpacker:install))
194
203
  end
@@ -208,6 +217,21 @@ module ShopifyCLI
208
217
  def install_gem(name, version = nil)
209
218
  Rails::Gem.install(context, name, version)
210
219
  end
220
+
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
231
+
232
+ version = output.scan(/Rails \d+\.\d+\.\d+/).first.split(" ").last
233
+ ::Semantic::Version.new(version)
234
+ end
211
235
  end
212
236
  end
213
237
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module DevServer
6
+ class HotReload
7
+ class RemoteFileReloader
8
+ def initialize(ctx, theme:, streams:)
9
+ @ctx = ctx
10
+ @theme = theme
11
+ @streams = streams
12
+ end
13
+
14
+ def reload(file)
15
+ retries = 6
16
+
17
+ until retries.zero?
18
+ retries -= 1
19
+
20
+ _status, body = fetch_asset(file)
21
+ retries = 0 if updated_file?(body, file)
22
+
23
+ wait
24
+ end
25
+
26
+ notify(file)
27
+ end
28
+
29
+ private
30
+
31
+ def updated_file?(body, file)
32
+ remote_checksum = body.dig("asset", "checksum")
33
+ local_checksum = file.checksum
34
+
35
+ remote_checksum == local_checksum
36
+ end
37
+
38
+ def notify(file)
39
+ @streams.broadcast(JSON.generate(modified: [file]))
40
+ @ctx.debug("[RemoteFileReloader] Modified #{file}")
41
+ end
42
+
43
+ def wait
44
+ sleep(1)
45
+ end
46
+
47
+ def fetch_asset(file)
48
+ ShopifyCLI::AdminAPI.rest_request(
49
+ @ctx,
50
+ shop: @theme.shop,
51
+ path: "themes/#{@theme.id}/assets.json",
52
+ method: "GET",
53
+ api_version: "unstable",
54
+ query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
55
+ )
56
+ rescue ShopifyCLI::API::APIRequestNotFoundError
57
+ [404, {}]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end