shopify-cli 2.24.0 → 2.26.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/dev.yml +0 -3
  6. data/lib/project_types/extension/commands/serve.rb +57 -3
  7. data/lib/project_types/extension/extension_project.rb +8 -1
  8. data/lib/project_types/extension/loaders/project.rb +3 -2
  9. data/lib/project_types/extension/messages/messages.rb +21 -6
  10. data/lib/project_types/extension/models/server_config/development_renderer.rb +1 -1
  11. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +18 -6
  12. data/lib/project_types/script/cli.rb +0 -79
  13. data/lib/project_types/script/commands/connect.rb +3 -8
  14. data/lib/project_types/script/commands/create.rb +4 -29
  15. data/lib/project_types/script/commands/javy.rb +3 -8
  16. data/lib/project_types/script/commands/push.rb +4 -41
  17. data/lib/project_types/script/messages/messages.rb +1 -258
  18. data/lib/project_types/theme/commands/common/shop_helper.rb +13 -0
  19. data/lib/project_types/theme/commands/delete.rb +4 -1
  20. data/lib/project_types/theme/commands/list.rb +3 -4
  21. data/lib/project_types/theme/commands/open.rb +4 -1
  22. data/lib/project_types/theme/commands/publish.rb +4 -1
  23. data/lib/project_types/theme/commands/pull.rb +3 -1
  24. data/lib/project_types/theme/commands/push.rb +3 -1
  25. data/lib/project_types/theme/commands/serve.rb +15 -3
  26. data/lib/project_types/theme/messages/messages.rb +9 -7
  27. data/lib/shopify_cli/commands/logout.rb +13 -2
  28. data/lib/shopify_cli/environment.rb +1 -1
  29. data/lib/shopify_cli/file_system_listener.rb +30 -0
  30. data/lib/shopify_cli/git.rb +116 -33
  31. data/lib/shopify_cli/identity_auth.rb +1 -0
  32. data/lib/shopify_cli/messages/messages.rb +1 -1
  33. data/lib/shopify_cli/packager.rb +12 -3
  34. data/lib/shopify_cli/project.rb +1 -1
  35. data/lib/shopify_cli/release.rb +4 -2
  36. data/lib/shopify_cli/tasks/ensure_project_type.rb +3 -1
  37. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +1 -1
  38. data/lib/shopify_cli/theme/dev_server/certificate_manager.rb +1 -1
  39. data/lib/shopify_cli/theme/dev_server/errors.rb +9 -0
  40. data/lib/shopify_cli/theme/dev_server/header_hash.rb +1 -1
  41. data/lib/shopify_cli/theme/dev_server/hooks/file_change_hook.rb +77 -0
  42. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +1 -1
  43. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  44. data/lib/shopify_cli/theme/dev_server/{hot-reload-no-script.html → hot_reload/resources/hot-reload-no-script.html} +0 -0
  45. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/hot_reload.js +48 -0
  46. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/sse_client.js +43 -0
  47. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme.js +114 -0
  48. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme_extension.js +121 -0
  49. data/lib/shopify_cli/theme/dev_server/hot_reload/script_injector.rb +57 -0
  50. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +1 -1
  51. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +8 -76
  52. data/lib/shopify_cli/theme/dev_server/local_assets.rb +28 -28
  53. data/lib/shopify_cli/theme/dev_server/proxy.rb +33 -25
  54. data/lib/shopify_cli/theme/dev_server/proxy_param_builder.rb +82 -0
  55. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +1 -1
  56. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +1 -1
  57. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +1 -1
  58. data/lib/shopify_cli/theme/dev_server/sse.rb +1 -1
  59. data/lib/shopify_cli/theme/dev_server/watcher.rb +8 -9
  60. data/lib/shopify_cli/theme/dev_server/web_server.rb +1 -1
  61. data/lib/shopify_cli/theme/dev_server.rb +287 -99
  62. data/lib/shopify_cli/theme/extension/app_extension.rb +40 -0
  63. data/lib/shopify_cli/theme/extension/dev_server/hooks/file_change_hook.rb +68 -0
  64. data/lib/shopify_cli/theme/extension/dev_server/hot_reload/script_injector.rb +30 -0
  65. data/lib/shopify_cli/theme/extension/dev_server/hot_reload.rb +13 -0
  66. data/lib/shopify_cli/theme/extension/dev_server/local_assets.rb +30 -0
  67. data/lib/shopify_cli/theme/{dev_server/proxy/template_param_builder.rb → extension/dev_server/proxy_param_builder.rb} +26 -16
  68. data/lib/shopify_cli/theme/extension/dev_server/watcher.rb +47 -0
  69. data/lib/shopify_cli/theme/extension/dev_server.rb +150 -0
  70. data/lib/shopify_cli/theme/extension/host_theme.rb +104 -0
  71. data/lib/shopify_cli/theme/extension/syncer/extension_serve_job.rb +133 -0
  72. data/lib/shopify_cli/theme/extension/syncer/operation.rb +21 -0
  73. data/lib/shopify_cli/theme/extension/syncer.rb +81 -0
  74. data/lib/shopify_cli/theme/extension/ui/host_theme_progress_bar.rb +35 -0
  75. data/lib/shopify_cli/theme/ignore_helper.rb +31 -0
  76. data/lib/shopify_cli/theme/root.rb +62 -0
  77. data/lib/shopify_cli/theme/syncer.rb +12 -6
  78. data/lib/shopify_cli/theme/theme.rb +10 -52
  79. data/lib/shopify_cli/version.rb +1 -1
  80. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +1 -1
  81. metadata +28 -53
  82. data/.github/workflows/triage.yml +0 -22
  83. data/lib/project_types/script/config/extension_points.yml +0 -45
  84. data/lib/project_types/script/errors.rb +0 -10
  85. data/lib/project_types/script/forms/ask_app.rb +0 -27
  86. data/lib/project_types/script/forms/ask_org.rb +0 -30
  87. data/lib/project_types/script/forms/ask_script_uuid.rb +0 -22
  88. data/lib/project_types/script/forms/create.rb +0 -33
  89. data/lib/project_types/script/forms/run_against_shopify_org.rb +0 -14
  90. data/lib/project_types/script/graphql/app_script_set.graphql +0 -46
  91. data/lib/project_types/script/graphql/get_app_scripts.graphql +0 -6
  92. data/lib/project_types/script/graphql/module_upload_url_generate.graphql +0 -13
  93. data/lib/project_types/script/graphql/script_service_proxy.graphql +0 -7
  94. data/lib/project_types/script/layers/application/build_script.rb +0 -25
  95. data/lib/project_types/script/layers/application/connect_app.rb +0 -86
  96. data/lib/project_types/script/layers/application/create_script.rb +0 -90
  97. data/lib/project_types/script/layers/application/extension_points.rb +0 -66
  98. data/lib/project_types/script/layers/application/project_dependencies.rb +0 -26
  99. data/lib/project_types/script/layers/application/push_script.rb +0 -74
  100. data/lib/project_types/script/layers/domain/app_bridge.rb +0 -16
  101. data/lib/project_types/script/layers/domain/errors.rb +0 -47
  102. data/lib/project_types/script/layers/domain/extension_point.rb +0 -81
  103. data/lib/project_types/script/layers/domain/metadata.rb +0 -46
  104. data/lib/project_types/script/layers/domain/push_package.rb +0 -41
  105. data/lib/project_types/script/layers/domain/script_config.rb +0 -32
  106. data/lib/project_types/script/layers/domain/script_project.rb +0 -61
  107. data/lib/project_types/script/layers/infrastructure/api_clients/partners_proxy_api_client.rb +0 -53
  108. data/lib/project_types/script/layers/infrastructure/api_clients/script_service_api_client.rb +0 -35
  109. data/lib/project_types/script/layers/infrastructure/command_runner.rb +0 -19
  110. data/lib/project_types/script/layers/infrastructure/errors.rb +0 -211
  111. data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +0 -37
  112. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +0 -62
  113. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +0 -47
  114. data/lib/project_types/script/layers/infrastructure/languages/tool_version_checker.rb +0 -26
  115. data/lib/project_types/script/layers/infrastructure/languages/typescript_project_creator.rb +0 -45
  116. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +0 -103
  117. data/lib/project_types/script/layers/infrastructure/languages/wasm_project_creator.rb +0 -12
  118. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +0 -32
  119. data/lib/project_types/script/layers/infrastructure/metadata_repository.rb +0 -18
  120. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +0 -36
  121. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +0 -273
  122. data/lib/project_types/script/layers/infrastructure/script_service.rb +0 -135
  123. data/lib/project_types/script/layers/infrastructure/script_uploader.rb +0 -40
  124. data/lib/project_types/script/layers/infrastructure/service_locator.rb +0 -20
  125. data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +0 -35
  126. data/lib/project_types/script/ui/error_handler.rb +0 -331
  127. data/lib/project_types/script/ui/printing_spinner.rb +0 -75
  128. data/lib/project_types/script/ui/strict_spinner.rb +0 -20
  129. data/lib/shopify_cli/theme/dev_server/hot-reload.js +0 -194
  130. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +0 -33
