shopify-cli 2.14.0 → 2.15.2

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +1 -1
  3. data/.github/workflows/stale.yml +46 -0
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +39 -7
  7. data/Rakefile +40 -0
  8. data/ext/shopify-extensions/version +1 -1
  9. data/lib/project_types/extension/forms/questions/ask_template.rb +5 -8
  10. data/lib/project_types/extension/messages/messages.rb +11 -1
  11. data/lib/project_types/extension/models/development_server_requirements.rb +13 -7
  12. data/lib/project_types/extension/models/server_config/root.rb +2 -0
  13. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +13 -0
  14. data/lib/project_types/script/config/extension_points.yml +18 -0
  15. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  16. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  17. data/lib/project_types/script/messages/messages.rb +3 -0
  18. data/lib/project_types/script/ui/error_handler.rb +11 -0
  19. data/lib/project_types/theme/commands/pull.rb +2 -2
  20. data/lib/project_types/theme/commands/push.rb +2 -2
  21. data/lib/project_types/theme/commands/serve.rb +1 -0
  22. data/lib/project_types/theme/conversions/base_glob.rb +20 -5
  23. data/lib/project_types/theme/messages/messages.rb +47 -8
  24. data/lib/shopify_cli/changelog.rb +76 -0
  25. data/lib/shopify_cli/command.rb +8 -7
  26. data/lib/shopify_cli/environment.rb +19 -11
  27. data/lib/shopify_cli/git.rb +36 -0
  28. data/lib/shopify_cli/messages/messages.rb +16 -9
  29. data/lib/shopify_cli/release.rb +194 -0
  30. data/lib/shopify_cli/sed.rb +19 -0
  31. data/lib/shopify_cli/services/app/create/node_service.rb +2 -14
  32. data/lib/shopify_cli/services/app/create/php_service.rb +1 -6
  33. data/lib/shopify_cli/services/app/create/rails_service.rb +4 -12
  34. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  35. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  36. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  37. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  38. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  39. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +34 -0
  40. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  41. data/lib/shopify_cli/theme/dev_server/watcher.rb +1 -1
  42. data/lib/shopify_cli/theme/dev_server.rb +15 -3
  43. data/lib/shopify_cli/theme/file.rb +15 -4
  44. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  45. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  46. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  47. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  48. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  49. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  50. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  51. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  52. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  53. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  54. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  55. data/lib/shopify_cli/theme/syncer.rb +79 -63
  56. data/lib/shopify_cli/theme/theme.rb +12 -4
  57. data/lib/shopify_cli/theme/theme_admin_api.rb +24 -23
  58. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  59. data/lib/shopify_cli/thread_pool.rb +15 -3
  60. data/lib/shopify_cli/tunnel.rb +9 -0
  61. data/lib/shopify_cli/version.rb +1 -1
  62. data/shopify-cli.gemspec +3 -1
  63. metadata +21 -4
  64. data/lib/project_types/rails/ruby.rb +0 -17
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module DevServer
6
+ class HotReload
7
+ class SectionsIndex
8
+ def initialize(theme)
9
+ @theme = theme
10
+ end
11
+
12
+ def section_names_by_type
13
+ index = {}
14
+
15
+ files.each do |file|
16
+ section_hash(file).each do |key, value|
17
+ name = key
18
+ type = value&.dig("type")
19
+
20
+ next if !name || !type
21
+
22
+ index[type] = [] unless index[type]
23
+ index[type] << name
24
+ end
25
+ end
26
+
27
+ index
28
+ end
29
+
30
+ private
31
+
32
+ def section_hash(file)
33
+ content = JSON.parse(file.read)
34
+ return [] unless content.is_a?(Hash)
35
+
36
+ sections = content["sections"]
37
+ return [] if sections.nil?
38
+
39
+ sections
40
+ rescue JSON::JSONError
41
+ []
42
+ end
43
+
44
+ def files
45
+ @theme.json_files
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "hot_reload/remote_file_reloader"
4
+ require_relative "hot_reload/sections_index"
4
5
 
5
6
  module ShopifyCLI
6
7
  module Theme
@@ -13,6 +14,7 @@ module ShopifyCLI
13
14
  @mode = mode
14
15
  @streams = SSE::Streams.new
15
16
  @remote_file_reloader = RemoteFileReloader.new(ctx, theme: @theme, streams: @streams)
17
+ @sections_index = SectionsIndex.new(@theme)
16
18
  @watcher = watcher
17
19
  @watcher.add_observer(self, :notify_streams_of_file_change)
18
20
  @ignore_filter = ignore_filter
@@ -78,7 +80,10 @@ module ShopifyCLI
78
80
  end
79
81
 
80
82
  def params_js
