shopify-cli 2.16.1 → 2.18.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +3 -3
  4. data/ext/shopify-extensions/version +1 -1
  5. data/lib/project_types/extension/cli.rb +1 -0
  6. data/lib/project_types/extension/commands/build.rb +0 -2
  7. data/lib/project_types/extension/commands/serve.rb +8 -3
  8. data/lib/project_types/extension/features/argo_serve.rb +1 -1
  9. data/lib/project_types/extension/forms/questions/ask_template.rb +3 -3
  10. data/lib/project_types/extension/messages/messages.rb +16 -0
  11. data/lib/project_types/extension/models/development_server_requirements.rb +1 -1
  12. data/lib/project_types/extension/models/server_config/capabilities.rb +11 -0
  13. data/lib/project_types/extension/models/server_config/development.rb +9 -0
  14. data/lib/project_types/extension/models/server_config/development_renderer.rb +2 -0
  15. data/lib/project_types/extension/models/server_config/extension.rb +10 -3
  16. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +1 -1
  17. data/lib/project_types/extension/models/specification_handlers/{beacon_extension.rb → web_pixel_extension.rb} +11 -10
  18. data/lib/project_types/extension/models/specification_handlers/{beacon_extension_utils → web_pixel_extension_utils}/script_config.rb +1 -1
  19. data/lib/project_types/extension/models/specification_handlers/{beacon_extension_utils → web_pixel_extension_utils}/script_config_repository.rb +2 -2
  20. data/lib/project_types/extension/tasks/configure_options.rb +1 -1
  21. data/lib/project_types/extension/tasks/convert_server_config.rb +8 -4
  22. data/lib/project_types/extension/tasks/execute_commands/outdated_extension_detection.rb +5 -1
  23. data/lib/project_types/extension/tasks/merge_server_config.rb +4 -2
  24. data/lib/project_types/script/cli.rb +1 -0
  25. data/lib/project_types/script/config/extension_points.yml +20 -18
  26. data/lib/project_types/script/graphql/app_script_set.graphql +2 -0
  27. data/lib/project_types/script/layers/application/push_script.rb +1 -0
  28. data/lib/project_types/script/layers/domain/app_bridge.rb +16 -0
  29. data/lib/project_types/script/layers/domain/script_project.rb +1 -0
  30. data/lib/project_types/script/layers/infrastructure/errors.rb +11 -0
  31. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +15 -1
  32. data/lib/project_types/script/layers/infrastructure/script_service.rb +9 -0
  33. data/lib/project_types/script/messages/messages.rb +4 -1
  34. data/lib/project_types/script/ui/error_handler.rb +8 -0
  35. data/lib/project_types/theme/commands/delete.rb +1 -0
  36. data/lib/project_types/theme/commands/open.rb +6 -1
  37. data/lib/project_types/theme/commands/publish.rb +1 -0
  38. data/lib/project_types/theme/commands/serve.rb +28 -0
  39. data/lib/project_types/theme/forms/select.rb +4 -1
  40. data/lib/project_types/theme/messages/messages.rb +33 -3
  41. data/lib/shopify_cli/admin_api.rb +1 -1
  42. data/lib/shopify_cli/commands/login.rb +2 -2
  43. data/lib/shopify_cli/context.rb +1 -1
  44. data/lib/shopify_cli/core/monorail.rb +1 -1
  45. data/lib/shopify_cli/git.rb +7 -2
  46. data/lib/shopify_cli/heroku.rb +5 -3
  47. data/lib/shopify_cli/messages/messages.rb +1 -1
  48. data/lib/shopify_cli/release.rb +5 -2
  49. data/lib/shopify_cli/services/app/create/node_service.rb +1 -1
  50. data/lib/shopify_cli/services/app/create/php_service.rb +1 -1
  51. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +5 -6
  52. data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -1
  53. data/lib/shopify_cli/theme/dev_server.rb +20 -3
  54. data/lib/shopify_cli/theme/development_theme.rb +11 -1
  55. data/lib/shopify_cli/theme/file.rb +9 -0
  56. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +2 -2
  57. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +1 -1
  58. data/lib/shopify_cli/theme/theme_admin_api.rb +3 -1
  59. data/lib/shopify_cli/version.rb +1 -1
  60. metadata +7 -5
