shopify-cli 2.11.0 → 2.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/Gemfile.lock +1 -1
  4. data/bin/shopify +13 -0
  5. data/docs/users/installation.md +1 -44
  6. data/lib/project_types/extension/commands/build.rb +0 -3
  7. data/lib/project_types/extension/commands/check.rb +0 -1
  8. data/lib/project_types/extension/commands/create.rb +0 -1
  9. data/lib/project_types/extension/commands/push.rb +13 -1
  10. data/lib/project_types/extension/commands/serve.rb +0 -1
  11. data/lib/project_types/extension/loaders/project.rb +28 -8
  12. data/lib/project_types/extension/messages/messages.rb +10 -2
  13. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +114 -0
  14. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +7 -1
  15. data/lib/project_types/script/cli.rb +2 -0
  16. data/lib/project_types/script/commands/create.rb +2 -2
  17. data/lib/project_types/script/commands/push.rb +4 -6
  18. data/lib/project_types/script/config/extension_points.yml +0 -4
  19. data/lib/project_types/script/forms/create.rb +1 -14
  20. data/lib/project_types/script/layers/application/connect_app.rb +3 -2
  21. data/lib/project_types/script/layers/application/create_script.rb +1 -1
  22. data/lib/project_types/script/layers/application/push_script.rb +1 -1
  23. data/lib/project_types/script/layers/infrastructure/errors.rb +11 -0
  24. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +2 -6
  25. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +30 -26
  26. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +35 -9
  27. data/lib/project_types/script/layers/infrastructure/languages/tool_version_checker.rb +26 -0
  28. data/lib/project_types/script/layers/infrastructure/languages/typescript_project_creator.rb +3 -6
  29. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +31 -27
  30. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +3 -7
  31. data/lib/project_types/script/loaders/project.rb +8 -7
  32. data/lib/project_types/script/messages/messages.rb +14 -13
  33. data/lib/project_types/script/ui/error_handler.rb +13 -0
  34. data/lib/project_types/theme/commands/check.rb +0 -1
  35. data/lib/project_types/theme/commands/common/root_helper.rb +65 -0
  36. data/lib/project_types/theme/commands/delete.rb +0 -1
  37. data/lib/project_types/theme/commands/init.rb +2 -1
  38. data/lib/project_types/theme/commands/language_server.rb +0 -1
  39. data/lib/project_types/theme/commands/package.rb +0 -1
  40. data/lib/project_types/theme/commands/publish.rb +0 -1
  41. data/lib/project_types/theme/commands/pull.rb +18 -9
  42. data/lib/project_types/theme/commands/push.rb +16 -11
  43. data/lib/project_types/theme/commands/serve.rb +6 -3
  44. data/lib/project_types/theme/conversions/base_glob.rb +50 -0
  45. data/lib/project_types/theme/conversions/ignore_glob.rb +15 -0
  46. data/lib/project_types/theme/conversions/include_glob.rb +15 -0
  47. data/lib/project_types/theme/messages/messages.rb +5 -5
  48. data/lib/shopify_cli/command.rb +11 -3
  49. data/lib/shopify_cli/commands/app/create/node.rb +1 -0
  50. data/lib/shopify_cli/commands/app/create/php.rb +1 -0
  51. data/lib/shopify_cli/commands/app/create/rails.rb +2 -1
  52. data/lib/shopify_cli/commands/app/create.rb +0 -3
  53. data/lib/shopify_cli/commands/app/deploy.rb +1 -1
  54. data/lib/shopify_cli/commands/app/serve.rb +0 -1
  55. data/lib/shopify_cli/constants.rb +2 -2
  56. data/lib/shopify_cli/environment.rb +45 -45
  57. data/lib/shopify_cli/git.rb +9 -1
  58. data/lib/shopify_cli/messages/messages.rb +23 -2
  59. data/lib/shopify_cli/tasks/ensure_git_dependency.rb +14 -0
  60. data/lib/shopify_cli/tasks.rb +1 -0
  61. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +63 -0
  62. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +22 -6
  63. data/lib/shopify_cli/theme/dev_server/proxy.rb +18 -7
  64. data/lib/shopify_cli/theme/dev_server.rb +1 -3
  65. data/lib/shopify_cli/theme/development_theme.rb +11 -0
  66. data/lib/shopify_cli/theme/file.rb +4 -0
  67. data/lib/shopify_cli/theme/include_filter.rb +4 -2
  68. data/lib/shopify_cli/theme/syncer.rb +13 -4
  69. data/lib/shopify_cli/theme/theme.rb +0 -4
  70. data/lib/shopify_cli/version.rb +1 -1
  71. metadata +9 -2