@@ -46,12 +46,77 @@ module ShopifyCLI
46
46
  end
47
47
 
48
48
  ##
49
- # will make calls to git to clone a new repo into a supplied destination,
50
- # it will also output progress of the cloning process.
49
+ # returns array with components of git clone command
51
50
  #
52
51
  # #### Parameters
53
52
  #
54
- # * `repository` - a git url for git to clone the repo from
53
+ # * `repo` - repo url without branch name
54
+ # * `dest` - a filepath to where the repo should be cloned to
55
+ # * `branch` - branch name when cloning
56
+ #
57
+ # #### Returns
58
+ #
59
+ # * array of strings
60
+ #
61
+ # #### Example
62
+ #
63
+ # ["clone", "--single-branch", "--branch", "test-branch", "test-app"]
64
+ #
65
+ def git_clone_command(repo, dest, branch)
66
+ if branch
67
+ ["clone", "--single-branch", "--branch", branch, repo, dest]
68
+ else
69
+ ["clone", "--single-branch", repo, dest]
70
+ end
71
+ end
72
+
73
+ ##
74
+ # calls git to clone a new repo into a supplied destination,
75
+ # it will also call a supplied block with the percentage of clone completion
76
+ #
77
+ # #### Parameters
78
+ #
79
+ # * `repo_with_branch` - a git url for git to clone the repo from
80
+ # * `dest` - a filepath to where the repo should be cloned to
81
+ # * `ctx` - the current running context of your command, defaults to a new context.
82
+ #
83
+ # #### Returns
84
+ #
85
+ # * `sha_string` - string of the sha of the most recent commit to the repo
86
+ #
87
+ # #### Example
88
+ #
89
+ # ShopifyCLI::Git.raw_clone('git@github.com:shopify/test.git', 'test-app')
90
+ #
91
+ def raw_clone(repo_with_branch, dest, ctx: Context.new)
92
+ if Dir.exist?(dest) && !Dir.empty?(dest)
93
+ ctx.abort(ctx.message("core.git.error.directory_exists"))
94
+ else
95
+ msg = []
96
+ # require at usage point to not slow down CLI startup
97
+ # https://github.com/Shopify/shopify-cli/pull/698#discussion_r444342445
98
+ require "open3"
99
+
100
+ repo, branch = repo_with_branch.split("#")
101
+ git_cmd = git_clone_command(repo, dest, branch)
102
+
103
+ success = Open3.popen3("git", *git_cmd, "--progress") do |_stdin, _stdout, stderr, thread|
104
+ msg = clone_progress(stderr, bar: nil)
105
+
106
+ thread.value
107
+ end.success?
108
+
109
+ ctx.abort((msg.join("\n"))) unless success
110
+ end
111
+ end
112
+
113
+ ##
114
+ # calls git to clone a new repo into a supplied destination,
115
+ # it will also output progress of the cloning process into a new progress bar
116
+ #
117
+ # #### Parameters
118
+ #
119
+ # * `repo_with_branch` - a git url for git to clone the repo from
55
120
  # * `dest` - a filepath to where the repo should be cloned to