@@ -37,7 +37,9 @@ module Script
37
37
  extension_point_type: extension_point_type,
38
38
  title: title,
39
39
  description: nil,
40
- language: language
40
+ language: language,
41
+ app_bridge_create_path: "/",
42
+ app_bridge_details_path: "/",
41
43
  )
42
44
 
43
45
  build_script_project(script_config: nil)
@@ -54,6 +56,7 @@ module Script
54
56
  extension_point_type: extension_point_type,
55
57
  language: language,
56
58
  script_config: script_config_repository.get!,
59
+ app_bridge: app_bridge,
57
60
  input_query: read_input_query,
58
61
  )
59
62
  end
@@ -94,6 +97,7 @@ module Script
94
97
  extension_point_type: extension_point_type,
95
98
  language: language,
96
99
  script_config: script_config,
100
+ app_bridge: app_bridge,
97
101
  )
98
102
  end
99
103
 
@@ -121,6 +125,16 @@ module Script
121
125
  project_config_value("language")&.downcase || default_language
122
126
  end
123
127
 
128
+ def app_bridge
129
+ create_path = project_config_value!("app_bridge_create_path")
130
+ details_path = project_config_value!("app_bridge_details_path")
131
+
132
+ Domain::AppBridge.new(
133
+ create_path: create_path,
134
+ details_path: details_path,
135
+ )
136
+ end
137
+
124
138
  def project_config_value(key)
125
139
  return nil unless project.config.key?(key)
126
140
  project.config[key]
@@ -20,6 +20,7 @@ module Script
20
20
  force: false,
21
21
  metadata:,
22
22
  script_config:,
23
+ app_bridge:,
23
24
  module_upload_url:,
24
25
  library:,
25
26
  input_query: nil
@@ -36,6 +37,10 @@ module Script
36
37
  scriptConfigVersion: script_config.version,
37
38
  configurationUi: script_config.configuration_ui,
38
39
  configurationDefinition: script_config.configuration&.to_json,
40
+ appBridge: {
41
+ createPath: app_bridge.create_path,
42
+ detailsPath: app_bridge.details_path,
43
+ },
39
44
  moduleUploadUrl: module_upload_url,
40
45
  inputQuery: input_query,
41
46
  }
@@ -87,6 +92,10 @@ module Script
87
92
  raise Errors::InvalidInputQueryErrors, errors.map { |err| err["message"] }
88
93
  elsif user_errors.find { |err| %w(not_use_msgpack_error schema_version_argument_error).include?(err["tag"]) }
89
94
  raise Domain::Errors::MetadataValidationError
95
+ elsif user_errors.find { |err| err["tag"] == "invalid_app_bridge_create_path" }
96
+ raise Errors::InvalidAppBridgePathError, "create"
97
+ elsif user_errors.find { |err| err["tag"] == "invalid_app_bridge_details_path" }
98
+ raise Errors::InvalidAppBridgePathError, "details"
90
99
  else
91
100
  raise Errors::GraphqlError, user_errors
92
101
  end
@@ -24,7 +24,7 @@ module Script
24
24
  oauth_help: "Wait a few minutes and try again.",
25
25
 
26
26
  invalid_context_cause: "Your .shopify-cli.yml is formatted incorrectly. It's missing values for "\
27
- "extension_point_type or title.",
27
+ "extension_point_type, title, app_bridge_create_path or app_bridge_details_path.",
28
28
  invalid_context_help: "Add these values.",
29
29
 
30
30
  invalid_script_title_cause: "Script title contains unsupported characters.",
@@ -164,6 +164,9 @@ module Script
164
164
  missing_env_file_variables: "The following are missing in the .env file: %s. ",
165
165
  missing_env_file_variables_solution: "To add it, connect your script with " \
