shopify-cli 2.8.0 → 2.9.0

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