@@ -4,14 +4,21 @@ require "shopify_cli/theme/development_theme"
4
4
  require "shopify_cli/theme/ignore_filter"
5
5
  require "shopify_cli/theme/include_filter"
6
6
  require "shopify_cli/theme/syncer"
7
+ require "project_types/theme/commands/common/root_helper"
8
+ require "project_types/theme/conversions/include_glob"
9
+ require "project_types/theme/conversions/ignore_glob"
7
10
 
8
11
  module Theme
9
12
  class Command
10
13
  class Push < ShopifyCLI::Command::SubCommand
11
- recommend_default_node_range
14
+ include Common::RootHelper
15
+
12
16
  recommend_default_ruby_range
13
17
 
14
18
  options do |parser, flags|
19
+ Conversions::IncludeGlob.register(parser)
20
+ Conversions::IgnoreGlob.register(parser)
21
+
15
22
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
16
23
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
17
24
  parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
@@ -21,18 +28,18 @@ module Theme
21
28
  parser.on("-j", "--json") { flags[:json] = true }
22
29
  parser.on("-a", "--allow-live") { flags[:allow_live] = true }
23
30
  parser.on("-p", "--publish") { flags[:publish] = true }
24
- parser.on("-o", "--only=PATTERN") do |pattern|
31
+ parser.on("-o", "--only=PATTERN", Conversions::IncludeGlob) do |pattern|
25
32
  flags[:includes] ||= []
26
- flags[:includes] << pattern
33
+ flags[:includes] += pattern
27
34
  end
28
- parser.on("-x", "--ignore=PATTERN") do |pattern|
35
+ parser.on("-x", "--ignore=PATTERN", Conversions::IgnoreGlob) do |pattern|
29
36
  flags[:ignores] ||= []
30
- flags[:ignores] << pattern
37
+ flags[:ignores] += pattern
31
38
  end
32
39
  end
33
40
 
34
- def call(args, _name)
35
- root = args.first || "."
41
+ def call(_args, name)
42
+ root = root_value(options, name)
36
43
  delete = !options.flags[:nodelete]
37
44
  theme = find_theme(root, **options.flags)
38
45
  return if theme.nil?
@@ -43,7 +50,7 @@ module Theme
43
50
  return unless CLI::UI::Prompt.confirm(question)
44
51
  end
45
52
 
46
- include_filter = ShopifyCLI::Theme::IncludeFilter.new(options.flags[:includes])
53
+ include_filter = ShopifyCLI::Theme::IncludeFilter.new(root, options.flags[:includes])
47
54
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
48
55
  ignore_filter.add_patterns(options.flags[:ignores]) if options.flags[:ignores]
49
56
 
@@ -92,9 +99,7 @@ module Theme
92
99
  end
93
100
 
94
101
  if development
95
- new_theme = ShopifyCLI::Theme::DevelopmentTheme.new(@ctx, root: root)
96
- new_theme.ensure_exists!
97
- return new_theme
102
+ return ShopifyCLI::Theme::DevelopmentTheme.find_or_create!(@ctx, root: root)
98
103
  end
99
104
 
100
105
  if unpublished
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  require "shopify_cli/theme/dev_server"
3
+ require "project_types/theme/commands/common/root_helper"
3
4
 
