shopify-cli 2.8.0 → 2.9.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/Gemfile.lock +1 -1
  4. data/RELEASING.md +4 -3
  5. data/ext/javy/javy.rb +1 -1
  6. data/lib/project_types/extension/commands/push.rb +2 -2
  7. data/lib/project_types/extension/messages/messages.rb +1 -1
  8. data/lib/project_types/extension/models/development_server.rb +2 -4
  9. data/lib/project_types/rails/gem.rb +1 -2
  10. data/lib/project_types/script/cli.rb +5 -0
  11. data/lib/project_types/script/commands/connect.rb +1 -1
  12. data/lib/project_types/script/commands/create.rb +8 -2
  13. data/lib/project_types/script/commands/push.rb +35 -12
  14. data/lib/project_types/script/layers/application/connect_app.rb +11 -5
  15. data/lib/project_types/script/layers/application/extension_points.rb +50 -26
  16. data/lib/project_types/script/layers/application/push_script.rb +5 -2
  17. data/lib/project_types/script/layers/domain/extension_point.rb +14 -0
  18. data/lib/project_types/script/layers/infrastructure/errors.rb +2 -0
  19. data/lib/project_types/script/loaders/project.rb +44 -0
  20. data/lib/project_types/script/loaders/specification_handler.rb +22 -0
  21. data/lib/project_types/script/messages/messages.rb +15 -1
  22. data/lib/project_types/script/ui/error_handler.rb +5 -0
  23. data/lib/project_types/theme/commands/pull.rb +39 -16
  24. data/lib/project_types/theme/commands/push.rb +56 -26
  25. data/lib/project_types/theme/commands/serve.rb +5 -0
  26. data/lib/project_types/theme/messages/messages.rb +29 -18
  27. data/lib/shopify_cli/commands/login.rb +10 -4
  28. data/lib/shopify_cli/constants.rb +1 -0
  29. data/lib/shopify_cli/context.rb +66 -12
  30. data/lib/shopify_cli/environment.rb +15 -4
  31. data/lib/shopify_cli/identity_auth.rb +1 -0
  32. data/lib/shopify_cli/messages/messages.rb +3 -1
  33. data/lib/shopify_cli/resources/env_file.rb +5 -1
  34. data/lib/shopify_cli/theme/dev_server/hot-reload.js +19 -1
  35. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +18 -2
  36. data/lib/shopify_cli/theme/dev_server/proxy.rb +1 -0
  37. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +34 -0
  38. data/lib/shopify_cli/theme/dev_server.rb +6 -21
  39. data/lib/shopify_cli/theme/theme.rb +26 -4
  40. data/lib/shopify_cli/version.rb +1 -1
  41. data/lib/shopify_cli.rb +4 -0
  42. metadata +5 -2
@@ -9,7 +9,9 @@ module Theme
9
9
  options do |parser, flags|
10
10
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
11
11
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
12
+ parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
12
13
  parser.on("-l", "--live") { flags[:live] = true }
14
+ parser.on("-d", "--development") { flags[:development] = true }
13
15
  parser.on("-x", "--ignore=PATTERN") do |pattern|
14
16
  flags[:ignores] ||= []
15
17
  flags[:ignores] << pattern
@@ -19,21 +21,8 @@ module Theme
19
21
  def call(args, _name)
20
22
  root = args.first || "."
21
23
  delete = !options.flags[:nodelete]
22
-
23
- theme = if (theme_id = options.flags[:theme_id])
24
- ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
25
- elsif options.flags[:live]
26
- ShopifyCLI::Theme::Theme.live(@ctx, root: root)
27
- else
28
- form = Forms::Select.ask(
29
- @ctx,
30
- [],
31
- title: @ctx.message("theme.pull.select"),
32
- root: root,
33
- )
34
- return unless form
35
- form.theme
36
- end
24
+ theme = find_theme(root, **options.flags)
25
+ return if theme.nil?
37
26
 
38
27
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
39
28
  ignore_filter.add_patterns(options.flags[:ignores]) if options.flags[:ignores]
@@ -46,7 +35,7 @@ module Theme
46
35
  end
47
36
  @ctx.done(@ctx.message("theme.pull.done"))
48
37
  rescue ShopifyCLI::API::APIRequestNotFoundError