81
- env = { mode: @mode }
83
+ env = {
84
+ mode: @mode,
85
+ section_names_by_type: @sections_index.section_names_by_type,
86
+ }
82
87
  <<~JS
83
88
  (() => {
84
89
  window.__SHOPIFY_CLI_ENV__ = #{env.to_json};
@@ -71,7 +71,7 @@ module ShopifyCLI
71
71
 
72
72
  def replace_asset_urls(body)
73
73
  replaced_body = body.join.gsub(ASSET_REGEX) do |match|
74
- path = Pathname.new(Regexp.last_match[1])
74
+ path = Regexp.last_match[1]
75
75
  if @theme.static_asset_paths.include?(path)
76
76
  "/#{path}"
77
77
  else
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool/job"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ module DevServer
8
+ class RemoteWatcher
9
+ class JsonFilesUpdateJob < ShopifyCLI::ThreadPool::Job
10
+ def initialize(theme, syncer, interval)
11
+ super(interval)
12
+
13
+ @theme = theme
14
+ @syncer = syncer
15
+ end
16
+
17
+ def perform!
18
+ @syncer.fetch_checksums!
19
+ @syncer.enqueue_get(json_files)
20
+ end
21
+
22
+ private
23
+
24
+ def json_files
25
+ @theme
26
+ .json_files
27
+ .reject { |file| @syncer.pending_updates.include?(file) }
28
+ .reject { |file| @syncer.broken_file?(file) }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool"
4
+
5
+ require_relative "remote_watcher/json_files_update_job"
6
+
7
+ module ShopifyCLI
8
+ module Theme
9
+ module DevServer
10
+ class RemoteWatcher
11
+ SYNC_INTERVAL = 3 # seconds
12
+
13
+ class << self
14
+ def to(theme:, syncer:)
15
+ new(theme, syncer)
16
+ end
17
+ end
18
+
19
+ def start
20
+ thread_pool.schedule(recurring_job)
21
+ end
22
+
23
+ def stop
24
+ thread_pool.shutdown
25
+ end
26
+
27
+ private
28
+
29
+ def initialize(theme, syncer)
30
+ @theme = theme
31
+ @syncer = syncer
32
+ end
33
+
34
+ def thread_pool
35
+ @thread_pool ||= ShopifyCLI::ThreadPool.new(pool_size: 1)
36
+ end
37
+
38
+ def recurring_job
39
+ JsonFilesUpdateJob.new(@theme, @syncer, SYNC_INTERVAL)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -59,7 +59,7 @@ module ShopifyCLI
59
59
  private
60
60
 
61
61
  def ignore_file?(file)
62
- @ignore_filter&.ignore?(file.relative_path.to_s)
62
+ @ignore_filter&.ignore?(file.relative_path)
63
63
  end
64
64
  end
65
65
  end
@@ -11,6 +11,7 @@ require_relative "dev_server/local_assets"
11
11
  require_relative "dev_server/proxy"
12
12
  require_relative "dev_server/sse"
13
13
  require_relative "dev_server/watcher"
14
+ require_relative "dev_server/remote_watcher"
14
15
  require_relative "dev_server/web_server"
15
16
  require_relative "dev_server/certificate_manager"
16
17
 
@@ -26,12 +27,13 @@ module ShopifyCLI
26
27
  class << self
27
28
  attr_accessor :ctx
28
29
 
29
- def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false, mode: ReloadMode.default)
30
+ def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false, editor_sync: false, mode: ReloadMode.default)
30
31
  @ctx = ctx
31
32
  theme = DevelopmentTheme.find_or_create!(ctx, root: root)
32
33
  ignore_filter = IgnoreFilter.from_path(root)
33
- @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter)
34
+ @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter, overwrite_json: !editor_sync)
34
35
  watcher = Watcher.new(ctx, theme: theme, syncer: @syncer, ignore_filter: ignore_filter, poll: poll)
36
+ remote_watcher = RemoteWatcher.to(theme: theme, syncer: @syncer)
35
37
 
36
38
  # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
37
39
  @app = Proxy.new(ctx, theme: theme, syncer: @syncer)
@@ -57,9 +59,17 @@ module ShopifyCLI
57
59
 
58
60
  return if stopped
59
61
 
62
+ preview_suffix = editor_sync ? "" : ctx.message("theme.serve.download_changes")
63
+ preview_message = ctx.message(
64
+ "theme.serve.customize_or_preview",
65
+ preview_suffix,
66
+ theme.editor_url,
67
+ theme.preview_url
68
+ )
69
+
60
70
  ctx.puts(ctx.message("theme.serve.serving", theme.root))
61
71
  ctx.open_url!(address)
62
- ctx.puts(ctx.message("theme.serve.customize_or_preview", theme.editor_url, theme.preview_url))
72
+ ctx.puts(preview_message)
63
73
  end