166
166
  "{{command:%1$s script connect}} ",
167
+
168
+ invalid_app_bridge_path_cause: "The script couldn't be pushed because the App Bridge path is incorrect in .shopify-cli.yml.",
169
+ invalid_app_bridge_path_help: "The %{path_key} needs to be set to a path that starts with {{command:/}}.",
167
170
  },
168
171
 
169
172
  create: {
@@ -316,6 +316,14 @@ module Script
316
316
  ),
317
317
  help_suggestion: ShopifyCLI::Context.message("script.error.language_library_for_api_not_found_help"),
318
318
  }
319
+ when Layers::Infrastructure::Errors::InvalidAppBridgePathError
320
+ {
321
+ cause_of_error: ShopifyCLI::Context.message("script.error.invalid_app_bridge_path_cause"),
322
+ help_suggestion: ShopifyCLI::Context.message(
323
+ "script.error.invalid_app_bridge_path_help",
324
+ path_key: e.path_key,
325
+ ),
326
+ }
319
327
  end
320
328
  end
321
329
  end
@@ -25,6 +25,7 @@ module Theme
25
25
  title: @ctx.message("theme.delete.select"),
26
26
  exclude_roles: ["live"],
27
27
  include_foreign_developments: options.flags[:show_all],
28
+ cmd: :delete
28
29
  )
29
30
  return unless form
30
31
  [form.theme]
@@ -12,13 +12,18 @@ module Theme
12
12
  parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
13
13
  parser.on("-l", "--live") { flags[:live] = true }
14
14
  parser.on("-d", "--development") { flags[:development] = true }
15
+ parser.on("-e", "--editor") { flags[:editor] = true }
15
16
  end
16
17
 
17
18
  def call(_args, _name)
18
19
  theme = find_theme(**options.flags)
19
20
 
20
21
  @ctx.puts(@ctx.message("theme.open.details", theme.name, theme.preview_url, theme.editor_url))
21
- @ctx.open_browser_url!(theme.preview_url)
22
+ if options.flags[:editor]
23
+ @ctx.open_browser_url!(theme.editor_url)
24
+ else
25
+ @ctx.open_browser_url!(theme.preview_url)
26
+ end
22
27
  end
23
28
 
24
29
  def self.help
@@ -19,6 +19,7 @@ module Theme
19
19
  [],
20
20
  title: @ctx.message("theme.publish.select"),
21
21
  exclude_roles: ["live", "development", "demo"],
22
+ cmd: :publish
22
23
  )
23
24
  return unless form
24
25
  form.theme
@@ -17,12 +17,16 @@ module Theme
17
17
  parser.on("--poll") { flags[:poll] = true }
18
18
  parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
19
19
  parser.on("--theme-editor-sync") { flags[:editor_sync] = true }
20
+ parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
20
21
  end
21
22
 
22
23
  def call(_args, name)
24
+ valid_authentication_method!
25
+
23
26
  root = root_value(options, name)
24
27
  flags = options.flags.dup
25
28
  host = flags[:host] || DEFAULT_HTTP_HOST
29
+
26
30
  ShopifyCLI::Theme::DevServer.start(@ctx, root, host: host, **flags) do |syncer|
27
31
  UI::SyncProgressBar.new(syncer).progress(:upload_theme!, delay_low_priority_files: true)
28
32
  end
@@ -38,6 +42,30 @@ module Theme
38
42
  def self.help
39
43
  ShopifyCLI::Context.message("theme.serve.help", ShopifyCLI::TOOL_NAME)
40
44
  end
45
+
46
+ private
47
+
48
+ def valid_authentication_method!
49
+ if exchange_token && !storefront_renderer_token
50
+ ShopifyCLI::Context.abort(error_message, help_message)
51
+ end
52
+ end
53
+
54
+ def error_message
55
+ ShopifyCLI::Context.message("theme.serve.auth.error_message", ShopifyCLI::TOOL_NAME)
56
+ end
57
+
58
+ def help_message
59
+ ShopifyCLI::Context.message("theme.serve.auth.help_message", ShopifyCLI::TOOL_NAME, ShopifyCLI::TOOL_NAME)
60
+ end
61
+
62
+ def exchange_token
63
+ ShopifyCLI::DB.get(:shopify_exchange_token)
64
+ end
65
+
66
+ def storefront_renderer_token
67
+ ShopifyCLI::DB.get(:storefront_renderer_production_exchange_token)
68
+ end
41
69
  end