49
- @ctx.abort(@ctx.message("theme.pull.theme_not_found", theme.id))
38
+ @ctx.abort(@ctx.message("theme.pull.theme_not_found", "##{theme.id}"))
50
39
  ensure
51
40
  syncer.shutdown
52
41
  end
@@ -55,6 +44,40 @@ module Theme
55
44
  def self.help
56
45
  ShopifyCLI::Context.message("theme.pull.help", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
57
46
  end
47
+
48
+ private
49
+
50
+ def find_theme(root, theme_id: nil, theme: nil, live: nil, development: nil, **_args)
51
+ if theme_id
52
+ @ctx.warn(@ctx.message("theme.pull.deprecated_themeid"))
53
+ return ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
54
+ end
55
+
56
+ if theme
57
+ selected_theme = ShopifyCLI::Theme::Theme.find_by_identifier(@ctx, root: root, identifier: theme)
58
+ return selected_theme || @ctx.abort(@ctx.message("theme.pull.theme_not_found", theme))
59
+ end
60
+
61
+ if live
62
+ return ShopifyCLI::Theme::Theme.live(@ctx, root: root)
63
+ end
64
+
65
+ if development
66
+ return ShopifyCLI::Theme::Theme.development(@ctx, root: root)
67
+ end
68
+
69
+ select_theme(root)
70
+ end
71
+
72
+ def select_theme(root)
73
+ form = Forms::Select.ask(
74
+ @ctx,
75
+ [],
76
+ title: @ctx.message("theme.pull.select"),
77
+ root: root,
78
+ )
79
+ form&.theme
80
+ end
58
81
  end
59
82
  end
60
83
  end
@@ -10,6 +10,7 @@ module Theme
10
10
  options do |parser, flags|
11
11
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
12
12
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
13
+ parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
13
14
  parser.on("-l", "--live") { flags[:live] = true }
14
15
  parser.on("-d", "--development") { flags[:development] = true }
15
16
  parser.on("-u", "--unpublished") { flags[:unpublished] = true }
@@ -25,30 +26,8 @@ module Theme
25
26
  def call(args, _name)
26
27
  root = args.first || "."
27
28
  delete = !options.flags[:nodelete]
28
-
29
- theme = if (theme_id = options.flags[:theme_id])
30
- ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
31
- elsif options.flags[:live]
32
- ShopifyCLI::Theme::Theme.live(@ctx, root: root)
33
- elsif options.flags[:development]
34
- theme = ShopifyCLI::Theme::DevelopmentTheme.new(@ctx, root: root)
35
- theme.ensure_exists!
36
- theme
37
- elsif options.flags[:unpublished]
38
- name = CLI::UI::Prompt.ask(@ctx.message("theme.push.name"), allow_empty: false)
39
- theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
40
- theme.create
41
- theme
42
- else
43
- form = Forms::Select.ask(
44
- @ctx,
45
- [],
46
- title: @ctx.message("theme.push.select"),
47
- root: root,
48
- )
49
- return unless form
50
- form.theme
51
- end
29
+ theme = find_theme(root, **options.flags)
30
+ return if theme.nil?
52
31
 
53
32
  if theme.live? && !options.flags[:allow_live]
54
33
  question = @ctx.message("theme.push.live")
@@ -78,16 +57,67 @@ module Theme
78
57
  end
79
58
  end
80
59
  raise ShopifyCLI::AbortSilent if syncer.has_any_error?
81
- rescue ShopifyCLI::API::APIRequestNotFoundError
82
- @ctx.abort(@ctx.message("theme.push.theme_not_found", theme.id))
83
60
  ensure
84
61
  syncer.shutdown
85
62
  end
63
+ rescue ShopifyCLI::API::APIRequestNotFoundError
64
+ @ctx.abort(@ctx.message("theme.push.theme_not_found", "##{theme.id}"))
86
65
  end
87
66
 
88
67
  def self.help
89
68
  ShopifyCLI::Context.message("theme.push.help", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
90
69
  end
70
+
71
+ private
72
+
73
+ def find_theme(root, theme_id: nil, theme: nil, live: nil, development: nil, unpublished: nil, **_args)
74
+ if theme_id
75
+ @ctx.warn(@ctx.message("theme.push.deprecated_themeid"))
76
+ return ShopifyCLI::Theme::Theme.new(@ctx, root: root, id: theme_id)
77
+ end
78
+
79
+ if live
80
+ return ShopifyCLI::Theme::Theme.live(@ctx, root: root)
81
+ end
82
+
83
+ if development
84
+ new_theme = ShopifyCLI::Theme::DevelopmentTheme.new(@ctx, root: root)
85
+ new_theme.ensure_exists!
86
+ return new_theme
87
+ end
88
+
89
+ if unpublished
90
+ name = theme || ask_theme_name
91
+ new_theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
92
+ new_theme.create
93
+ return new_theme
94
+ end
95
+
96
+ if theme
97
+ selected_theme = ShopifyCLI::Theme::Theme.find_by_identifier(@ctx, root: root, identifier: theme)
98
+ return selected_theme || @ctx.abort(@ctx.message("theme.push.theme_not_found", theme))
99
+ end
100
+
101
+ select_theme(root)
102
+ end
103
+
104
+ def ask_theme_name
105
+ CLI::UI::Prompt.ask(@ctx.message("theme.push.name"), allow_empty: false)
106
+ end
107
+
108
+ def select_theme(root)
109
+ form = Forms::Select.ask(
110
+ @ctx,
111
+ [],
112
+ title: @ctx.message("theme.push.select"),
113
+ root: root,
114
+ )
115
+ form&.theme
116
+ end
117
+
118
+ def themes(root)
119
+ ShopifyCLI::Theme::Theme.all(@ctx, root: root)
120
+ end
91
121
  end
92
122
  end
93
123
  end
@@ -10,6 +10,7 @@ module Theme
10
10
  parser.on("--host=HOST") { |host| flags[:host] = host.to_s }
11
11
  parser.on("--port=PORT") { |port| flags[:port] = port.to_i }
12
12
  parser.on("--poll") { flags[:poll] = true }
13
+ parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
13
14
  end
14
15
 
15
16
  def call(*)
@@ -23,6 +24,10 @@ module Theme
23
24
  ShopifyCLI::Context.message("theme.serve.error.address_binding_error", ShopifyCLI::TOOL_NAME)
24
25
  end
25
26
 
27
+ def self.as_reload_mode(mode)
28
+ ShopifyCLI::Theme::DevServer::ReloadMode.get!(mode)
29
+ end
30
+
26
31
  def self.help
27
32
  ShopifyCLI::Context.message("theme.serve.help", ShopifyCLI::TOOL_NAME)
28
33
  end
@@ -57,14 +57,14 @@ module Theme
57
57
  Usage: {{command:%s theme push [ ROOT ]}}
58
58
 
59
59
  Options:
60
- {{command:-i, --themeid=THEMEID}} Theme ID. Must be an existing theme on your store.
61
- {{command:-l, --live}} Push to your remote live theme, and update your live store.
62
- {{command:-d, --development}} Push to your remote development theme, and create it if needed.
63
- {{command:-u, --unpublished}} Create a new unpublished theme and push to it.
64
- {{command:-n, --nodelete}} Runs the push command without deleting remote files from Shopify.
65
- {{command:-j, --json}} Output JSON instead of a UI.
66
- {{command:-a, --allow-live}} Allow push to a live theme.
67
- {{command:-p, --publish}} Publish as the live theme after uploading.
60
+ {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
61
+ {{command:-l, --live}} Push to your remote live theme, and update your live store.
62
+ {{command:-d, --development}} Push to your remote development theme, and create it if needed.
63
+ {{command:-u, --unpublished}} Create a new unpublished theme and push to it.
64
+ {{command:-n, --nodelete}} Runs the push command without deleting remote files from Shopify.
65
+ {{command:-j, --json}} Output JSON instead of a UI.
66
+ {{command:-a, --allow-live}} Allow push to a live theme.
67
+ {{command:-p, --publish}} Publish as the live theme after uploading.
68
68
 
69
69
  Run without options to select theme from a list.
70
70
  HELP
@@ -75,7 +75,10 @@ module Theme
75
75
  select: "Select theme to push to",
76
76
  live: "Are you sure you want to push to your live theme?",
77
77
  theme: "\n Theme: {{blue:%s #%s}} {{green:[live]}}",
78
- theme_not_found: "Theme #%s doesn't exist",
78
+ deprecated_themeid: <<~WARN,
79
+ {{warning:The {{command:-i, --themeid}} flag is deprecated. Use {{command:-t, --theme}} instead.}}
80
+ WARN
81
+ theme_not_found: "Theme \"%s\" doesn't exist",
79
82
  done: <<~DONE,
80
83
  {{green:Your theme was pushed successfully}}
81
84
 
@@ -94,10 +97,16 @@ module Theme
94
97
  Usage: {{command:%s theme serve}}
95
98
 
96
99
  Options:
97
- {{command:--port=PORT}} Local port to serve theme preview from
98
- {{command:--poll}} Force polling to detect file changes
99
- {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
100
+ {{command:--port=PORT}} Local port to serve theme preview from.
101
+ {{command:--poll}} Force polling to detect file changes.
102
+ {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
103
+ {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
104
+ - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
105
+ - {{command:full-page}} Always refreshes the entire page
106
+ - {{command:off}} Deactivate live reload
100
107
  HELP
108
+ reload_mode_is_not_valid: "The live reload mode `%s` is not valid.",
109
+ try_a_valid_reload_mode: "Try a valid live reload mode: %s.",
101
110
  viewing_theme: "Viewing theme…",
102
111
  syncing_theme: "Syncing theme #%s on %s",
103
112
  open_fail: "Couldn't open the theme",
@@ -131,9 +140,7 @@ module Theme
131
140
  You are not authorized to edit themes on %s.
132
141
  Make sure you are a user of that store, and allowed to edit themes.
133
142
  ENSURE_USER
134
- already_in_use_error: "Error",
135
143
  address_already_in_use: "The address \"%s\" is already in use.",
136
- try_this: "Try this",
137
144
  try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
138
145
  },
139
146
  check: {
@@ -189,16 +196,20 @@ module Theme
189
196
  Usage: {{command:%s theme pull [ ROOT ]}}
190
197
 
191
198
  Options:
192
- {{command:-i, --themeid=THEMEID}} The Theme ID. Must be an existing theme on your store.
193
- {{command:-l, --live}} Pull theme files from your remote live theme.
194
- {{command:-n, --nodelete}} Runs the pull command without deleting local files.
199
+ {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
200
+ {{command:-l, --live}} Pull theme files from your remote live theme.
201
+ {{command:-d, --development}} Pull theme files from your remote development theme.
202
+ {{command:-n, --nodelete}} Runs the pull command without deleting local files.
195
203
 
196
204
  Run without options to select theme from a list.
197
205
  HELP
198
206
  select: "Select a theme to pull from",
199
207
  pulling: "Pulling theme files from %s (#%s) on %s",
200
208
  done: "Theme pulled successfully",
201
- not_found: "{{x}} Theme #%s doesn't exist",
209
+ deprecated_themeid: <<~WARN,
210
+ {{warning:The {{command:-i, --themeid}} flag is deprecated. Use {{command:-t, --theme}} instead.}}
211
+ WARN
212
+ theme_not_found: "Theme \"%s\" doesn't exist",
202
213
  },
203
214
  },
204
215
  }.freeze
@@ -15,8 +15,6 @@ module ShopifyCLI
15
15
 
16
16
  def call(*)
17
17
  shop = (options.flags[:shop] || @ctx.getenv("SHOPIFY_SHOP" || nil))
18
- ShopifyCLI::DB.set(shop: self.class.validate_shop(shop, context: @ctx)) unless shop.nil?
19
-
20
18
  if shop.nil? && Shopifolk.check
21
19
  Shopifolk.reset
22
20
  @ctx.puts(@ctx.message("core.tasks.select_org_and_shop.identified_as_shopify"))
@@ -33,17 +31,25 @@ module ShopifyCLI
33
31
  IdentityAuth.new(ctx: @ctx).authenticate
34
32
  org = select_organization
35
33
  ShopifyCLI::DB.set(organization_id: org["id"].to_i) unless org.nil?
36
- Whoami.call([], "whoami")
34
+
37
35
  end
36
+ # validate that shop belongs to organization
37
+ ShopifyCLI::DB.set(shop: self.class.validate_shop(shop: shop, org: org, context: @ctx)) unless shop.nil?
38
+ Whoami.call([], "whoami")
38
39
  end
39
40
 
40
41
  def self.help
41
42
  ShopifyCLI::Context.message("core.login.help", ShopifyCLI::TOOL_NAME)
42
43
  end
43
44
 
44
- def self.validate_shop(shop, context:)
45
+ def self.validate_shop(shop:, org:, context:)
45
46
  permanent_domain = shop_to_permanent_domain(shop)
46
47
  context.abort(context.message("core.login.invalid_shop", shop)) unless permanent_domain
48
+ if org
49
+ stores_owned = org["stores"]
50
+ is_verified = stores_owned.any? { |store| store["shopDomain"] == permanent_domain }
51
+ context.abort(context.message("core.login.invalid_shop", shop)) unless is_verified
52
+ end
47
53
  permanent_domain
48
54
  end
49
55
 
@@ -30,6 +30,7 @@ module ShopifyCLI
30
30
 
31
31
  module EnvironmentVariables
32
32
  STACKTRACE = "SHOPIFY_CLI_STACKTRACE"
33
+ TTY = "SHOPIFY_CLI_TTY"
33
34
 
34
35
  # When true the CLI points to a local instance of
35
36
  # the partners dashboard and identity.
@@ -44,6 +44,56 @@ module ShopifyCLI
44
44
  str = Context.messages.dig(*key_parts)
45
45
  str ? str % params : key
46
46
  end
47
+
48
+ # a wrapper around Kernel.puts to allow for easy formatting
49
+ #
50
+ # #### Parameters
51
+ # * `text` - a string message to output
52
+ def puts(*args)
53
+ Kernel.puts(CLI::UI.fmt(*args))
54
+ end
55
+
56
+ # aborts the current running command and outputs an error message:
57
+ # - when the `help_message` is not provided, the error message appears in
58
+ # a red frame, prefixed by an ✗ icon
59
+ # - when the `help_message` is provided, the error message appears in a
60
+ # red frame, and the help message appears in a green frame
61
+ #
62
+ # #### Parameters
63
+ # * `error_message` - an error message to output
64
+ # * `help_message` - an optional help message
65
+ #
66
+ # #### Example
67
+ #
68
+ # ShopifyCLI::Context.abort("Execution error")
69
+ # # Output:
70
+ # # ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71
+ # # ┃ ✗ Execution error
72
+ # # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
+ #
74
+ # ShopifyCLI::Context.abort("Execution error", "export EXECUTION=1")
75
+ # # Output:
76
+ # # ┏━━ Error ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
+ # # ┃ Execution error
78
+ # # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79
+ # # ┏━━ Try this ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80
+ # # ┃ export EXECUTION=1
81
+ # # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
+ #
83
+ def abort(error_message, help_message = nil)
84
+ raise ShopifyCLI::Abort, "{{x}} #{error_message}" if help_message.nil?
85
+
86
+ frame(message("core.error"), color: :red) { self.puts(error_message) }
87
+ frame(message("core.try_this"), color: :green) { self.puts(help_message) }
88
+
89
+ raise ShopifyCLI::AbortSilent
90
+ end
91
+
92
+ private
93
+
94
+ def frame(title, color:, &block)
95
+ CLI::UI::Frame.open(title, color: CLI::UI.resolve_color(color), timing: false, &block)
96
+ end
47
97
  end
48
98
 
49
99
  # is the directory root that the current command is running in. If you want to
@@ -61,14 +111,19 @@ module ShopifyCLI
61
111
  # will return which operating system that the cli is running on [:mac, :linux]
62
112
  def os
63
113
  host = uname
64
- return :mac_m1 if /arm64-apple-darwin/i.match(host)
114
+ return :mac_m1 if /arm64.*darwin/i.match(host)
65
115
  return :mac if /darwin/i.match(host)
66
116
  return :windows if /mswin|mingw|cygwin/i.match(host)
67
117
  return :linux if /linux|bsd/i.match(host)
68
118
  :unknown
69
119
  end
70
120
 
71
- # will return true if the cli is running on an apple computer.
121
+ # will return true if the cli is running on an ARM Apple computer.
122
+ def mac_m1?
123
+ os == :mac_m1
124
+ end
125
+
126
+ # will return true if the cli is running on a Intel x86 Apple computer.
72
127
  def mac?
73
128
  os == :mac
74
129
  end
@@ -90,7 +145,7 @@ module ShopifyCLI
90
145
 
91
146
  # will return true if being launched from a tty
92
147
  def tty?
93
- !testing? && $stdin.tty?
148
+ $stdin.tty?
94
149
  end
95
150
 
96
151
  # will return true if the cli is being run from an installation, and not a
@@ -329,7 +384,7 @@ module ShopifyCLI
329
384
  system("xdg-open", uri.to_s)
330
385
  elsif windows?
331
386
  system("start \"\" \"#{uri}\"")
332
- elsif mac?
387
+ elsif mac? || mac_m1?
333
388
  system("open", uri.to_s)
334
389
  else
335
390
  open_url!(uri)
@@ -349,13 +404,13 @@ module ShopifyCLI
349
404
  puts "{{yellow:*}} #{text}"
350
405
  end
351
406
 
352
- # a wrapper around Kernel.puts to allow for easy formatting
407
+ # proxy call to Context.puts.
353
408
  #
354
409
  # #### Parameters
355
410
  # * `text` - a string message to output
356
411
  #
357
412
  def puts(*args)
358
- Kernel.puts(CLI::UI.fmt(*args))
413
+ Context.puts(*args)
359
414
  end
360
415
 
361
416
  # a wrapper around $stderr.puts to allow for easy formatting
@@ -385,14 +440,13 @@ module ShopifyCLI
385
440
  puts("{{v}} #{text}")
386
441
  end
387
442
 
388
- # aborts the current running command and outputs an error message, prefixed
389
- # by a red x
443
+ # proxy call to Context.abort.
390
444
  #
391
445
  # #### Parameters
392
- # * `text` - a string message to output
393
- #
394
- def abort(text)
395
- raise ShopifyCLI::Abort, "{{x}} #{text}"
446
+ # * `error_message` - an error message to output
447
+ # * `help_message` - an optional help message
448
+ def abort(error_message, help_message = nil)
449
+ Context.abort(error_message, help_message)
396
450
  end
397
451
 
398
452
  # outputs a message, prefixed by a red `DEBUG` tag. This will only output to
@@ -4,6 +4,21 @@ module ShopifyCLI
4
4
  module Environment
5
5
  TRUTHY_ENV_VARIABLE_VALUES = ["1", "true", "TRUE", "yes", "YES"]
6
6
 
7
+ def self.interactive=(interactive)
8
+ @interactive = interactive
9
+ end
10
+
11
+ def self.interactive?(env_variables: ENV)
12
+ if env_variables.key?(Constants::EnvironmentVariables::TTY)
13
+ env_variable_truthy?(
14
+ Constants::EnvironmentVariables::TTY,
15
+ env_variables: env_variables
16
+ )
17
+ else
18
+ @interactive ||= STDIN.tty?
19
+ end
20
+ end
21
+
7
22
  def self.development?(env_variables: ENV)
8
23
  env_variable_truthy?(
9
24
  Constants::EnvironmentVariables::DEVELOPMENT,
@@ -11,10 +26,6 @@ module ShopifyCLI
11
26
  )
12
27
  end
13
28
 
14
- def self.interactive?
15
- ShopifyCLI::Context.new.tty?
16
- end
17
-
18
29
  def self.use_local_partners_instance?(env_variables: ENV)
19
30
  env_variable_truthy?(
20
31
  Constants::EnvironmentVariables::LOCAL_PARTNERS,
@@ -229,6 +229,7 @@ module ShopifyCLI
229
229
  uri = URI.parse("#{auth_url}#{endpoint}")
230
230
  https = Net::HTTP.new(uri.host, uri.port)
231
231
  https.use_ssl = true
232
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV["SSL_VERIFY_NONE"]
232
233
  request = Net::HTTP::Post.new(uri.path)
233
234
  request["User-Agent"] = "Shopify CLI #{::ShopifyCLI::VERSION}"
234
235
  request.body = URI.encode_www_form(params)
@@ -415,7 +415,7 @@ module ShopifyCLI
415
415
  Usage: {{command:%s login [--store=STORE]}}
416
416
  HELP
417
417
  invalid_shop: <<~MESSAGE,
418
- Invalid store provided (%s). Please provide the store in the following format: my-store.myshopify.com
418
+ Invalid store provided (%s). Please make sure that the store belongs to your partner organization, and provide the store in the following format: my-store.myshopify.com
419
419
  MESSAGE
420
420
  shop_prompt: <<~PROMPT,
421
421
  What store are you connecting to? (e.g. my-store.myshopify.com; do {{bold:NOT}} include protocol part, e.g., https://)
@@ -790,6 +790,8 @@ module ShopifyCLI
790
790
  logged_in_partner_only: "Logged into partner organization {{green:%s}}",
791
791
  logged_in_partner_and_shop: "Logged into store {{green:%s}} in partner organization {{green:%s}}",
792
792
  },
793
+ error: "Error",
794
+ try_this: "Try this",
793
795
  },
794
796
  }.freeze
795
797
  end
@@ -14,6 +14,10 @@ module ShopifyCLI
14
14
  }
15
15
 
16
16
  class << self
17
+ def path(directory)
18
+ File.join(directory, FILENAME)
19
+ end
20
+
17
21
  def read(_directory = Dir.pwd, overrides: {})
18
22
  input = parse_external_env(overrides: overrides)
19
23
  new(input)
@@ -24,7 +28,7 @@ module ShopifyCLI
24
28
  end
25
29
 
26
30
  def parse(directory)
27
- File.read(File.join(directory, FILENAME))
31
+ File.read(path(directory))
28
32
  .gsub("\r\n", "\n").split("\n").each_with_object({}) do |line, output|
29
33
  match = /\A([A-Za-z_0-9]+)\s*=\s*(.*)\z/.match(line)
30
34
  if match
@@ -15,9 +15,23 @@
15
15
  eventSource.onerror = () => eventSource.close();
16
16
  }
17
17
 
18
- connect();
18
+ function reloadMode() {
19
+ var namespace = window.__SHOPIFY_CLI_ENV__;
20
+ return namespace.mode;
21
+ }
22
+
23
+ function isFullPageReloadMode(){
24
+ return reloadMode() === "full-page";
25
+ }
26
+
27
+ function isReloadModeActive(){
28
+ return reloadMode() !== "off";
29
+ }
19
30
 
20
31
  function isRefreshRequired(files) {
32
+ if (isFullPageReloadMode()) {
33
+ return true;
34
+ }
21
35
  return files.some((file) => !isCssFile(file) && !isSectionFile(file));
22
36
  }
23
37
 
@@ -119,4 +133,8 @@
119
133
  }