56
121
  # * `ctx` - the current running context of your command, defaults to a new context.
57
122
  #
@@ -63,17 +128,30 @@ module ShopifyCLI
63
128
  #
64
129
  # ShopifyCLI::Git.clone('git@github.com:shopify/test.git', 'test-app')
65
130
  #
66
- def clone(repository, dest, ctx: Context.new)
67
- if Dir.exist?(dest)
131
+ def clone(repo_with_branch, dest, ctx: Context.new)
132
+ if Dir.exist?(dest) && !Dir.empty?(dest)
68
133
  ctx.abort(ctx.message("core.git.error.directory_exists"))
69
134
  else
70
- repo, branch = repository.split("#")
135
+ msg = []
136
+ # require at usage point to not slow down CLI startup
137
+ # https://github.com/Shopify/shopify-cli/pull/698#discussion_r444342445
138
+ require "open3"
139
+
140
+ repo, branch = repo_with_branch.split("#")
141
+ git_cmd = git_clone_command(repo, dest, branch)
142
+
71
143
  success_message = ctx.message("core.git.cloned", dest)
144
+
72
145
  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)
146
+ CLI::UI::Progress.progress do |bar|
147
+ success = Open3.popen3("git", *git_cmd, "--progress") do |_stdin, _stdout, stderr, thread|
148
+ msg = clone_progress(stderr, bar: bar)
149
+
150
+ thread.value
151
+ end.success?
152
+
153
+ ctx.abort((msg.join("\n"))) unless success
154
+ bar.tick(set_percent: 1.0)
77
155
  end