42
70
  end
43
71
  end
@@ -6,7 +6,7 @@ module Theme
6
6
  module Forms
7
7
  class Select < ShopifyCLI::Form
8
8
  attr_accessor :theme
9
- flag_arguments :root, :title, :exclude_roles, :include_foreign_developments
9
+ flag_arguments :root, :title, :exclude_roles, :include_foreign_developments, :cmd
10
10
 
11
11
  def ask
12
12
  self.theme = CLI::UI::Prompt.ask(title, allow_empty: false) do |handler|
@@ -18,6 +18,9 @@ module Theme
18
18
 
19
19
  handler.option(presenter.to_s(:short)) { theme }
20
20
  end
21
+ if handler.options.empty? && cmd
22
+ @ctx.abort(@ctx.message("theme.#{cmd}.no_themes_error"), @ctx.message("theme.#{cmd}.no_themes_resolution"))
23
+ end
21
24
  end
22
25
  end
23
26
 
@@ -8,6 +8,11 @@ module Theme
8
8
  Usage: {{command:%1$s theme [ %2$s ]}}
9
9
  HELP
10
10
  ensure_user_error: "You are not authorized to edit themes on %s.",
11
+ unauthorized_error: <<~EOD,
12
+ You can't use Shopify CLI with development stores if you only have Partner staff member access. If you want to use Shopify CLI to work on a development store, then you should be the store owner or create a staff account on the store.
13
+
14
+ If you're the store owner, then you need to log in to the store directly using the store URL at least once (for example, using %s.myshopify.com/admin) before you log in using Shopify CLI. Logging in to the Shopify admin directly connects the development store with your Shopify login.
15
+ EOD
11
16
  ensure_user_try_this: <<~ENSURE_USER,
12
17
  Check if your user is activated, has permission to edit themes at the store, and try to re-login.
13
18
  ENSURE_USER
@@ -37,6 +42,8 @@ module Theme
37
42
  HELP
38
43
  done: "Your theme is now live at %s",
39
44
  not_found: "Theme #%s does not exist",
45
+ no_themes_error: "You don't have any theme to be published.",
46
+ no_themes_resolution: "Try to create an unpublished theme with {{command:theme push -u -t <theme_name>}}.",
40
47
  select: "Select theme to push to",
41
48
  confirm: "Are you sure you want to make %s the new live theme on %s?",
42
49
  },
@@ -96,6 +103,7 @@ module Theme
96
103
  name: "Theme name",
97
104
  },