4
5
  module Theme
5
6
  class Command
6
7
  class Serve < ShopifyCLI::Command::SubCommand
7
- recommend_default_node_range
8
+ include Common::RootHelper
9
+
8
10
  recommend_default_ruby_range
9
11
 
10
12
  DEFAULT_HTTP_HOST = "127.0.0.1"
@@ -16,10 +18,11 @@ module Theme
16
18
  parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
17
19
  end
18
20
 
19
- def call(*)
21
+ def call(_args, name)
22
+ root = root_value(options, name)
20
23
  flags = options.flags.dup
21
24
  host = flags[:host] || DEFAULT_HTTP_HOST
22
- ShopifyCLI::Theme::DevServer.start(@ctx, ".", host: host, **flags) do |syncer|
25
+ ShopifyCLI::Theme::DevServer.start(@ctx, root, host: host, **flags) do |syncer|
23
26
  UI::SyncProgressBar.new(syncer).progress(:upload_theme!, delay_low_priority_files: true)
24
27
  end
25
28
  rescue ShopifyCLI::Theme::DevServer::AddressBindingError
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Theme
4
+ module Conversions
5
+ class BaseGlob
6
+ class << self
7
+ def register(parser)
8
+ parser.accept(self) { |_val| convert(parser) }
9
+ end
10
+
11
+ def convert(parser)
12
+ argv = parser.default_argv
13
+ option_index = argv.index { |v| options.include?(v) }
14
+
15
+ return [] if option_index.nil?
16
+
17
+ start_index = option_index + 1
18
+ option_by_key = options_map(parser)
19
+ values = []
20
+
21
+ argv[start_index..-1].each do |value|
22
+ return values unless option_by_key[value].nil?
23
+ values << value
24
+ end
25
+
26
+ values
27
+ end
28
+
29
+ def options
30
+ raise "`#{self.class.name}#options` must be defined"
31
+ end
32
+
33
+ private
34
+
35
+ def options_map(parser)
36
+ map = {}
37
+ parser.top.list.each do |option|
38
+ map[option.short.first] = option
39
+ map[option.long.first] = option
40
+ end
41
+ map
42
+ end
43
+
44
+ def parameter?(value)
45
+ value.start_with?("-")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_glob"
4
+
5
+ module Theme
6
+ module Conversions
7
+ class IgnoreGlob < BaseGlob
8
+ class << self
9
+ def options
10
+ %w(-x --ignore)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_glob"
4
+
5
+ module Theme
6
+ module Conversions
7
+ class IncludeGlob < BaseGlob
8
+ class << self
9
+ def options
10
+ %w(-o --only)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -65,8 +65,8 @@ module Theme
65
65
  {{command:-j, --json}} Output JSON instead of a UI.
66
66
  {{command:-a, --allow-live}} Allow push to a live theme.
67
67
  {{command:-p, --publish}} Publish as the live theme after uploading.
68
- {{command:-o, --only}} Upload only the specified files.
69
- {{command:-x, --ignore}} Skip uploading the specified files.
68
+ {{command:-o, --only}} Upload only the specified files (Multiple flags allowed).
69
+ {{command:-x, --ignore}} Skip uploading the specified files (Multiple flags allowed).
70
70
 
71
71
  Run without options to select theme from a list.
72
72
  HELP
@@ -96,7 +96,7 @@ module Theme
96
96
  help: <<~HELP,
97
97
  Uploads the current theme as a development theme to the connected store, then prints theme editor and preview URLs to your terminal. While running, changes will push to the store in real time.
98
98
 
99
- Usage: {{command:%s theme serve}}
99
+ Usage: {{command:%s theme serve [ ROOT ]}}
100
100
 
101
101
  Options:
102
102
  {{command:--port=PORT}} Local port to serve theme preview from.
@@ -202,8 +202,8 @@ module Theme
202
202
  {{command:-l, --live}} Pull theme files from your remote live theme.
