shopify-cli 2.15.1 → 2.15.4

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.vscode/settings.json +1 -2
  3. data/CHANGELOG.md +68 -20
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +21 -0
  6. data/ext/javy/hashes/javy-arm-macos-v0.3.0.gz.sha256 +1 -0
  7. data/ext/javy/hashes/javy-x86_64-linux-v0.3.0.gz.sha256 +1 -0
  8. data/ext/javy/hashes/javy-x86_64-macos-v0.3.0.gz.sha256 +1 -0
  9. data/ext/javy/hashes/javy-x86_64-windows-v0.3.0.gz.sha256 +1 -0
  10. data/ext/javy/version +1 -1
  11. data/ext/shopify-extensions/version +1 -1
  12. data/lib/project_types/extension/cli.rb +4 -0
  13. data/lib/project_types/extension/commands/check.rb +6 -1
  14. data/lib/project_types/extension/forms/questions/ask_template.rb +1 -2
  15. data/lib/project_types/extension/messages/messages.rb +1 -3
  16. data/lib/project_types/extension/models/development_server_requirements.rb +1 -0
  17. data/lib/project_types/extension/models/specification_handlers/beacon_extension.rb +57 -0
  18. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config.rb +33 -0
  19. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config_repository.rb +75 -0
  20. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +16 -1
  21. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +4 -1
  22. data/lib/project_types/extension/tasks/configure_options.rb +2 -1
  23. data/lib/project_types/extension/tasks/convert_server_config.rb +13 -2
  24. data/lib/project_types/extension/tasks/merge_server_config.rb +5 -2
  25. data/lib/project_types/script/cli.rb +1 -0
  26. data/lib/project_types/script/layers/application/create_script.rb +14 -6
  27. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  28. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +6 -21
  29. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  30. data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +35 -0
  31. data/lib/project_types/script/messages/messages.rb +3 -0
  32. data/lib/project_types/script/ui/error_handler.rb +11 -0
  33. data/lib/project_types/theme/cli.rb +1 -0
  34. data/lib/project_types/theme/commands/check.rb +4 -1
  35. data/lib/project_types/theme/commands/open.rb +2 -2
  36. data/lib/project_types/theme/commands/push.rb +1 -3
  37. data/lib/project_types/theme/commands/serve.rb +1 -0
  38. data/lib/project_types/theme/commands/share.rb +56 -0
  39. data/lib/project_types/theme/messages/messages.rb +64 -11
  40. data/lib/shopify_cli/changelog.rb +97 -25
  41. data/lib/shopify_cli/command_options/command_serve_options.rb +10 -0
  42. data/lib/shopify_cli/commands/app/serve.rb +7 -7
  43. data/lib/shopify_cli/commands/login.rb +5 -2
  44. data/lib/shopify_cli/context.rb +13 -0
  45. data/lib/shopify_cli/git.rb +36 -0
  46. data/lib/shopify_cli/identity_auth.rb +24 -4
  47. data/lib/shopify_cli/messages/messages.rb +22 -11
  48. data/lib/shopify_cli/release.rb +120 -20
  49. data/lib/shopify_cli/services/app/create/rails_service.rb +9 -1
  50. data/lib/shopify_cli/services/app/serve/node_service.rb +2 -25
  51. data/lib/shopify_cli/services/app/serve/php_service.rb +2 -25
  52. data/lib/shopify_cli/services/app/serve/rails_service.rb +8 -28
  53. data/lib/shopify_cli/services/app/serve/serve_service.rb +57 -0
  54. data/lib/shopify_cli/services.rb +1 -0
  55. data/lib/shopify_cli/tasks/update_dashboard_urls.rb +7 -9
  56. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  57. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  58. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  59. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  60. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  61. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +35 -0
  62. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  63. data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -8
  64. data/lib/shopify_cli/theme/dev_server.rb +18 -5
  65. data/lib/shopify_cli/theme/file.rb +15 -4
  66. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  67. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  68. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  69. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  70. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  71. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  72. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  73. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  74. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  75. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  76. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  77. data/lib/shopify_cli/theme/syncer.rb +79 -63
  78. data/lib/shopify_cli/theme/theme.rb +21 -7
  79. data/lib/shopify_cli/theme/theme_admin_api.rb +23 -8
  80. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  81. data/lib/shopify_cli/thread_pool.rb +15 -3
  82. data/lib/shopify_cli/tunnel.rb +3 -13
  83. data/lib/shopify_cli/version.rb +1 -1
  84. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +8 -0
  85. metadata +25 -2