98
105
  serve: {
106
+ theme_not_found: "Theme \"%s\" doesn't exist",
99
107
  help: <<~HELP,
100
108
  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.
101
109
 
@@ -116,6 +124,14 @@ module Theme
116
124
  viewing_theme: "Viewing theme…",
117
125
  syncing_theme: "Syncing theme #%s on %s",
118
126
  open_fail: "Couldn't open the theme",
127
+ auth: {
128
+ error_message: <<~ERROR_MESSAGE,
129
+ It looks like you are using credentials that do not work with {{command:%s theme serve}}.
130
+ ERROR_MESSAGE
131
+ help_message: <<~HELP_MESSAGE,
132
+ Run {{command:%s logout}} and {{command:%s login --password "" --store STORE}} to force the authentication thought your browser.
133
+ HELP_MESSAGE
134
+ },
119
135
  operation: {
120
136
  status: {
121
137
  error: "ERROR",
@@ -126,14 +142,14 @@ module Theme
126
142
  syncer: {
127
143
  forms: {
128
144
  apply_to_all: {
129
- title: "Would like apply this to all the other %s files?",
145
+ title: "Would you like to apply this to all the other %s files?",
130
146
  yes: "Yes",
131
147
  no: "No",
132
148
  },
133
149
  update_strategy: {
134
150
  title_context: <<~TITLE,
135
151
 
136
- The local file {{command:%s}} is different from the remote version in the development theme."
152
+ The local file {{command:%s}} is different from the remote version in the development theme.
137
153
  TITLE
138
154
  title_question: "What would you like to do?",
139
155
  keep_remote: "Keep the remote version",
@@ -141,10 +157,21 @@ module Theme
141
157
  union_merge: "Merge files (it may break the local file)",
142
158
  exit: "Exit",
143
159
  },
160
+ update_remote_deleted_strategy: {
161
+ title_context: <<~TITLE,
162
+
163
+ The local file {{command:%s}} doesn’t exist in the remote version of the development theme.
164
+ TITLE
165
+ title_question: "What would you like to do?",
166
+ keep_remote: "Keep the remote version (and remove it locally)",
167
+ keep_local: "Keep the local version (and restore it remotely)",
168
+ union_merge: "Merge files (it may break the local file)",
169
+ exit: "Exit",
170
+ },
144
171
  delete_strategy: {
145
172
  title_context: <<~TITLE,
146
173
 
147
- The local file {{command:%s}} has been recently removed, but it's present on your remote development theme.",
174
+ The local file {{command:%s}} has been recently removed, but it's present on your remote development theme.
148
175
  TITLE
149
176
  title_question: "What would you like to do?",
150
177
  delete: "Delete permanently",
@@ -209,6 +236,8 @@ module Theme
209
236
  HELP
210
237
  select: "Select theme to delete",
211
238
  done: "%s theme(s) deleted",
239
+ no_themes_error: "You don't have any theme to be deleted.",
240
+ no_themes_resolution: "Try to create an unpublished theme with {{command:theme push -u -t <theme_name>}}.",
212
241
  not_found: "{{x}} Theme #%s does not exist",
213
242
  live: "{{x}} Theme #%s is your live theme. You can't delete it.",
214
243
  confirm: "Are you sure you want to delete %s on %s?",
@@ -280,6 +309,7 @@ module Theme
280
309
  {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of your theme.
281
310
  {{command:-l, --live}} Open your live theme.
282
311
  {{command:-d, --development}} Open your development theme.
312
+ {{command:-e, --editor}} Open the editor to the specified/selected theme.
283
313
  HELP
284
314
  },
285
315
  list: {
@@ -156,7 +156,7 @@ module ShopifyCLI
156
156
  def auth_headers(token)
157
157
  {
158
158
  Authorization: "Bearer #{token}",
159
- "X-Shopify-Access-Token" => token, # TODO: Remove when we no longer need private apps
159
+ "X-Shopify-Access-Token" => token,
160
160
  }
161
161
  end
162
162
  end
@@ -26,8 +26,8 @@ module ShopifyCLI
26
26
  end
27
27
  end
28
28
 
29
- # As password auth will soon be deprecated, we enable only in CI
30
- if @ctx.ci? && (password = options.flags[:password] || @ctx.getenv("SHOPIFY_PASSWORD"))
29
+ password = options.flags[:password] || @ctx.getenv("SHOPIFY_PASSWORD")
30
+ if !password.nil? && !password.empty?
31
31
  ShopifyCLI::DB.set(shopify_exchange_token: password)
32
32
  else
33
33
  IdentityAuth.new(ctx: @ctx).authenticate(spinner: true)
@@ -630,7 +630,7 @@ module ShopifyCLI
630
630
  end
631
631
  end
632
632
  latest_version = ShopifyCLI::Config.get(VERSION_CHECK_SECTION, LATEST_VERSION_FIELD, default: ShopifyCLI::VERSION)
633
- latest_version unless latest_version == ShopifyCLI::VERSION
633
+ latest_version if ::Semantic::Version.new(latest_version) > ::Semantic::Version.new(ShopifyCLI::VERSION)
634
634
  end
635
635
 
636
636
  # Returns file extension depending on OS
@@ -6,7 +6,7 @@ require "rbconfig"
6
6
  module ShopifyCLI
7
7
  module Core
8
8
  module Monorail
9
- ENDPOINT_URI = URI.parse("https://monorail-edge.shopifycloud.com/v1/produce")
9
+ ENDPOINT_URI = URI.parse("https://monorail-edge.shopifysvc.com/v1/produce")
10
10
  INVOCATIONS_SCHEMA = "app_cli_command/5.0"
11
11
 
12
12
  # Extra hash of data that will be sent in the payload
@@ -67,9 +67,14 @@ module ShopifyCLI
67
67
  if Dir.exist?(dest)
68
68
  ctx.abort(ctx.message("core.git.error.directory_exists"))
69
69
  else
70
+ repo, branch = repository.split("#")
70
71
  success_message = ctx.message("core.git.cloned", dest)
71
- CLI::UI::Frame.open(ctx.message("core.git.cloning", repository, dest), success_text: success_message) do
72
- clone_progress("clone", "--single-branch", repository, dest, ctx: ctx)
72
+ CLI::UI::Frame.open(ctx.message("core.git.cloning", repo, dest), success_text: success_message) do
73
+ if branch
74
+ clone_progress("clone", "--single-branch", "--branch", branch, repo, dest, ctx: ctx)
75
+ else
76
+ clone_progress("clone", "--single-branch", repo, dest, ctx: ctx)
77
+ end
73
78
  end
74
79
  end
75
80
  end
@@ -4,6 +4,7 @@ module ShopifyCLI
4
4
  linux: "https://cli-assets.heroku.com/heroku-linux-x64.tar.gz",
5
5
  mac: "https://cli-assets.heroku.com/heroku-darwin-x64.tar.gz",
6
6
  windows: "https://cli-assets.heroku.com/heroku-x64.exe",
7
+ mac_m1: "https://cli-assets.heroku.com/heroku-darwin-x64.tar.gz",
7
8
  }
8
9
 
9
10
  def initialize(ctx)
@@ -97,15 +98,16 @@ module ShopifyCLI
97
98
  if File.exist?(local_path)
98
99
  local_path
99
100
  elsif @ctx.windows?
100
- # Check if Heroku exists in the Windows registry and run it from there
101
- require "win32/registry"
102
101
  begin
102
+ # Check if Heroku exists in the Windows registry and run it from there
103
+ require "win32/registry"
104
+
103
105
  windows_path = Win32::Registry::HKEY_CURRENT_USER.open('SOFTWARE\heroku') do |reg|
104
106
  reg[""] # This reads the 'Default' registry key
105
107
  end
106
108
 
107
109
  File.join(windows_path, "bin", "heroku").to_s
108
- rescue
110
+ rescue StandardError, LoadError
109
111
  "heroku"
110
112
  end
111
113
  else
@@ -285,7 +285,7 @@ module ShopifyCLI
285
285
  },
286
286
  extension: {
287
287
  push: {
288
- beacon_extension: {
288
+ web_pixel_extension: {
289
289
  error: {
290
290
  file_read_error: "There was a problem reading %s",
291
291
  missing_config_key_error: "Configuration is missing key: %s",
@@ -8,7 +8,6 @@ module ShopifyCLI
8
8
  class Release
9
9
  def initialize(new_version, github_access_token)
10
10
  @new_version = new_version
11
- @changelog = ShopifyCLI::Changelog.new
12
11
  @github = Octokit::Client.new(access_token: github_access_token)
13
12
  end
14
13
 
@@ -32,7 +31,7 @@ module ShopifyCLI
32
31
 
33
32
  private
34
33
 
35
- attr_reader :new_version, :changelog, :github
34
+ attr_reader :new_version, :github
36
35
 
37
36
  def ensure_updated_main
38
37
  # We can't be sure what is the correct action to take if changes have been
@@ -190,5 +189,9 @@ module ShopifyCLI
190
189
  def system_or_fail(command, action)
191
190
  raise "Failed to #{action}!" unless system(command)
192
191
  end
192
+
193
+ def changelog
194
+ @changelog ||= ShopifyCLI::Changelog.new
195
+ end
193
196
  end
194
197
  end
@@ -117,7 +117,7 @@ module ShopifyCLI
117
117
  end
118
118
 
119
119
  def build(name)
120
- ShopifyCLI::Git.clone("https://github.com/Shopify/shopify-app-node.git", name)
120
+ ShopifyCLI::Git.clone("https://github.com/Shopify/shopify-app-template-node.git#cli_two", name)
121
121
 
122
122
  context.root = File.join(context.root, name)
123
123
 
@@ -74,7 +74,7 @@ module ShopifyCLI
74
74
  end
75
75
 
76
76
  def build(form)
77
- ShopifyCLI::Git.clone("https://github.com/Shopify/shopify-app-php.git", form.name)
77
+ ShopifyCLI::Git.clone("https://github.com/Shopify/shopify-app-template-php.git#cli_two", form.name)
78
78
 
79
79
  context.root = File.join(context.root, form.name)
80
80
  context.chdir(context.root)
@@ -14,13 +14,12 @@ module ShopifyCLI
14
14
 
15
15
  files.each do |file|
16
16
  section_hash(file).each do |key, value|
17
- name = key
18
- type = value&.dig("type")
19
-
20
- next if !name || !type
17
+ next unless key
18
+ next unless value.is_a?(Hash)
19
+ next unless (type = value&.dig("type"))
21
20
 
22
21
  index[type] = [] unless index[type]
23
- index[type] << name
22
+ index[type] << key
24
23
  end
25
24
  end
26
25
 
@@ -42,7 +41,7 @@ module ShopifyCLI
42
41
  end
43
42
 
44
43
  def files
45
- @theme.json_files
44
+ @theme.json_files.filter(&:template?)
46
45
  end
47
46
  end
48
47
  end
@@ -14,7 +14,8 @@ module ShopifyCLI
14
14
  @theme = theme
15
15
  @syncer = syncer
16
16
  @ignore_filter = ignore_filter
17
- @listener = Listen.to(@theme.root, force_polling: poll) do |modified, added, removed|
17
+ @listener = Listen.to(@theme.root, force_polling: poll,
18
+ ignore: @ignore_filter&.regexes) do |modified, added, removed|
18
19
  changed
19
20
  notify_observers(modified, added, removed)
20
21
  end
@@ -27,12 +27,13 @@ module ShopifyCLI
27
27
  class << self
28
28
  attr_accessor :ctx
29
29
 
30
- def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false, editor_sync: false, mode: ReloadMode.default)
30
+ def start(ctx, root, host: "127.0.0.1", theme: nil, port: 9292, poll: false, editor_sync: false,
31
+ mode: ReloadMode.default)
31
32
  @ctx = ctx
32
- theme = DevelopmentTheme.find_or_create!(ctx, root: root)
33
+ theme = find_theme(root, theme)
33
34
  ignore_filter = IgnoreFilter.from_path(root)
34
35
  @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter, overwrite_json: !editor_sync)
35
- watcher = Watcher.new(ctx, theme: theme, syncer: @syncer, poll: poll)
36
+ watcher = Watcher.new(ctx, theme: theme, ignore_filter: ignore_filter, syncer: @syncer, poll: poll)
36
37
  remote_watcher = RemoteWatcher.to(theme: theme, syncer: @syncer)
37
38
 
38
39
  # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
@@ -108,6 +109,22 @@ module ShopifyCLI
108
109
  @syncer.shutdown
109
110
  WebServer.shutdown
110
111
  end
112
+
113
+ private
114
+
115
+ def find_theme(root, identifier)
116
+ return theme_by_identifier(root, identifier) if identifier
117
+ DevelopmentTheme.find_or_create!(@ctx, root: root)
118
+ end
119
+
120
+ def theme_by_identifier(root, identifier)
121
+ theme = ShopifyCLI::Theme::Theme.find_by_identifier(@ctx, root: root, identifier: identifier)
122
+ theme || not_found_error(identifier)
123
+ end
124
+
125
+ def not_found_error(identifier)
126
+ @ctx.abort(@ctx.message("theme.serve.theme_not_found", identifier))
127
+ end
111
128
  end
112
129
  end
113
130
  end
@@ -79,13 +79,23 @@ module ShopifyCLI
79
79
 
80
80
  theme_name = "Development ()"
81
81
  hostname_character_limit = API_NAME_LIMIT - theme_name.length - hash.length - 1
82
- identifier = "#{hash}-#{hostname[0, hostname_character_limit]}"
82
+ identifier = encode_identifier("#{hash}-#{hostname[0, hostname_character_limit]}")
83
83
  theme_name = "Development (#{identifier})"
84
84
 
85
85
  ShopifyCLI::DB.set(development_theme_name: theme_name)
86
86
 
87
87
  theme_name
88
88
  end
89
+
90
+ ##
91
+ # In some cases, the identifier string encoding may be obfuscated by the hostname,
92
+ # which may be an ASCII string.
93
+ #
94
+ # This method ensures the result identifier is a UTF-8 valid string.
95
+ #
96
+ def encode_identifier(identifier)
97
+ identifier.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "-")
98
+ end
89
99
  end
90
100
  end
91
101
  end
@@ -30,6 +30,15 @@ module ShopifyCLI
30
30
  else
31
31
  path.write(content, 0, mode: "wb")
32
32
  end
33
+ rescue Encoding::UndefinedConversionError
34
+ ##
35
+ # The CLI tries to write the file and normalize EOL characters to avoid
36
+ # errors on Windows when files are shared across different operational systems.
37
+ #
38
+ # The CLI fallbacks any error during the conversion by writing the file
39
+ # in binary mode when the normalization fails (e.g., ASCII files), so no data is lost.
40
+ #
41
+ path.write(content, 0, mode: "wb")
33
42
  end
34
43
 
35
44
  def delete
@@ -7,7 +7,7 @@ module ShopifyCLI
7
7
  class Syncer
8
8
  module Forms
9
9
  class SelectUpdateStrategy < BaseStrategyForm
10
- flag_arguments :file
10
+ flag_arguments :file, :exists_remotely
11
11
 
12
12
  def strategies
13
13
  %i[
@@ -19,7 +19,7 @@ module ShopifyCLI
19
19
  end
20
20
 
21
21
  def prefix
22
- "theme.serve.syncer.forms.update_strategy"
22
+ "theme.serve.syncer.forms.#{exists_remotely ? "update_strategy" : "update_remote_deleted_strategy"}"
23
23
  end
24
24
  end
25
25
  end
@@ -74,7 +74,7 @@ module ShopifyCLI
74
74
  end
75
75
 
76
76
  def ask_update_strategy(file)
77
- Forms::SelectUpdateStrategy.ask(@ctx, [], file: file).strategy
77
+ Forms::SelectUpdateStrategy.ask(@ctx, [], file: file, exists_remotely: file_exist_remotely?(file)).strategy
78
78
  end
79
79
  end
80
80
  end
@@ -53,8 +53,10 @@ module ShopifyCLI
53
53
  # * when an asset operation cannot be performed:
54
54
  # - <APIRequestForbiddenError: 403 {"message":"templates/gift_card.liquid could not be deleted"}>
55
55
  #
56
- if empty_response?(error) || unauthorized_response?(error)
56
+ if empty_response?(error)
57
57
  return permission_error
58
+ elsif unauthorized_response?(error)
59
+ raise ShopifyCLI::Abort, @ctx.message("theme.unauthorized_error", @shop)
58
60
  end
59
61
 
60
62
  raise error
@@ -1,3 +1,3 @@
1
1
  module ShopifyCLI
2
- VERSION = "2.16.1"
2
+ VERSION = "2.18.1"
3
3
  end