203
203
  {{command:-d, --development}} Pull theme files from your remote development theme.
204
204
  {{command:-n, --nodelete}} Runs the pull command without deleting local files.
205
- {{command:-o, --only}} Download only the specified files.
206
- {{command:-x, --ignore}} Skip downloading the specified files.
205
+ {{command:-o, --only}} Download only the specified files (Multiple flags allowed).
206
+ {{command:-x, --ignore}} Skip downloading the specified files (Multiple flags allowed).
207
207
 
208
208
  Run without options to select theme from a list.
209
209
  HELP
@@ -100,14 +100,22 @@ module ShopifyCLI
100
100
  end
101
101
 
102
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
+
103
110
  check_version(
104
111
  Environment.node_version,
105
112
  range: @compatible_node_range,
106
- runtime: "Node"
113
+ runtime: "Node",
114
+ context: context
107
115
  )
108
116
  end
109
117
 
110
- def check_version(version, range:, runtime:)
118
+ def check_version(version, range:, runtime:, context: Context.new)
111
119
  return if Environment.test?
112
120
  return if range.nil?
113
121
 
@@ -116,7 +124,7 @@ module ShopifyCLI
116
124
  is_lower_than_top = version_without_pre_nor_build < Utilities.version_dropping_pre_and_build(range.to)
117
125
  return if is_higher_than_bottom && is_lower_than_top
118
126
 
119
- Context.new.warn("Your environment #{runtime} version, #{version},"\
127
+ context.warn("Your environment #{runtime} version, #{version},"\
120
128
  " is outside of the range supported by the CLI,"\
121
129
  " #{range.from}..<#{range.to},"\
122
130
  " and might cause incompatibility issues.")
@@ -4,6 +4,7 @@ module ShopifyCLI
4
4
  class Create
5
5
  class Node < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
+ prerequisite_task :ensure_git_dependency
7
8
 
8
9
  recommend_default_node_range
9
10
  recommend_default_ruby_range
@@ -4,6 +4,7 @@ module ShopifyCLI
4
4
  class Create
5
5
  class PHP < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
+ prerequisite_task :ensure_git_dependency
7
8
 
8
9
  options do |parser, flags|
9
10
  parser.on("--name=NAME") { |name| flags[:name] = name }
@@ -4,9 +4,10 @@ module ShopifyCLI
4
4
  class Create
5
5
  class Rails < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
+ prerequisite_task :ensure_git_dependency
7
8
 
8
- recommend_default_node_range
9
9
  recommend_default_ruby_range
10
+ recommend_default_node_range
10
11
 
11
12
  options do |parser, flags|
12
13
  parser.on("--name=NAME") { |t| flags[:name] = t }
@@ -6,9 +6,6 @@ module ShopifyCLI
6
6
  subcommand :PHP, "php", "shopify_cli/commands/app/create/php"
7
7
  subcommand :Node, "node", "shopify_cli/commands/app/create/node"
8
8
 
9
- recommend_default_node_range
10
- recommend_default_ruby_range
11
-
12
9
  def call(_args, _command_name)
13
10
  @ctx.puts(self.class.help)
14
11
  end
@@ -3,8 +3,8 @@ module ShopifyCLI
3
3
  class App
4
4
  class Deploy < ShopifyCLI::Command::AppSubCommand
5
5
  subcommand :Heroku, "heroku", "shopify_cli/commands/app/deploy/heroku"
6
+ prerequisite_task :ensure_git_dependency
6
7
 
7
- recommend_default_node_range
8
8
  recommend_default_ruby_range
9
9
 
10
10
  def call(args, _name)
@@ -7,7 +7,6 @@ module ShopifyCLI
7
7
  prerequisite_task :ensure_env, :ensure_dev_store
8
8
 
9
9
  recommend_default_ruby_range
10
- recommend_default_node_range
11
10
 
12
11
  options do |parser, flags|
13
12
  parser.on("--host=HOST") do |h|
@@ -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"
@@ -65,7 +65,7 @@ module ShopifyCLI
65
65
  end
66
66
 
67
67
  module Node
68
- FROM = "12.0.0"
68
+ FROM = "14.5.0"
69
69
  TO = "17.0.0"
70
70
  end
71
71
  end
@@ -5,6 +5,11 @@ module ShopifyCLI
5
5
  # the environment in which the CLI runs
6
6
  module Environment
7
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
+ ]
8
13
 
