shopify-cli 2.24.0 → 2.25.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/lib/project_types/extension/commands/serve.rb +57 -3
  6. data/lib/project_types/extension/extension_project.rb +8 -1
  7. data/lib/project_types/extension/loaders/project.rb +3 -2
  8. data/lib/project_types/extension/messages/messages.rb +21 -6
  9. data/lib/project_types/extension/models/server_config/development_renderer.rb +1 -1
  10. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +18 -6
  11. data/lib/project_types/theme/commands/serve.rb +15 -3
  12. data/lib/project_types/theme/messages/messages.rb +4 -2
  13. data/lib/shopify_cli/commands/logout.rb +13 -2
  14. data/lib/shopify_cli/environment.rb +1 -1
  15. data/lib/shopify_cli/file_system_listener.rb +30 -0
  16. data/lib/shopify_cli/git.rb +116 -33
  17. data/lib/shopify_cli/identity_auth.rb +1 -0
  18. data/lib/shopify_cli/project.rb +1 -1
  19. data/lib/shopify_cli/tasks/ensure_project_type.rb +3 -1
  20. data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +1 -1
  21. data/lib/shopify_cli/theme/dev_server/certificate_manager.rb +1 -1
  22. data/lib/shopify_cli/theme/dev_server/errors.rb +9 -0
  23. data/lib/shopify_cli/theme/dev_server/header_hash.rb +1 -1
  24. data/lib/shopify_cli/theme/dev_server/hooks/file_change_hook.rb +77 -0
  25. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +1 -1
  26. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  27. data/lib/shopify_cli/theme/dev_server/{hot-reload-no-script.html → hot_reload/resources/hot-reload-no-script.html} +0 -0
  28. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/hot_reload.js +48 -0
  29. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/sse_client.js +43 -0
  30. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme.js +114 -0
  31. data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme_extension.js +121 -0
  32. data/lib/shopify_cli/theme/dev_server/hot_reload/script_injector.rb +57 -0
  33. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +1 -1
  34. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +8 -76
  35. data/lib/shopify_cli/theme/dev_server/local_assets.rb +28 -28
  36. data/lib/shopify_cli/theme/dev_server/proxy.rb +33 -25
  37. data/lib/shopify_cli/theme/dev_server/proxy_param_builder.rb +82 -0
  38. data/lib/shopify_cli/theme/dev_server/reload_mode.rb +1 -1
  39. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +1 -1
  40. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +1 -1
  41. data/lib/shopify_cli/theme/dev_server/sse.rb +1 -1
  42. data/lib/shopify_cli/theme/dev_server/watcher.rb +8 -9
  43. data/lib/shopify_cli/theme/dev_server/web_server.rb +1 -1
  44. data/lib/shopify_cli/theme/dev_server.rb +287 -99
  45. data/lib/shopify_cli/theme/extension/app_extension.rb +40 -0
  46. data/lib/shopify_cli/theme/extension/dev_server/hooks/file_change_hook.rb +68 -0
  47. data/lib/shopify_cli/theme/extension/dev_server/hot_reload/script_injector.rb +30 -0
  48. data/lib/shopify_cli/theme/extension/dev_server/hot_reload.rb +13 -0
  49. data/lib/shopify_cli/theme/extension/dev_server/local_assets.rb +30 -0
  50. data/lib/shopify_cli/theme/{dev_server/proxy/template_param_builder.rb → extension/dev_server/proxy_param_builder.rb} +26 -16
  51. data/lib/shopify_cli/theme/extension/dev_server/watcher.rb +47 -0
  52. data/lib/shopify_cli/theme/extension/dev_server.rb +150 -0
  53. data/lib/shopify_cli/theme/extension/host_theme.rb +104 -0
  54. data/lib/shopify_cli/theme/extension/syncer/extension_serve_job.rb +133 -0
  55. data/lib/shopify_cli/theme/extension/syncer/operation.rb +21 -0
  56. data/lib/shopify_cli/theme/extension/syncer.rb +81 -0
  57. data/lib/shopify_cli/theme/extension/ui/host_theme_progress_bar.rb +35 -0
  58. data/lib/shopify_cli/theme/ignore_helper.rb +31 -0
  59. data/lib/shopify_cli/theme/root.rb +62 -0
  60. data/lib/shopify_cli/theme/syncer.rb +12 -6
  61. data/lib/shopify_cli/theme/theme.rb +10 -52
  62. data/lib/shopify_cli/version.rb +1 -1
  63. metadata +27 -7
  64. data/.github/workflows/triage.yml +0 -22
  65. data/lib/shopify_cli/theme/dev_server/hot-reload.js +0 -194
  66. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +0 -33