64
74
 
65
75
  logger = if ctx.debug?
@@ -69,6 +79,7 @@ module ShopifyCLI
69
79
  end
70
80
 
71
81
  watcher.start
82
+ remote_watcher.start if editor_sync
72
83
  WebServer.run(
73
84
  @app,
74
85
  BindAddress: host,
@@ -76,6 +87,7 @@ module ShopifyCLI
76
87
  Logger: logger,
77
88
  AccessLog: [],
78
89
  )
90
+ remote_watcher.stop if editor_sync
79
91
  watcher.stop
80
92
 
81
93
  rescue ShopifyCLI::API::APIRequestForbiddenError,
@@ -4,7 +4,6 @@ require_relative "mime_type"
4
4
  module ShopifyCLI
5
5
  module Theme
6
6
  class File < Struct.new(:path)
7
- attr_reader :relative_path
8
7
  attr_accessor :remote_checksum
9
8
 
10
9
  def initialize(path, root)
@@ -42,7 +41,7 @@ module ShopifyCLI
42
41
  end
43
42
 
44
43
  def mime_type
45
- @mime_type ||= MimeType.by_filename(relative_path)
44
+ @mime_type ||= MimeType.by_filename(@relative_path)
46
45
  end
47
46
 
48
47
  def text?
@@ -54,7 +53,7 @@ module ShopifyCLI
54
53
  end
55
54
 
56
55
  def liquid_css?
57
- relative_path.to_s.end_with?(".css.liquid")
56
+ relative_path.end_with?(".css.liquid")
58
57
  end
59
58
 
60
59
  def json?
@@ -62,7 +61,7 @@ module ShopifyCLI
62
61
  end
63
62
 
64
63
  def template?
65
- relative_path.to_s.start_with?("templates/")
64
+ relative_path.start_with?("templates/")
66
65
  end
67
66
 
68
67
  def checksum
@@ -84,6 +83,18 @@ module ShopifyCLI
84
83
  relative_path == other.relative_path
85
84
  end
86
85
 
86
+ def name(*args)
87
+ ::File.basename(path, *args)
88
+ end
89
+
90
+ def absolute_path
91
+ path.realpath.to_s
92
+ end
93
+
94
+ def relative_path
95
+ @relative_path.to_s
96
+ end
97
+
87
98
  private
88
99
 
89
100
  def normalize_json(content)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ class Checksums