9
14
  def self.ruby_version(context: Context.new)
10
15
  out, err, stat = context.capture3('ruby -e "puts RUBY_VERSION"')
@@ -20,6 +25,12 @@ module ShopifyCLI
20
25
  ::Semantic::Version.new(out.chomp)
21
26
  end
22
27
 
28
+ def self.npm_version(context: Context.new)
29
+ out, err, stat = context.capture3("npm", "--version")
30
+ raise ShopifyCLI::Abort, err unless stat.success?
31
+ ::Semantic::Version.new(out.chomp)
32
+ end
33
+
23
34
  def self.interactive=(interactive)
24
35
  @interactive = interactive
25
36
  end
@@ -87,6 +98,20 @@ module ShopifyCLI
87
98
  end
88
99
  end
89
100
 
101
+ def self.spin_url_override(env_variables: ENV)
102
+ tokens = SPIN_OVERRIDE_ENV_NAMES.map do |name|
103
+ env_variables[name]
104
+ end
105
+
106
+ return if tokens.all?(&:nil?)
107
+
108
+ if tokens.any?(&:nil?)
109
+ raise "To manually target a spin instance, you must set #{SPIN_OVERRIDE_ENV_NAMES}"
110
+ else
111
+ tokens.join(".")
112
+ end
113
+ end
114
+
90
115
  def self.use_spin?(env_variables: ENV)
91
116
  env_variable_truthy?(
92
117
  Constants::EnvironmentVariables::SPIN,
@@ -97,34 +122,31 @@ module ShopifyCLI
97
122
  )
98
123
  end
99
124
 
100
- def self.infer_spin?(env_variables: ENV)
101
- env_variable_truthy?(
102
- Constants::EnvironmentVariables::INFER_SPIN,
103
- env_variables: env_variables
104
- )
105
- end
106
-
107
125
  def self.spin_url(env_variables: ENV)
108
- if infer_spin?(env_variables: env_variables)
109
- # TODO: Remove version check and delete spin-legacy branch
110
- # once spin2 becomes the installed "spin" binary by default
111
- spin_version = %x(spin version 2> /dev/null).strip
112
- if spin_version.start_with?("spin-")
113
- # spin2
114
- raise ShopifyCLI:: Abort, "SPIN_INSTANCE must be specified" unless ENV.key?("SPIN_INSTANCE")
115
- %x(spin show -o fqdn 2> /dev/null).strip
116
- else
117
- # spin-legacy
118
- %x(spin info fqdn 2> /dev/null).strip
119
- end
126
+ override = spin_url_override(env_variables: env_variables)
127
+ return override unless override.nil?
128
+
129
+ spin_response = if env_variables.key?(
130
+ Constants::EnvironmentVariables::SPIN_INSTANCE
131
+ )
132
+ spin_show
120
133
  else
121
- spin_workspace = spin_workspace(env_variables: env_variables)
122
- spin_namespace = spin_namespace(env_variables: env_variables)
123
- spin_host = spin_host(env_variables: env_variables)
124
- "#{spin_workspace}.#{spin_namespace}.#{spin_host}"
134
+ spin_show(latest: true)
135
+ end
136
+
137
+ begin
138
+ instance = JSON.parse(spin_response)
139
+ raise "Missing key 'fqdn' from spin show. Actual response: #{instance}" unless instance.include?("fqdn")
140
+ instance["fqdn"]
141
+ rescue => e
142
+ raise "Failed to infer spin environment from spin show response #{spin_response}: #{e}"
125
143
  end