78
156
  end
79
157
  end
@@ -197,6 +275,34 @@ module ShopifyCLI
197
275
  end
198
276
  end
199
277
 
278
+ ##
279
+ # handles showing the progress of the git clone command.
280
+ # if block given, assumes passing percent to block, otherwise
281
+ # increments bar for progress bar
282
+ #
283
+ # #### Parameters
284
+ #
285
+ # * `stderr` - Open3.popen3 output stream
286
+ # * `bar` - progress bar object to set percent
287
+ #
288
+ def clone_progress(stderr, bar: nil)
289
+ msg = []
290
+
291
+ while (line = stderr.gets)
292
+ msg << line.chomp
293
+ next unless line.strip.start_with?("Receiving objects:")
294
+ percent = (line.match(/Receiving objects:\s+(\d+)/)[1].to_f / 100).round(2)
295
+
296
+ if block_given?
297
+ yield percent
298
+ elsif !bar.nil?
299
+ bar.tick(set_percent: percent)
300
+ end
301
+ end
302
+
303
+ msg
304
+ end
305
+
200
306
  private
201
307
 
202
308
  def exec(*args, dir: Dir.pwd, default: nil, ctx: Context.new)
@@ -209,29 +315,6 @@ module ShopifyCLI
209
315
  def rev_parse(*args, dir: nil, ctx: Context.new)
210
316
  exec("rev-parse", *args, dir: dir, ctx: ctx)
211
317
  end