7
+ def initialize(theme)
8
+ @theme = theme
9
+ @checksum_by_key = {}
10
+
11
+ # Mutex used to coordinate changes in the checksums (shared accross `Syncer` threads)
12
+ @checksums_mutex = Mutex.new
13
+ end
14
+
15
+ def has?(file)
16
+ checksum_by_key.key?(to_key(file))
17
+ end
18
+
19
+ def file_has_changed?(file)
20
+ file.checksum != checksum_by_key[file.relative_path]
21
+ end
22
+
23
+ def keys
24
+ checksum_by_key.keys
25
+ end
26
+
27
+ def [](key)
28
+ checksum_by_key[key]
29
+ end
30
+
31
+ def []=(key, value)
32
+ checksums_mutex.synchronize do
33
+ checksum_by_key[key] = value
34
+ end
35
+ end
36
+
37
+ # Generate .liquid asset files are reported twice in checksum:
38
+ # once of generated, once for .liquid. We only keep the .liquid, that's the one we have
39
+ # on disk.
40
+ def reject_duplicated_checksums!
41
+ checksums_mutex.synchronize do
42
+ checksum_by_key.reject! { |key, _| checksum_by_key.key?("#{key}.liquid") }
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def to_key(file)
49
+ theme[file].relative_path
50
+ end
51
+
52
+ # Private getters only used in unit tests
53
+
54
+ attr_reader :checksum_by_key
55
+ attr_reader :theme
56
+ attr_reader :checksums_mutex
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "apply_to_all_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ module Forms
9
+ class ApplyToAll
10
+ attr_reader :value
11
+
12
+ def initialize(ctx, number_of_files)
13
+ @ctx = ctx
14
+ @number_of_files = number_of_files
15
+ @value = nil
16
+ @apply = nil
17
+ end
18
+
19
+ def apply?(value)
20
+ return unless @number_of_files > 1
21
+
22
+ if @apply.nil?
23
+ @apply = ask.apply?
24
+ @value = value if @apply
25
+ end
26
+
27
+ @apply
28
+ end
29
+
30
+ private
31
+
32
+ def ask
33
+ ApplyToAllForm.ask(@ctx, [], number_of_files: @number_of_files)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ module Forms
7
+ class ApplyToAllForm < ShopifyCLI::Form
8
+ attr_accessor :apply
9
+ flag_arguments :number_of_files
10
+
11
+ def ask
12
+ title = message("title", number_of_files - 1)
13
+
14
+ self.apply = CLI::UI::Prompt.ask(title, allow_empty: false) do |handler|
15
+ handler.option(message("yes")) { true }
16
+ handler.option(message("no")) { false }
17
+ end
18
+
19
+ self
20
+ end
21
+
22
+ def apply?
23
+ apply
24
+ end
25
+
26
+ private
27
+
28
+ def message(key, *params)
29
+ ctx.message("theme.serve.syncer.forms.apply_to_all.#{key}", *params)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ module Forms
7
+ class BaseStrategyForm < ShopifyCLI::Form
8
+ attr_accessor :strategy
9
+
10
+ def ask
11
+ ctx.puts(title_context(file))
12
+
13
+ self.strategy = CLI::UI::Prompt.ask(title_question, allow_empty: false) do |handler|
14
+ strategies.each do |strategy|
15
+ handler.option(as_text(strategy)) { strategy }
16
+ end
17
+ end
18
+
19
+ exit_cli if self.strategy == :exit
20
+
21
+ self
22
+ end
23
+
24
+ protected
25
+
26
+ ##
27
+ # List of strategies that populate the form options
28
+ #
29
+ def strategies
30
+ raise "`#{self.class.name}#strategies' must be defined"
31
+ end
32
+
33
+ ##
34
+ # Message prefix for the form title and options (strategies).
35
+ # See the methods `title` and `as_text`
36
+ #
37
+ def prefix
38
+ raise "`#{self.class.name}#prefix' must be defined"
39
+ end
40
+
41
+ private
42
+
43
+ def exit_cli
44
+ exit(0)
45
+ end
46
+
47
+ def title_context(file)
48
+ ctx.message("#{prefix}.title_context", file.relative_path)
49
+ end
50
+
51
+ def title_question
52
+ ctx.message("#{prefix}.title_question")
53
+ end
54
+
55
+ def as_text(strategy)
56
+ ctx.message("#{prefix}.#{strategy}")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ module Forms
9
+ class SelectDeleteStrategy < BaseStrategyForm
10
+ flag_arguments :file
11
+
12
+ def strategies
13
+ %i[
14
+ delete
15
+ restore
16
+ exit
17
+ ]
18
+ end
19
+
20
+ def prefix
21
+ "theme.serve.syncer.forms.delete_strategy"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ module Forms
9
+ class SelectUpdateStrategy < BaseStrategyForm
10
+ flag_arguments :file
11
+
12
+ def strategies
13
+ %i[
14
+ keep_remote
15
+ keep_local
16
+ union_merge
17
+ exit
18
+ ]
19
+ end
20
+
21
+ def prefix
22
+ "theme.serve.syncer.forms.update_strategy"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ module IgnoreHelper
7
+ def ignore_operation?(operation)
8
+ path = operation.file_path
9
+ ignore_path?(path)
10
+ end
11
+
12
+ def ignore_file?(file)
13
+ path = file.path
14
+ ignore_path?(path)
15
+ end
16
+
17
+ def ignore_path?(path)
18
+ ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
19
+ end
20
+
21
+ private
22
+
23
+ def ignored_by_ignore_filter?(path)
24
+ ignore_filter&.ignore?(path)
25
+ end
26
+
27
+ def ignored_by_include_filter?(path)
28
+ !!include_filter && !include_filter.match?(path)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "forms/apply_to_all"
4
+ require_relative "forms/select_delete_strategy"
5
+
6
+ module ShopifyCLI
7
+ module Theme
8
+ class Syncer
9
+ module JsonDeleteHandler
10
+ def enqueue_json_deletes(files)
11
+ files = files.select { |file| !ignore_file?(file) }
12
+
13
+ if overwrite_json?
14
+ enqueue_deletes(files)
15
+ else
16
+ # Handle conflicts when JSON files cannot be overwritten
17
+ handle_delete_conflicts(files)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def handle_delete_conflicts(files)
24
+ to_delete = []
25
+ to_get = []
26
+
27
+ apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
28
+
29
+ files.each do |file|
30
+ delete_strategy = apply_to_all.value || ask_delete_strategy(file)
31
+ apply_to_all.apply?(delete_strategy)
32
+
33
+ case delete_strategy
34
+ when :delete
35
+ to_delete << file
36
+ when :restore
37
+ to_get << file
38
+ end
39
+ end
40
+
41
+ enqueue_deletes(to_delete)
42
+ enqueue_get(to_get)
43
+ end
44
+
45
+ def ask_delete_strategy(file)
46
+ Forms::SelectDeleteStrategy.ask(@ctx, [], file: file).strategy
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end