126
144
  end
127
145
 
146
+ def self.spin_show(latest: false)
147
+ latest ? %x(spin show --latest --json) : %x(spin show --json)
148
+ end
149
+
128
150
  def self.send_monorail_events?(env_variables: ENV)
129
151
  env_variable_truthy?(
130
152
  Constants::EnvironmentVariables::MONORAIL_REAL_EVENTS,
@@ -139,27 +161,5 @@ module ShopifyCLI
139
161
  def self.env_variable_truthy?(variable_name, env_variables: ENV)
140
162
  TRUTHY_ENV_VARIABLE_VALUES.include?(env_variables[variable_name.to_s])
141
163
  end
142
-
143
- def self.spin_workspace(env_variables: ENV)
144
- env_value = env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE]
145
- return env_value unless env_value.nil?
146
-
147
- if env_value.nil?
148
- raise "No value set for #{Constants::EnvironmentVariables::SPIN_WORKSPACE}"
149
- end
150
- end
151
-
152
- def self.spin_namespace(env_variables: ENV)
153
- env_value = env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE]
154
- return env_value unless env_value.nil?
155
-
156
- if env_value.nil?
157
- raise "No value set for #{Constants::EnvironmentVariables::SPIN_NAMESPACE}"
158
- end
159
- end
160
-
161
- def self.spin_host(env_variables: ENV)
162
- env_variables[Constants::EnvironmentVariables::SPIN_HOST] || "us.spin.dev"
163
- end
164
164
  end
165
165
  end
@@ -4,7 +4,15 @@ module ShopifyCLI
4
4
  # git.
5
5
  class Git
6
6
  class << self
7
- # Check if Git is available in the environment
7
+ # Check if Git exists in the environment
8
+ def exists?(ctx)
9
+ _output, status = ctx.capture2e("git", "version")
10
+ status.success?
11
+ rescue Errno::ENOENT # git is not installed
12
+ false
13
+ end
14
+
15
+ # Check if the current working directory is a Git repository
8
16
  def available?(ctx)
9
17
  _output, status = ctx.capture2e("git", "status")
10
18
  status.success?
@@ -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: {
@@ -276,6 +277,24 @@ module ShopifyCLI
276
277
  HELP
277
278
  },
278
279
  },
