shopify-cli 2.10.1 → 2.11.2

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