shopify-cli 2.24.0 → 2.25.0

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