@@ -0,0 +1,57 @@
1
+ module ShopifyCLI
2
+ module Services
3
+ module App
4
+ module Serve
5
+ class ServeService < BaseService
6
+ attr_accessor :host, :port, :no_update, :context
7
+
8
+ def initialize(host:, port:, no_update:, context:)
9
+ @host = host
10
+ @port = port
11
+ @no_update = no_update
12
+ @context = context
13
+ super()
14
+ end
15
+
16
+ def call
17
+ raise NotImplementedError
18
+ end
19
+
20
+ private
21
+
22
+ def generate_url
23
+ create_tunnel
24
+ update_url unless no_update
25
+ show_app_url
26
+ end
27
+
28
+ def create_tunnel
29
+ url = host || ShopifyCLI::Tunnel.start(context, port: port)
30
+ raise ShopifyCLI::Abort,
31
+ context.message("core.app.serve.error.host_must_be_https") if url.match(/^https/i).nil?
32
+ project.env.update(context, :host, url)
33
+ end
34
+
35
+ def update_url
36
+ ShopifyCLI::Tasks::UpdateDashboardURLS.call(
37
+ context,
38
+ url: project.env.host,
39
+ callback_url: "/auth/shopify/callback",
40
+ )
41
+ end
42
+
43
+ def show_app_url
44
+ return unless project.env.shop
45
+
46
+ project_url = "#{project.env.host}/login?shop=#{project.env.shop}"
47
+ context.puts("\n" + context.message("core.app.serve.open_info", project_url) + "\n")
48
+ end
49
+
50
+ def project
51
+ @project ||= ShopifyCLI::Project.current
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -5,6 +5,7 @@ module ShopifyCLI
5
5
 
6
6
  module App
7
7
  module Serve
8
+ autoload :ServeService, "shopify_cli/services/app/serve/serve_service"
8
9
  autoload :NodeService, "shopify_cli/services/app/serve/node_service"
9
10
  autoload :RailsService, "shopify_cli/services/app/serve/rails_service"
10
11
  autoload :PHPService, "shopify_cli/services/app/serve/php_service"
@@ -9,24 +9,22 @@ module ShopifyCLI
9
9
  api_key = project.env.api_key
10
10
  result = ShopifyCLI::PartnersAPI.query(ctx, "get_app_urls", apiKey: api_key)
11
11
  app = result["data"]["app"]
12
- consent = check_application_url(app["applicationUrl"], url)
12
+
13
+ return if app["applicationUrl"].match(url)
14
+
13
15
  constructed_urls = construct_redirect_urls(app["redirectUrlWhitelist"], url, callback_url)
14
- return if url == app["applicationUrl"]
15
16
  ShopifyCLI::PartnersAPI.query(@ctx, "update_dashboard_urls", input: {
16
- applicationUrl: consent ? url : app["applicationUrl"],
17
- redirectUrlWhitelist: constructed_urls, apiKey: api_key
17
+ applicationUrl: url,
18
+ redirectUrlWhitelist: constructed_urls,
19
+ apiKey: api_key,
18
20
  })
21
+
19
22
  @ctx.puts(@ctx.message("core.tasks.update_dashboard_urls.updated"))
20
23
  rescue
21
24
  @ctx.puts(@ctx.message("core.tasks.update_dashboard_urls.update_error", ShopifyCLI::TOOL_NAME))
22
25
  raise
23
26
  end
24
27
 
25
- def check_application_url(application_url, new_url)
26
- return false if application_url.match(new_url)
27
- CLI::UI::Prompt.confirm(@ctx.message("core.tasks.update_dashboard_urls.update_prompt"))
28
- end
29
-
30
28
  def construct_redirect_urls(urls, new_url, callback_url)
31
29
  new_urls = urls.map do |url|
32
30
  if (match = url.match(NGROK_REGEX))
@@ -15,17 +15,37 @@
15
15
  eventSource.onerror = () => eventSource.close();
16
16
  }
17
17
 
18
+ function sectionNamesByType(type) {
19
+ const namespace = window.__SHOPIFY_CLI_ENV__;
20
+ return namespace.section_names_by_type[type] || [];
21
+ }
22
+
18
23
  function reloadMode() {
19
- var namespace = window.__SHOPIFY_CLI_ENV__;
24
+ const namespace = window.__SHOPIFY_CLI_ENV__;
20
25
  return namespace.mode;
21
26
  }
22
27
 
28
+ function querySelectDOMSections(idSuffix) {
29
+ const elements = document.querySelectorAll(`[id^='shopify-section'][id$='${idSuffix}']`);
30
+ return Array.from(elements);
31
+ }
32
+
33
+ function fetchDOMSections(name) {
34
+ const domSections = sectionNamesByType(name).flatMap((n) => querySelectDOMSections(n));
35
+
36
+ if (domSections.length > 0) {
37
+ return domSections;
38
+ }
39
+
40
+ return querySelectDOMSections(name);
41
+ }
42
+
23
43
  function isFullPageReloadMode(){
24
- return reloadMode() === "full-page";
44
+ return reloadMode() === 'full-page';
25
45
  }