212
-
213
- def clone_progress(*git_command, ctx:)
214
- CLI::UI::Progress.progress do |bar|
215
- msg = []
216
- require "open3"
217
-
218
- success = Open3.popen3("git", *git_command, "--progress") do |_stdin, _stdout, stderr, thread|
219
- while (line = stderr.gets)
220
- msg << line.chomp
221
- next unless line.strip.start_with?("Receiving objects:")
222
- percent = (line.match(/Receiving objects:\s+(\d+)/)[1].to_f / 100).round(2)
223
- bar.tick(set_percent: percent)
224
- next
225
- end
226
-
227
- thread.value
228
- end.success?
229
-
230
- ctx.abort(msg.join("\n")) unless success
231
- bar.tick(set_percent: 1.0)
232
- success
233
- end
234
- end
235
318
  end
236
319
  end
237
320
  end
@@ -87,6 +87,7 @@ module ShopifyCLI
87
87
 
88
88
  def fetch_or_auth_partners_token
89
89
  if EnvAuthToken.partners_token_present?
90
+ return Environment.auth_token if Environment.run_as_subprocess?
90
91
  return EnvAuthToken.fetch_exchanged_partners_token do |env_token|
91
92
  exchange_partners_auth_token(env_token)
92
93
  end
@@ -788,7 +788,7 @@ module ShopifyCLI
788
788
  {{*}} {{yellow:A new version of Shopify CLI is available! You have version %s and the latest version is %s.
789
789
 
790
790
  To upgrade, follow the instructions for the package manager you’re using:
791
- {{underline:https://shopify.dev/tools/cli/troubleshooting#upgrade-shopify-cli}}}}
791
+ {{underline:https://shopify.dev/themes/tools/cli/upgrade-uninstall}}}}
792
792
 
793
793
  MESSAGE
794
794
 
@@ -70,9 +70,10 @@ module ShopifyCLI
70
70
  root_dir = File.join(PACKAGING_DIR, "homebrew")
71
71
 
72
72
  build_path = File.join(BUILDS_DIR, "shopify-cli.rb")
73
- puts "\nBuilding Homebrew package"
73
+ build_path_2 = File.join(BUILDS_DIR, "shopify-cli@2.rb")
74
+ puts "\nBuilding Homebrew packages"
74
75
 
75
- puts "Generating formula…"
76
+ puts "Generating formulae…"
76
77
  File.delete(build_path) if File.exist?(build_path)
77
78
 
78
79
  spec_contents = File.read(File.join(root_dir, "shopify-cli.base.rb"))
@@ -89,7 +90,15 @@ module ShopifyCLI
89
90
  spec_contents = spec_contents.gsub("SHOPIFY_CLI_GEM_CHECKSUM", gem_checksum)
90
91
 
91
92
  puts "Writing generated formula\n To: #{build_path}\n\n"
92
- File.write(build_path, spec_contents)
93
+ File.write(build_path, spec_contents.gsub("SHOPIFY_CLI_BINSTUB_SUFFIX", ""))
94
+
95
+ puts "Writing generated formula\n To: #{build_path_2}\n\n"
96
+ File.write(
97
+ build_path_2,
98
+ spec_contents
99
+ .sub("class ShopifyCli < Formula", "class ShopifyCliAT2 < Formula")
100
+ .sub("SHOPIFY_CLI_BINSTUB_SUFFIX", "2")
101
+ )
93
102
  end
94
103
 
95
104
  private
@@ -109,7 +109,7 @@ module ShopifyCLI
109
109
 
110
110
  def at(dir)
111
111
  proj_dir = directory(dir)
112
- unless proj_dir
112
+ if !proj_dir && !ShopifyCLI::Environment.run_as_subprocess?
113
113
  raise(ShopifyCLI::Abort, Context.message("core.project.error.not_in_project"))
114
114
  end
115
115
  @at ||= Hash.new { |h, k| h[k] = new(directory: k) }
@@ -120,8 +120,10 @@ module ShopifyCLI
120
120
  end
121
121
 
122
122
  def update_homebrew_repo
123
- source_file = File.join(package_dir, "shopify-cli.rb")
124
- FileUtils.copy(source_file, homebrew_path)
123
+ %w(shopify-cli.rb shopify-cli@2.rb).each do |source_filename|
124
+ source_file = File.join(package_dir, source_filename)
125
+ FileUtils.copy(source_file, homebrew_path)
126
+ end
125
127
  Dir.chdir(homebrew_path) do
126
128
  system_or_fail("git commit -am '#{homebrew_update_message}'", "commit homebrew update")
127
129
  system_or_fail("git push -u origin #{homebrew_release_branch}", "push homebrew branch")
@@ -4,7 +4,9 @@ module ShopifyCLI
4
4
  module Tasks
5
5
  class EnsureProjectType < ShopifyCLI::Task
6
6
  def call(ctx, project_type)
7
- return true if project_type.to_sym == ShopifyCLI::Project.current_project_type
7
+ if project_type.to_sym == ShopifyCLI::Project.current_project_type || Environment.run_as_subprocess?
8
+ return true
9
+ end
8
10
  ctx.abort(ctx.message("core.tasks.ensure_project_type.wrong_project_type", project_type))
9
11
  end
10
12
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class CdnFonts
7
7
  FONTS_PATH = "/fonts"
8
8
  FONTS_CDN = "https://fonts.shopifycdn.com"
@@ -4,7 +4,7 @@ require "openssl"
4
4
 
5
5
  module ShopifyCLI
6
6
  module Theme
7
- module DevServer
7
+ class DevServer
8
8
  class CertificateManager
9
9
  attr_reader :ctx, :domain_name, :certificate, :private_key
10
10
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyCLI
3
+ module Theme
4
+ class DevServer
5
+ # Errors
6
+ class Error < StandardError; end
7
+ end
8
+ end
9
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  # Based on Rack::HeaderHash
7
7
  class HeaderHash < Hash
8
8
  def self.[](headers)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../hot_reload/remote_file_reloader"
3
+ require_relative "../hot_reload/remote_file_deleter"
4
+ require "shopify_cli/theme/ignore_helper"
5
+
6
+ module ShopifyCLI
7
+ module Theme
8
+ class DevServer
9
+ module Hooks
10
+ class FileChangeHook
11
+ include ShopifyCLI::Theme::IgnoreHelper
12
+
13
+ attr_reader :include_filter, :ignore_filter
14
+
15
+ def initialize(ctx, theme:, include_filter: nil, ignore_filter: nil)
16
+ @ctx = ctx
17
+ @theme = theme
18
+ @include_filter = include_filter
19
+ @ignore_filter = ignore_filter
20
+ end
21
+
22
+ def call(modified, added, removed, streams: nil)
23
+ @streams = streams
24
+ files = (modified + added)
25
+ .map { |f| @theme[f] }
26
+ .reject { |f| ignore_file?(f) }
27
+ files -= liquid_css_files = files.select(&:liquid_css?)
28
+ deleted_files = removed
29
+ .map { |f| @theme[f] }
30
+ .reject { |f| ignore_file?(f) }
31
+
32
+ remote_delete(deleted_files) unless deleted_files.empty?
33
+ reload_page(removed) unless deleted_files.empty?
34
+
35
+ hot_reload(files) unless files.empty?
36
+ remote_reload(liquid_css_files)
37
+ end
38
+
39
+ private
40
+
41
+ def hot_reload(files)
42
+ paths = files.map(&:relative_path)
43
+ @streams.broadcast(JSON.generate(modified: paths))
44
+ @ctx.debug("[HotReload] Modified #{paths.join(", ")}")
45
+ end
46
+
47
+ def reload_page(removed)
48
+ @streams.broadcast(JSON.generate(reload_page: true))
49
+ @ctx.debug("[ReloadPage] Deleted #{removed.join(", ")}")
50
+ end
51
+
52
+ def remote_reload(files)
53
+ files.each do |file|
54
+ @ctx.debug("reload file each -> file.relative_path #{file.relative_path}")
55
+ remote_file_reloader.reload(file)
56
+ end
57
+ end
58
+
59
+ def remote_delete(files)
60
+ files.each do |file|
61
+ @ctx.debug("delete file each -> file.relative_path #{file.relative_path}")
62
+ remote_file_deleter.delete(file)
63
+ end
64
+ end
65
+
66
+ def remote_file_deleter
67
+ @remote_file_deleter ||= HotReload::RemoteFileDeleter.new(@ctx, theme: @theme, streams: @streams)
68
+ end
69
+
70
+ def remote_file_reloader
71
+ @remote_file_reloader ||= HotReload::RemoteFileReloader.new(@ctx, theme: @theme, streams: @streams)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class HotReload
7
7
  class RemoteFileDeleter
8
8
  def initialize(ctx, theme:, streams:)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class HotReload
7
7
  class RemoteFileReloader
8
8
  def initialize(ctx, theme:, streams:)
@@ -0,0 +1,48 @@
1
+ class HotReload {
2
+ static reloadMode = () => {
3
+ const namespace = window.__SHOPIFY_CLI_ENV__;
4
+ return namespace.mode;
5
+ };
6
+ static isFullPageReloadMode = () => {
7
+ return HotReload.reloadMode() === "full-page";
8
+ };
9
+ static isReloadModeActive = () => {
10
+ return HotReload.reloadMode() !== "off";
11
+ };
12
+ static setHotReloadCookie = (files) => {
13
+ const date = new Date();
14
+
15
+ // Hot reload cookie expires in 3 seconds
16
+ date.setSeconds(date.getSeconds() + 3);
17
+
18
+ const sections = files.join(",");
19
+ const expires = date.toUTCString();
20
+
21
+ document.cookie = `hot_reload_files=${sections}; expires=${expires}; path=/`;
22
+ };
23
+ static refreshPage = (files) => {
24
+ HotReload.setHotReloadCookie(files);
25
+ console.log("[HotReload] Refreshing entire page");
26
+ window.location.reload();
27
+ };
28
+ static isCSSFile = (filename) => {
29
+ return filename.endsWith(".css");
30
+ };
31
+ static reloadCssFile = (filename) => {
32
+ // Find a stylesheet link starting with /assets (locally-served only) containing the filename
33
+ let links = document.querySelectorAll(
34
+ `link[href^="/assets"][href*="${filename}"][rel="stylesheet"]`
35
+ );
36
+
37
+ Array.from(links).forEach((link) => {
38
+ if (!link) {
39
+ console.log(
40
+ `[HotReload] Could not find link for stylesheet ${filename}`
41
+ );
42
+ } else {
43
+ link.href = new URL(link.href).pathname + `?v=${Date.now()}`;
44
+ console.log(`[HotReload] Reloaded stylesheet ${filename}`);
45
+ }
46
+ });
47
+ };
48
+ }
@@ -0,0 +1,43 @@
1
+ class SSEClient {
2
+ constructor(eventsUrl, eventHandler) {
3
+ SSEClient.verifySSE();
4
+ this.eventsUrl = eventsUrl;
5
+ this.eventHandler = eventHandler;
6
+ }
7
+ static verifySSE() {
8
+ if (typeof EventSource === "undefined") {
9
+ console.error(
10
+ "[HotReload] Error: SSE features are not supported. Try a different browser."
11
+ );
12
+ }
13
+
14
+ console.log("[HotReload] Initializing...");
15
+ }
16
+ connect() {
17
+ const eventSource = new EventSource(this.eventsUrl);
18
+ eventSource.onmessage = (msg) => {
19
+ this.handleMessage(msg);
20
+ };
21
+
22
+ eventSource.onopen = () => console.log("[HotReload] SSE connected.");
23
+
24
+ eventSource.onclose = () => {
25
+ console.log("[HotReload] SSE closed. Attempting to reconnect...");
26
+
27
+ setTimeout(this.connect, 5000);
28
+ };
29
+
30
+ eventSource.onerror = () => {
31
+ console.log("[HotReload] SSE closed.");
32
+ eventSource.close();
33
+ };
34
+ }
35
+ handleMessage(message) {
36
+ const data = JSON.parse(message.data);
37
+ if (data.reload_page) {
38
+ HotReload.refreshPage([]);
39
+ return;
40
+ }
41
+ this.eventHandler(data);
42
+ }
43
+ }
@@ -0,0 +1,114 @@
1
+ (() => {
2
+ function sectionNamesByType(type) {
3
+ const namespace = window.__SHOPIFY_CLI_ENV__;
4
+ return namespace.section_names_by_type[type] || [];
5
+ }
6
+
7
+ function querySelectDOMSections(idSuffix) {
8
+ const elements = document.querySelectorAll(
9
+ `[id^='shopify-section'][id$='${idSuffix}']`
10
+ );
11
+ return Array.from(elements);
12
+ }
13
+
14
+ function fetchDOMSections(name) {
15
+ const domSections = sectionNamesByType(name).flatMap((n) =>
16
+ querySelectDOMSections(n)
17
+ );
18
+
19
+ if (domSections.length > 0) {
20
+ return domSections;
21
+ }
22
+
23
+ return querySelectDOMSections(name);
24
+ }
25
+
26
+ function isRefreshRequired(files) {
27
+ if (HotReload.isFullPageReloadMode()) {
28
+ return true;
29
+ }
30
+ return files.some(
31
+ (file) => !HotReload.isCSSFile(file) && !isSectionFile(file)
32
+ );
33
+ }
34
+
35
+ function refreshFile(file) {
36
+ if (HotReload.isCSSFile(file)) {
37
+ HotReload.reloadCssFile(file);
38
+ return;
39
+ }
40
+
41
+ if (isSectionFile(file)) {
42
+ reloadSection(file);
43
+ }
44
+ }
45
+
46
+ function handleUpdate(data) {
47
+ const modifiedFiles = data.modified;
48
+
49
+ if (modifiedFiles === undefined) {
50
+ return;
51
+ }
52
+
53
+ if (isRefreshRequired(modifiedFiles)) {
54
+ HotReload.refreshPage(modifiedFiles);
55
+ } else {
56
+ modifiedFiles.forEach(refreshFile);
57
+ }
58
+ }
59
+
60
+ function isSectionFile(filename) {
61
+ return new Section(filename).valid();
62
+ }
63
+
64
+ function reloadSection(filename) {
65
+ new Section(filename).refresh();
66
+ }
67
+
68
+ class Section {
69
+ constructor(filename) {
70
+ this.filename = filename;
71
+ this.name = filename.split("/").pop().replace(".liquid", "");
72
+ this.elements = fetchDOMSections(this.name);
73
+ }
74
+
75
+ valid() {
76
+ return this.filename.startsWith("sections/") && this.elements.length > 0;
77
+ }
78
+
79
+ async refreshElement(element) {
80
+ const sectionId = element.id.replace(/^shopify-section-/, "");
81
+ const url = new URL(window.location.href);
82
+
83
+ url.searchParams.append("section_id", sectionId);
84
+
85
+ const response = await fetch(url);
86
+
87
+ try {
88
+ if (response.headers.get("x-templates-from-params") === "1") {
89
+ element.outerHTML = await response.text();
90
+ } else {
91
+ window.location.reload();
92
+
93
+ console.log(
94
+ `[HotReload] Hot-reloading not supported, fully reloading ${this.name} section`
95
+ );
96
+ }
97
+ } catch (e) {
98
+ console.log(
99
+ `[HotReload] Failed to reload ${this.name} section: ${e.message}`
100
+ );
101
+ }
102
+ }
103
+
104
+ async refresh() {
105
+ console.log(`[HotReload] Reloaded ${this.name} sections`);
106
+ this.elements.forEach(this.refreshElement);
107
+ }
108
+ }
109
+
110
+ if (HotReload.isReloadModeActive()) {
111
+ let client = new SSEClient("/hot-reload", handleUpdate);
112
+ client.connect();
113
+ }
114
+ })();