120
134
  }
121
135
  }
136
+
137
+ if (isReloadModeActive()) {
138
+ connect();
139
+ }
122
140
  })();
@@ -4,10 +4,11 @@ module ShopifyCLI
4
4
  module Theme
5
5
  module DevServer
6
6
  class HotReload
7
- def initialize(ctx, app, theme:, watcher:, ignore_filter: nil)
7
+ def initialize(ctx, app, theme:, watcher:, mode:, ignore_filter: nil)
8
8
  @ctx = ctx
9
9
  @app = app
10
10
  @theme = theme
11
+ @mode = mode
11
12
  @streams = SSE::Streams.new
12
13
  @watcher = watcher
13
14
  @watcher.add_observer(self, :notify_streams_of_file_change)
@@ -48,12 +49,27 @@ module ShopifyCLI
48
49
 
49
50
  def inject_hot_reload_javascript(body)
50
51
  hot_reload_js = ::File.read("#{__dir__}/hot-reload.js")
51
- hot_reload_script = "<script>\n#{hot_reload_js}</script>"
52
+ hot_reload_script = [
53
+ "<script>",
54
+ params_js,
55
+ hot_reload_js,
56
+ "</script>",
57
+ ].join("\n")
58
+
52
59
  body = body.join.gsub("</body>", "#{hot_reload_script}\n</body>")
53
60
 
54
61
  [body]
55
62
  end
56
63
 
64
+ def params_js
65
+ env = { mode: @mode }
66
+ <<~JS
67
+ (() => {
68
+ window.__SHOPIFY_CLI_ENV__ = #{env.to_json};
69
+ })();
70
+ JS
71
+ end
72
+
57
73
  def create_stream
58
74
  stream = @streams.new
59
75
 
@@ -18,6 +18,7 @@ module ShopifyCLI
18
18
  "trailer",
19
19
  "transfer-encoding",
20
20
  "upgrade",
21
+ "content-security-policy",
21
22
  ]
22
23
 
23
24
  class Proxy