shopify-cli 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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