280
+ extension: {
281
+ push: {
282
+ checkout_ui_extension: {
283
+ localization: {
284
+ error: {
285
+ bundle_too_large: "Total size of all locale files must be less than %s.",
286
+ file_empty: "Locale file `%s` is empty.",
287
+ file_too_large: "Locale file `%s` too large; size must be less than %s.",
288
+ invalid_file_extension: "Invalid locale filename: `%s`; only .json files are allowed.",
289
+ invalid_locale_code: "Invalid locale filename: `%s`; locale code should be 2 or 3 letters,"\
290
+ " optionally followed by a two-letter region code, e.g. `fr-CA`.",
291
+ single_default_locale: "There must be one and only one locale identified as the default locale,"\
292
+ " e.g. `en.default.json`",
293
+ },
294
+ },
295
+ },
296
+ },
297
+ },
279
298
  error_reporting: {
280
299
  unhandled_error: {
281
300
  message: "{{x}} {{red:An unexpected error occured.}}",
@@ -354,6 +373,7 @@ module ShopifyCLI
354
373
  error: {
355
374
  directory_exists: "Project directory already exists. Please create a project with a new name.",
356
375
  no_branches_found: "Could not find any git branches",
376
+ nonexistent: "Git needs to be installed: https://git-scm.com/download",
357
377
  repo_not_initiated:
358
378
  "Git repo is not initiated. Please run {{command:git init}} and make at least one commit.",
359
379
  no_commits_made: "No git commits have been made. Please make at least one commit.",
@@ -671,7 +691,8 @@ module ShopifyCLI
671
691
  },
672
692
  },
673
693
  ensure_dev_store: {
674
- could_not_verify_store: "Couldn't verify your store %s",
694
+ could_not_verify_store: "Couldn't verify your store. If you don't have a development store set up, "\
695
+ "please create one in your Partners dashboard and run `shopify app connect`.",
675
696
  convert_to_dev_store: <<~MESSAGE,
676
697
  Do you want to convert %s to a development store?
677
698
  Doing this will allow you to install your app, but the store will become {{bold:transfer-disabled}}.
@@ -0,0 +1,14 @@
1
+ require "shopify_cli"
2
+
3
+ module ShopifyCLI
4
+ module Tasks
5
+ class EnsureGitDependency < ShopifyCLI::Task
6
+ def call(ctx)
7
+ return if ShopifyCLI::Environment.acceptance_test?
8
+ unless ShopifyCLI::Git.exists?(ctx)
9
+ raise ShopifyCLI::Abort, ctx.message("core.git.error.nonexistent")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -34,6 +34,7 @@ module ShopifyCLI
34
34
  register :CreateApiClient, :create_api_client, "shopify_cli/tasks/create_api_client"
35
35
  register :EnsureAuthenticated, :ensure_authenticated, "shopify_cli/tasks/ensure_authenticated"
36
36
  register :EnsureEnv, :ensure_env, "shopify_cli/tasks/ensure_env"
37
+ register :EnsureGitDependency, :ensure_git_dependency, "shopify_cli/tasks/ensure_git_dependency"
37
38
  register :EnsureLoopbackURL, :ensure_loopback_url, "shopify_cli/tasks/ensure_loopback_url"
38
39
  register :EnsureProjectType, :ensure_project_type, "shopify_cli/tasks/ensure_project_type"
39
40
  register :EnsureDevStore, :ensure_dev_store, "shopify_cli/tasks/ensure_dev_store"
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "hot_reload/remote_file_reloader"
4
+
3
5
  module ShopifyCLI
4
6
  module Theme
5
7
  module DevServer
@@ -10,6 +12,7 @@ module ShopifyCLI
10
12
  @theme = theme
11
13
  @mode = mode
12
14
  @streams = SSE::Streams.new
15
+ @remote_file_reloader = RemoteFileReloader.new(ctx, theme: @theme, streams: @streams)
13
16
  @watcher = watcher
14
17
  @watcher.add_observer(self, :notify_streams_of_file_change)
15
18
  @ignore_filter = ignore_filter
@@ -32,17 +35,30 @@ module ShopifyCLI
32
35
  end
33
36
 
34
37
  def notify_streams_of_file_change(modified, added, _removed)
35
- files = (modified + added).reject { |file| @ignore_filter&.ignore?(file) }
36
- .map { |file| @theme[file].relative_path }
38
+ files = (modified + added)
39
+ .reject { |file| @ignore_filter&.ignore?(file) }
40
+ .map { |file| @theme[file] }
37
41
 
38
- unless files.empty?
39
- @streams.broadcast(JSON.generate(modified: files))
40
- @ctx.debug("[HotReload] Modified #{files.join(", ")}")
41
- end
42
+ files -= liquid_css_files = files.select(&:liquid_css?)
43
+
44
+ hot_reload(files) unless files.empty?
45
+ remote_reload(liquid_css_files)
42
46
  end
43
47
 
44
48
  private
45
49
 
50
+ def hot_reload(files)
51
+ paths = files.map(&:relative_path)
52
+ @streams.broadcast(JSON.generate(modified: paths))
53
+ @ctx.debug("[HotReload] Modified #{paths.join(", ")}")
54
+ end
55
+
56
+ def remote_reload(files)
57
+ files.each do |file|
58
+ @remote_file_reloader.reload(file)
59
+ end
60
+ end
61
+
46
62
  def request_is_html?(headers)
47
63
  headers["content-type"]&.start_with?("text/html")
48
64
  end