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.
- checksums.yaml +4 -4
- data/.vscode/settings.json +1 -2
- data/CHANGELOG.md +68 -20
- data/Gemfile.lock +1 -1
- data/Rakefile +21 -0
- data/ext/javy/hashes/javy-arm-macos-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-linux-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-macos-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-windows-v0.3.0.gz.sha256 +1 -0
- data/ext/javy/version +1 -1
- data/ext/shopify-extensions/version +1 -1
- data/lib/project_types/extension/cli.rb +4 -0
- data/lib/project_types/extension/commands/check.rb +6 -1
- data/lib/project_types/extension/forms/questions/ask_template.rb +1 -2
- data/lib/project_types/extension/messages/messages.rb +1 -3
- data/lib/project_types/extension/models/development_server_requirements.rb +1 -0
- data/lib/project_types/extension/models/specification_handlers/beacon_extension.rb +57 -0
- data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config.rb +33 -0
- data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config_repository.rb +75 -0
- data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +16 -1
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +4 -1
- data/lib/project_types/extension/tasks/configure_options.rb +2 -1
- data/lib/project_types/extension/tasks/convert_server_config.rb +13 -2
- data/lib/project_types/extension/tasks/merge_server_config.rb +5 -2
- data/lib/project_types/script/cli.rb +1 -0
- data/lib/project_types/script/layers/application/create_script.rb +14 -6
- data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
- data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +6 -21
- data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
- data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +35 -0
- data/lib/project_types/script/messages/messages.rb +3 -0
- data/lib/project_types/script/ui/error_handler.rb +11 -0
- data/lib/project_types/theme/cli.rb +1 -0
- data/lib/project_types/theme/commands/check.rb +4 -1
- data/lib/project_types/theme/commands/open.rb +2 -2
- data/lib/project_types/theme/commands/push.rb +1 -3
- data/lib/project_types/theme/commands/serve.rb +1 -0
- data/lib/project_types/theme/commands/share.rb +56 -0
- data/lib/project_types/theme/messages/messages.rb +64 -11
- data/lib/shopify_cli/changelog.rb +97 -25
- data/lib/shopify_cli/command_options/command_serve_options.rb +10 -0
- data/lib/shopify_cli/commands/app/serve.rb +7 -7
- data/lib/shopify_cli/commands/login.rb +5 -2
- data/lib/shopify_cli/context.rb +13 -0
- data/lib/shopify_cli/git.rb +36 -0
- data/lib/shopify_cli/identity_auth.rb +24 -4
- data/lib/shopify_cli/messages/messages.rb +22 -11
- data/lib/shopify_cli/release.rb +120 -20
- data/lib/shopify_cli/services/app/create/rails_service.rb +9 -1
- data/lib/shopify_cli/services/app/serve/node_service.rb +2 -25
- data/lib/shopify_cli/services/app/serve/php_service.rb +2 -25
- data/lib/shopify_cli/services/app/serve/rails_service.rb +8 -28
- data/lib/shopify_cli/services/app/serve/serve_service.rb +57 -0
- data/lib/shopify_cli/services.rb +1 -0
- data/lib/shopify_cli/tasks/update_dashboard_urls.rb +7 -9
- data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
- data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
- data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +35 -0
- data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
- data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -8
- data/lib/shopify_cli/theme/dev_server.rb +18 -5
- data/lib/shopify_cli/theme/file.rb +15 -4
- data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
- data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
- data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
- data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
- data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
- data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
- data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
- data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
- data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
- data/lib/shopify_cli/theme/syncer.rb +79 -63
- data/lib/shopify_cli/theme/theme.rb +21 -7
- data/lib/shopify_cli/theme/theme_admin_api.rb +23 -8
- data/lib/shopify_cli/thread_pool/job.rb +10 -2
- data/lib/shopify_cli/thread_pool.rb +15 -3
- data/lib/shopify_cli/tunnel.rb +3 -13
- data/lib/shopify_cli/version.rb +1 -1
- data/vendor/deps/cli-ui/lib/cli/ui/os.rb +8 -0
- 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
|
data/lib/shopify_cli/services.rb
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
17
|
-
redirectUrlWhitelist: constructed_urls,
|
|
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
|
-
|
|
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() ===
|
|
44
|
+
return reloadMode() === 'full-page';
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
function isReloadModeActive(){
|
|
28
|
-
return reloadMode() !==
|
|
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.
|
|
127
|
+
this.elements = fetchDOMSections(this.name);
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
valid() {
|
|
111
|
-
return this.filename.startsWith('sections/') && this.
|
|
131
|
+
return this.filename.startsWith('sections/') && this.elements.length > 0;
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
async
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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};
|
|
@@ -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,
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|