26
46
 
27
47
  function isReloadModeActive(){
28
- return reloadMode() !== "off";
48
+ return reloadMode() !== 'off';
29
49
  }
30
50
 
31
51
  function isRefreshRequired(files) {
@@ -104,26 +124,28 @@
104
124
  constructor(filename) {
105
125
  this.filename = filename;
106
126
  this.name = filename.split('/').pop().replace('.liquid', '');
107
- this.element = document.querySelector(`[id^='shopify-section'][id$='${this.name}']`);
127
+ this.elements = fetchDOMSections(this.name);
108
128
  }
109
129
 
110
130
  valid() {
111
- return this.filename.startsWith('sections/') && this.element;
131
+ return this.filename.startsWith('sections/') && this.elements.length > 0;
112
132
  }
113
133
 
114
- async refresh() {
115
- var url = new URL(window.location.href);
116
- url.searchParams.append('section_id', this.name);
134
+ async refreshElement(element) {
135
+
136
+ const sectionId = element.id.replace(/^shopify-section-/, '');
137
+ const url = new URL(window.location.href);
138
+
139
+ url.searchParams.append('section_id', sectionId);
140
+
141
+ const response = await fetch(url);
117
142
 
118
143
  try {
119
- const response = await fetch(url);
120
144
  if (response.headers.get('x-templates-from-params') == '1') {
121
145
  const html = await response.text();
122
- this.element.outerHTML = html;
123
-
124
- console.log(`[HotReload] Reloaded ${this.name} section`);
146
+ element.outerHTML = html;
125
147
  } else {
126
- window.location.reload()
148
+ window.location.reload();
127
149
 
128
150
  console.log(`[HotReload] Hot-reloading not supported, fully reloading ${this.name} section`);
129
151
  }
@@ -132,6 +154,11 @@
132
154
  console.log(`[HotReload] Failed to reload ${this.name} section: ${e.message}`);
133
155
  }
134
156
  }
157
+
158
+ async refresh() {
159
+ console.log(`[HotReload] Reloaded ${this.name} sections`);
160
+ this.elements.forEach(this.refreshElement);
161
+ }
135
162
  }
136
163
 
137
164
  if (isReloadModeActive()) {
@@ -51,7 +51,7 @@ module ShopifyCLI
51
51
  def fetch_asset(file)
52
52
  api_client.get(
53
53
  path: "themes/#{@theme.id}/assets.json",
54
- query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
54
+ query: URI.encode_www_form("asset[key]" => file.relative_path),
55
55
  )
56
56
  rescue ShopifyCLI::API::APIRequestNotFoundError
57
57
  [404, {}]
@@ -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,35 @@
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
+ .reject { |file| @syncer.ignore_file?(file) }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ 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
@@ -46,20 +46,14 @@ module ShopifyCLI
46
46
  files
47
47
  .select { |file| @theme.theme_file?(file) }
48
48
  .map { |file| @theme[file] }
49
- .reject { |file| ignore_file?(file) }
49
+ .reject { |file| @syncer.ignore_file?(file) }
50
50
  end
51
51
 
52
52
  def filter_remote_files(files)
53
53
  files
54
54
  .select { |file| @syncer.remote_file?(file) }
55
55
  .map { |file| @theme[file] }
56
- .reject { |file| ignore_file?(file) }
57
- end
58
-
59
- private
60
-
61
- def ignore_file?(file)
62
- @ignore_filter&.ignore?(file.relative_path.to_s)
56
+ .reject { |file| @syncer.ignore_file?(file) }
63
57
  end
64
58
  end
65
59
  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
- watcher = Watcher.new(ctx, theme: theme, syncer: @syncer, ignore_filter: ignore_filter, poll: poll)
34
+ @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
+ 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,11 +87,13 @@ 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,
82
94
  ShopifyCLI::API::APIRequestUnauthorizedError
83
- raise ShopifyCLI::Abort, @ctx.message("theme.serve.ensure_user", theme.shop)
95
+ shop = ShopifyCLI::AdminAPI.get_shop_or_abort(@ctx)
96
+ raise ShopifyCLI::Abort, @ctx.message("theme.serve.ensure_user", shop)
84
97
  rescue Errno::EADDRINUSE
85
98
  error_message = @ctx.message("theme.serve.address_already_in_use", address)
86
99
  help_message = @ctx.message("theme.serve.try_port_option")
@@ -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