@@ -2,9 +2,9 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class LocalAssets
7
- ASSET_REGEX = %r{//cdn\.shopify\.com/s/.+?/(assets/.+?\.(?:css|js))}
7
+ THEME_REGEX = %r{//cdn\.shopify\.com/s/.+?/(assets/.+?\.(?:css|js))}
8
8
 
9
9
  class FileBody
10
10
  def initialize(path)
@@ -22,10 +22,10 @@ module ShopifyCLI
22
22
  end
23
23
  end
24
24
 
25
- def initialize(ctx, app, theme:)
25
+ def initialize(ctx, app, target)
26
26
  @ctx = ctx
27
27
  @app = app
28
- @theme = theme
28
+ @target = target
29
29
  end
30
30
 
31
31
  def call(env)
@@ -42,23 +42,20 @@ module ShopifyCLI
42
42
 
43
43
  private
44
44
 
45
- def serve_file(path_info)
46
- path = @theme.root.join(path_info[1..-1])
47
- if path.file? && path.readable? && @theme.static_asset_file?(path)
48
- [
49
- 200,
50
- {
51
- "Content-Type" => MimeType.by_filename(path).to_s,
52
- "Content-Length" => path.size.to_s,
53
- },
54
- FileBody.new(path),
55
- ]
56
- else
57
- fail(404, "Not found")
45
+ def replace_asset_urls(body)
46
+ replaced_body = body.join.gsub(THEME_REGEX) do |match|
47
+ path = Regexp.last_match[1]
48
+ if @target.static_asset_paths.include?(path)
49
+ "/#{path}"
50
+ else
51
+ match
52
+ end
58
53
  end
54
+
55
+ [replaced_body]
59
56
  end
60
57
 
61
- def fail(status, body)
58
+ def serve_fail(status, body)
62
59
  [
63
60
  status,
64
61
  {
@@ -69,17 +66,20 @@ module ShopifyCLI
69
66
  ]
70
67
  end
71
68
 
72
- def replace_asset_urls(body)
73
- replaced_body = body.join.gsub(ASSET_REGEX) do |match|
74
- path = Regexp.last_match[1]
75
- if @theme.static_asset_paths.include?(path)
76
- "/#{path}"
77
- else
78
- match
79
- end
69
+ def serve_file(path_info)
70
+ path = @target.root.join(path_info[1..-1])
71
+ if path.file? && path.readable? && @target.static_asset_file?(path)
72
+ [
73
+ 200,
74
+ {
75
+ "Content-Type" => MimeType.by_filename(path).to_s,
76
+ "Content-Length" => path.size.to_s,
77
+ },
78
+ FileBody.new(path),
79
+ ]
80
+ else
81
+ serve_fail(404, "Not found")
80
82
  end
81
-
82
- [replaced_body]
83
83
  end
84
84
  end
85
85
  end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require "net/http"
3
2
  require "stringio"
4
3
  require "time"
5
4
  require "cgi"
6
- require_relative "proxy/template_param_builder"
5
+ require "net/http"
6
+
7
+ require_relative "header_hash"
8
+ require_relative "proxy_param_builder"
7
9
 
8
10
  module ShopifyCLI
9
11
  module Theme
10
- module DevServer
12
+ class DevServer
11
13
  HOP_BY_HOP_HEADERS = [
12
14
  "connection",
13
15
  "keep-alive",
@@ -25,27 +27,27 @@ module ShopifyCLI
25
27
  SESSION_COOKIE_REGEXP = /#{SESSION_COOKIE_NAME}=(\h+)/
26
28
  SESSION_COOKIE_MAX_AGE = 60 * 60 * 23 # 1 day - leeway of 1h
27
29
 
28
- def initialize(ctx, theme:, syncer:)
30
+ def initialize(ctx, theme, param_builder)
29
31
  @ctx = ctx
30
32
  @theme = theme
31
- @syncer = syncer
32
- @core_endpoints = Set.new
33
+ @param_builder = param_builder
33
34
 
35
+ @core_endpoints = Set.new
34
36
  @secure_session_id = nil
35
37
  @last_session_cookie_refresh = nil
36
38
  end
37
39
 
38
40
  def call(env)
39
41
  headers = extract_http_request_headers(env)
40
- headers["Host"] = @theme.shop
42
+ headers["Host"] = shop
41
43
  headers["Cookie"] = add_session_cookie(headers["Cookie"])
42
44
  headers["Accept-Encoding"] = "none"
43
45
  headers["User-Agent"] = "Shopify CLI"
44
-
45
46
  query = URI.decode_www_form(env["QUERY_STRING"])
46
- replace_templates = build_replace_templates_param(env)
47
+ replace_templates = build_replacement_param(env)
47
48
  response = if replace_templates.any?
48
- # Pass to SFR the recently modified templates in `replace_templates` body param
49
+ # Pass to SFR the recently modified templates in `replace_templates` or
50
+ # `replace_extension_templates` body param
49
51
  headers["Authorization"] = "Bearer #{bearer_token}"
50
52
  form_data = URI.decode_www_form(env["rack.input"].read).to_h
51
53
  request(
@@ -63,7 +65,7 @@ module ShopifyCLI
63
65
  )
64
66
  end
65
67
 
66
- headers = get_response_headers(response)
68
+ headers = get_response_headers(response, env)
67
69
 
68
70
  unless headers["x-storefront-renderer-rendered"]
69
71
  @core_endpoints << env["PATH_INFO"]
@@ -79,7 +81,7 @@ module ShopifyCLI
79
81
  def patch_body(env, body)
80
82
  return [""] unless body
81
83
 
82
- body.gsub(%r{(data-.+=(["']))(http:|https:)?//#{@theme.shop}(.*)(\2)}) do |_|
84
+ body.gsub(%r{(data-.+=(["']))(http:|https:)?//#{shop}(.*)(\2)}) do |_|
83
85
  match = Regexp.last_match
84
86
  "#{match[1]}http://#{host(env)}#{match[4]}#{match[5]}"
85
87
  end
@@ -127,15 +129,6 @@ module ShopifyCLI
127
129
  name.sub(/^HTTP_/, "").gsub("_", "-")
128
130
  end
129
131
 
130
- def build_replace_templates_param(env)
131
- TemplateParamBuilder.new
132
- .with_core_endpoints(@core_endpoints)
133
- .with_syncer(@syncer)
134
- .with_theme(@theme)
135
- .with_rack_env(env)
136
- .build
137
- end
138
-
139
132
  def add_session_cookie(cookie_header)
140
133
  cookie_header = if cookie_header
141
134
  cookie_header.dup
@@ -170,7 +163,7 @@ module ShopifyCLI
170
163
  def secure_session_id
171
164
  if secure_session_id_expired?
172
165
  @ctx.debug("Refreshing preview _secure_session_id cookie")
173
- response = request("HEAD", "/", query: [[:preview_theme_id, @theme.id]])
166
+ response = request("HEAD", "/", query: [[:preview_theme_id, theme_id]])
174
167
  @secure_session_id = extract_secure_session_id_from_response_headers(response)
175
168
  @last_session_cookie_refresh = Time.now
176
169
  end
@@ -178,7 +171,7 @@ module ShopifyCLI
178
171
  @secure_session_id
179
172
  end
180
173
 
181
- def get_response_headers(response)
174
+ def get_response_headers(response, env)
182
175
  response_headers = normalize_headers(
183
176
  response.respond_to?(:headers) ? response.headers : response.to_hash
184
177
  )
@@ -188,7 +181,7 @@ module ShopifyCLI
188
181
  response_headers.reject! { |k| HOP_BY_HOP_HEADERS.include?(k.downcase) }
189
182
 
190
183
  if response_headers["location"]&.include?("myshopify.com")
191
- response_headers["location"].gsub!(%r{(https://#{@theme.shop})}, "http://127.0.0.1:9292")
184
+ response_headers["location"].gsub!(%r{(https://#{shop})}, "http://#{host(env)}")
192
185
  end
193
186
 
194
187
  new_session_id = extract_secure_session_id_from_response_headers(response_headers)
@@ -202,7 +195,7 @@ module ShopifyCLI
202
195
  end
203
196
 
204
197
  def request(method, path, headers: nil, query: [], form_data: nil, body_stream: nil)
205
- uri = URI.join("https://#{@theme.shop}", path)
198
+ uri = URI.join("https://#{shop}", path)
206
199
  uri.query = URI.encode_www_form(query + [[:_fd, 0], [:pb, 0]])
207
200
 
208
201
  @ctx.debug("Proxying #{method} #{uri}")
@@ -218,6 +211,21 @@ module ShopifyCLI
218
211
  response
219
212
  end
220
213
  end
214
+
215
+ def shop
216
+ @shop ||= @theme.shop
217
+ end
218
+
219
+ def theme_id
220
+ @theme_id ||= @theme.id
221
+ end
222
+
223
+ def build_replacement_param(env)
224
+ @param_builder
225
+ .with_core_endpoints(@core_endpoints)
226
+ .with_rack_env(env)
227
+ .build
228
+ end
221
229
  end
222
230
  end
223
231
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class DevServer
8
+ class ProxyParamBuilder
9
+ def build
10
+ # Core doesn't support replace_templates
11
+ return {} if core?(current_path)
12
+
13
+ (syncer_templates + request_templates)
14
+ .select { |file| file.liquid? || file.json? }
15
+ .uniq(&:relative_path)
16
+ .map { |file| as_param(file) }
17
+ .to_h
18
+ end
19
+
20
+ def with_core_endpoints(core_endpoints)
21
+ @core_endpoints = core_endpoints
22
+ self
23
+ end
24
+
25
+ def with_syncer(syncer)
26
+ @syncer = syncer
27
+ self
28
+ end
29
+
30
+ def with_rack_env(rack_env)
31
+ @rack_env = rack_env
32
+ self
33
+ end
34
+
35
+ def with_theme(theme)
36
+ @theme = theme
37
+ self
38
+ end
39
+
40
+ private
41
+
42
+ def as_param(file)
43
+ ["replace_templates[#{file.relative_path}]", file.read]
44
+ end
45
+
46
+ def syncer_templates
47
+ @syncer&.pending_updates || []
48
+ end
49
+
50
+ def request_templates
51
+ cookie_sections
52
+ .map { |section| @theme[section] unless @theme.nil? }
53
+ .compact
54
+ end
55
+
56
+ def cookie_sections
57
+ CGI::Cookie.parse(cookie)["hot_reload_files"].join.split(",") || []
58
+ end
59
+
60
+ def core?(path)
61
+ core_endpoints.include?(path)
62
+ end
63
+
64
+ def current_path
65
+ rack_env["PATH_INFO"]
66
+ end
67
+
68
+ def cookie
69
+ rack_env["HTTP_COOKIE"]
70
+ end
71
+
72
+ def core_endpoints
73
+ @core_endpoints || []
74
+ end
75
+
76
+ def rack_env
77
+ @rack_env || {}
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyCLI
4
4
  module Theme
5
- module DevServer
5
+ class DevServer
6
6
  class ReloadMode
7
7
  MODES = [:"hot-reload", :"full-page", :off]
8
8
 
@@ -4,7 +4,7 @@ require "shopify_cli/thread_pool/job"
4
4
 
5
5
  module ShopifyCLI
6
6
  module Theme
7
- module DevServer
7
+ class DevServer
8
8
  class RemoteWatcher
9
9
  class JsonFilesUpdateJob < ShopifyCLI::ThreadPool::Job
10
10
  def initialize(theme, syncer, interval)
@@ -6,7 +6,7 @@ require_relative "remote_watcher/json_files_update_job"
6
6
 
7
7
  module ShopifyCLI
8
8
  module Theme
9
- module DevServer
9
+ class DevServer
10
10
  class RemoteWatcher
11
11
  SYNC_INTERVAL = 3 # seconds
12
12
 
@@ -3,7 +3,7 @@ require "thread"
3
3
 
4
4
  module ShopifyCLI
5
5
  module Theme
6
- module DevServer
6
+ class DevServer
7
7
  # Server-Sent events implementation for Rack.
8
8
  # Based on https://gist.github.com/raggi/ff7971991297e5c8a1ce
9
9
  class SSE
@@ -1,24 +1,23 @@
1
1
  # frozen_string_literal: true
2
- require "listen"
3
- require "observer"
2
+ require "shopify_cli/file_system_listener"
3
+ require "forwardable"
4
4
 
5
5
  module ShopifyCLI
6
6
  module Theme
7
- module DevServer
7
+ class DevServer
8
8
  # Watches for file changes and publish events to the theme
9
9
  class Watcher
10
- include Observable
10
+ extend Forwardable
11
+
12
+ def_delegators :@listener, :add_observer, :changed, :notify_observers
11
13
 
12
14
  def initialize(ctx, theme:, syncer:, ignore_filter: nil, poll: false)
13
15
  @ctx = ctx
14
16
  @theme = theme
15
17
  @syncer = syncer
16
18
  @ignore_filter = ignore_filter
17
- @listener = Listen.to(@theme.root, force_polling: poll,
18
- ignore: @ignore_filter&.regexes) do |modified, added, removed|
19
- changed
20
- notify_observers(modified, added, removed)
21
- end
19
+ @listener = FileSystemListener.new(root: @theme.root, force_poll: poll,
20
+ ignore_regex: @ignore_filter&.regexes)
22
21
 
23
22
  add_observer(self, :upload_files_when_changed)
24
23
  end
@@ -5,7 +5,7 @@ require "stringio"
5
5
 
6
6
  module ShopifyCLI
7
7
  module Theme
8
- module DevServer
8
+ class DevServer
9
9
  # WEBrick will sometimes cause a fatal deadlock error on shutdown.
10
10
  # The error happens because `Thread#join` is called without a timeout argument.
11
11
  # We monkey-patch WEBrick to call `Thread#join(timeout)` before the existing