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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/lib/project_types/extension/commands/serve.rb +57 -3
- data/lib/project_types/extension/extension_project.rb +8 -1
- data/lib/project_types/extension/loaders/project.rb +3 -2
- data/lib/project_types/extension/messages/messages.rb +21 -6
- data/lib/project_types/extension/models/server_config/development_renderer.rb +1 -1
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +18 -6
- data/lib/project_types/theme/commands/serve.rb +15 -3
- data/lib/project_types/theme/messages/messages.rb +4 -2
- data/lib/shopify_cli/commands/logout.rb +13 -2
- data/lib/shopify_cli/environment.rb +1 -1
- data/lib/shopify_cli/file_system_listener.rb +30 -0
- data/lib/shopify_cli/git.rb +116 -33
- data/lib/shopify_cli/identity_auth.rb +1 -0
- data/lib/shopify_cli/project.rb +1 -1
- data/lib/shopify_cli/tasks/ensure_project_type.rb +3 -1
- data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/certificate_manager.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/errors.rb +9 -0
- data/lib/shopify_cli/theme/dev_server/header_hash.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hooks/file_change_hook.rb +77 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_deleter.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/{hot-reload-no-script.html → hot_reload/resources/hot-reload-no-script.html} +0 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/hot_reload.js +48 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/sse_client.js +43 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme.js +114 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/resources/theme_extension.js +121 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/script_injector.rb +57 -0
- data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/hot_reload.rb +8 -76
- data/lib/shopify_cli/theme/dev_server/local_assets.rb +28 -28
- data/lib/shopify_cli/theme/dev_server/proxy.rb +33 -25
- data/lib/shopify_cli/theme/dev_server/proxy_param_builder.rb +82 -0
- data/lib/shopify_cli/theme/dev_server/reload_mode.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/sse.rb +1 -1
- data/lib/shopify_cli/theme/dev_server/watcher.rb +8 -9
- data/lib/shopify_cli/theme/dev_server/web_server.rb +1 -1
- data/lib/shopify_cli/theme/dev_server.rb +287 -99
- data/lib/shopify_cli/theme/extension/app_extension.rb +40 -0
- data/lib/shopify_cli/theme/extension/dev_server/hooks/file_change_hook.rb +68 -0
- data/lib/shopify_cli/theme/extension/dev_server/hot_reload/script_injector.rb +30 -0
- data/lib/shopify_cli/theme/extension/dev_server/hot_reload.rb +13 -0
- data/lib/shopify_cli/theme/extension/dev_server/local_assets.rb +30 -0
- data/lib/shopify_cli/theme/{dev_server/proxy/template_param_builder.rb → extension/dev_server/proxy_param_builder.rb} +26 -16
- data/lib/shopify_cli/theme/extension/dev_server/watcher.rb +47 -0
- data/lib/shopify_cli/theme/extension/dev_server.rb +150 -0
- data/lib/shopify_cli/theme/extension/host_theme.rb +104 -0
- data/lib/shopify_cli/theme/extension/syncer/extension_serve_job.rb +133 -0
- data/lib/shopify_cli/theme/extension/syncer/operation.rb +21 -0
- data/lib/shopify_cli/theme/extension/syncer.rb +81 -0
- data/lib/shopify_cli/theme/extension/ui/host_theme_progress_bar.rb +35 -0
- data/lib/shopify_cli/theme/ignore_helper.rb +31 -0
- data/lib/shopify_cli/theme/root.rb +62 -0
- data/lib/shopify_cli/theme/syncer.rb +12 -6
- data/lib/shopify_cli/theme/theme.rb +10 -52
- data/lib/shopify_cli/version.rb +1 -1
- metadata +27 -7
- data/.github/workflows/triage.yml +0 -22
- data/lib/shopify_cli/theme/dev_server/hot-reload.js +0 -194
- data/lib/shopify_cli/theme/syncer/ignore_helper.rb +0 -33
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shopify_cli/file_system_listener"
|
4
|
+
require "shopify_cli/theme/dev_server"
|
5
|
+
require "forwardable"
|
6
|
+
|
7
|
+
module ShopifyCLI
|
8
|
+
module Theme
|
9
|
+
module Extension
|
10
|
+
class DevServer < ShopifyCLI::Theme::DevServer
|
11
|
+
# Watches for file changes and publish events to the theme
|
12
|
+
class Watcher
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
def_delegators :@listener, :add_observer, :changed, :notify_observers
|
16
|
+
|
17
|
+
def initialize(ctx, extension:, syncer:, poll: false)
|
18
|
+
@ctx = ctx
|
19
|
+
@extension = extension
|
20
|
+
@syncer = syncer
|
21
|
+
@listener = FileSystemListener.new(root: @extension.root.to_s, force_poll: poll, ignore_regex: nil)
|
22
|
+
|
23
|
+
add_observer(self, :notify_updates)
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
@listener.start
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
@listener.stop
|
32
|
+
end
|
33
|
+
|
34
|
+
def notify_updates(modified, added, removed)
|
35
|
+
@syncer.enqueue_updates(files(modified).select { |file| @extension.extension_file?(file) })
|
36
|
+
@syncer.enqueue_creates(files(added).select { |file| @extension.extension_file?(file) })
|
37
|
+
@syncer.enqueue_deletes(files(removed))
|
38
|
+
end
|
39
|
+
|
40
|
+
def files(paths)
|
41
|
+
paths.map { |file| @extension[file] }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
require "shopify_cli/theme/extension/app_extension"
|
6
|
+
require "shopify_cli/theme/dev_server"
|
7
|
+
require "shopify_cli/theme/extension/host_theme"
|
8
|
+
require "shopify_cli/theme/syncer"
|
9
|
+
|
10
|
+
require_relative "dev_server/local_assets"
|
11
|
+
require_relative "dev_server/proxy_param_builder"
|
12
|
+
require_relative "dev_server/watcher"
|
13
|
+
require_relative "dev_server/hooks/file_change_hook"
|
14
|
+
require_relative "dev_server/hot_reload"
|
15
|
+
require_relative "dev_server/hot_reload/script_injector"
|
16
|
+
require_relative "syncer"
|
17
|
+
|
18
|
+
module ShopifyCLI
|
19
|
+
module Theme
|
20
|
+
module Extension
|
21
|
+
class DevServer < ShopifyCLI::Theme::DevServer
|
22
|
+
# Themes
|
23
|
+
Proxy = ShopifyCLI::Theme::DevServer::Proxy
|
24
|
+
CdnFonts = ShopifyCLI::Theme::DevServer::CdnFonts
|
25
|
+
|
26
|
+
# Extensions
|
27
|
+
ScriptInjector = ShopifyCLI::Theme::Extension::DevServer::HotReload::ScriptInjector
|
28
|
+
|
29
|
+
attr_accessor :project, :specification_handler
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def start(ctx, root, port: 9292, theme: nil, project:, specification_handler:)
|
33
|
+
instance.project = project
|
34
|
+
instance.specification_handler = specification_handler
|
35
|
+
|
36
|
+
super(ctx, root, port: port, theme: theme)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def middleware_stack
|
43
|
+
@app = Proxy.new(ctx, theme, param_builder)
|
44
|
+
@app = CdnFonts.new(@app, theme: theme)
|
45
|
+
@app = LocalAssets.new(ctx, @app, extension)
|
46
|
+
@app = HotReload.new(ctx, @app, broadcast_hooks: broadcast_hooks, watcher: watcher, mode: mode,
|
47
|
+
script_injector: script_injector)
|
48
|
+
end
|
49
|
+
|
50
|
+
def sync_theme
|
51
|
+
# Ensures the host theme exists
|
52
|
+
theme
|
53
|
+
|
54
|
+
# Ensure the theme app extension is pushed
|
55
|
+
extension
|
56
|
+
end
|
57
|
+
|
58
|
+
def syncer
|
59
|
+
@syncer ||= Syncer.new(
|
60
|
+
ctx,
|
61
|
+
extension: extension,
|
62
|
+
project: project,
|
63
|
+
specification_handler: specification_handler
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def theme
|
68
|
+
@theme ||= if theme_identifier
|
69
|
+
theme = ShopifyCLI::Theme::Theme.find_by_identifier(ctx, identifier: theme_identifier)
|
70
|
+
theme || ctx.abort(not_found_error_message)
|
71
|
+
else
|
72
|
+
HostTheme.find_or_create!(ctx)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def extension
|
77
|
+
return @extension if @extension
|
78
|
+
|
79
|
+
app = fetch_theme_app_extension_info.dig(*%w(data app)) || {}
|
80
|
+
|
81
|
+
app_id = app["id"]
|
82
|
+
|
83
|
+
registrations = app["extensionRegistrations"] || []
|
84
|
+
registration = registrations.find { |r| r["type"] == "THEME_APP_EXTENSION" } || {}
|
85
|
+
|
86
|
+
location = registration.dig(*%w(draftVersion location))
|
87
|
+
registration_id = registration["id"]
|
88
|
+
|
89
|
+
@extension = AppExtension.new(
|
90
|
+
ctx,
|
91
|
+
root: root,
|
92
|
+
app_id: app_id,
|
93
|
+
location: location,
|
94
|
+
registration_id: registration_id,
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch_theme_app_extension_info
|
99
|
+
params = {
|
100
|
+
api_key: project.app.api_key,
|
101
|
+
type: specification_handler.identifier.downcase,
|
102
|
+
}
|
103
|
+
|
104
|
+
PartnersAPI.query(@ctx, "get_extension_registrations", **params)
|
105
|
+
end
|
106
|
+
|
107
|
+
def watcher
|
108
|
+
@watcher ||= Watcher.new(ctx, syncer: syncer, extension: extension, poll: poll)
|
109
|
+
end
|
110
|
+
|
111
|
+
def param_builder
|
112
|
+
@param_builder ||= ProxyParamBuilder
|
113
|
+
.new
|
114
|
+
.with_extension(extension)
|
115
|
+
.with_syncer(syncer)
|
116
|
+
end
|
117
|
+
|
118
|
+
def setup_server
|
119
|
+
CLI::UI::Frame.open(frame_title, color: :magenta, timing: nil) do
|
120
|
+
ctx.puts(preview_message)
|
121
|
+
end
|
122
|
+
|
123
|
+
watcher.start
|
124
|
+
syncer.start
|
125
|
+
end
|
126
|
+
|
127
|
+
# Hooks
|
128
|
+
|
129
|
+
def broadcast_hooks
|
130
|
+
file_handler = Hooks::FileChangeHook.new(ctx, extension: extension, syncer: syncer)
|
131
|
+
[file_handler]
|
132
|
+
end
|
133
|
+
|
134
|
+
def script_injector
|
135
|
+
ScriptInjector.new(ctx)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Messages
|
139
|
+
|
140
|
+
def frame_title
|
141
|
+
ctx.message("serve.frame_title", root)
|
142
|
+
end
|
143
|
+
|
144
|
+
def preview_message
|
145
|
+
ctx.message("serve.preview_message", extension.location, theme.editor_url, address)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require_relative "../syncer"
|
5
|
+
require_relative "../development_theme"
|
6
|
+
require "shopify_cli/git"
|
7
|
+
require "shopify_cli/theme/extension/ui/host_theme_progress_bar"
|
8
|
+
require "tmpdir"
|
9
|
+
|
10
|
+
require "shopify_cli/git"
|
11
|
+
require "shopify_cli/theme/development_theme"
|
12
|
+
require "shopify_cli/theme/syncer"
|
13
|
+
|
14
|
+
module ShopifyCLI
|
15
|
+
module Theme
|
16
|
+
module Extension
|
17
|
+
class HostTheme < DevelopmentTheme
|
18
|
+
def id
|
19
|
+
ShopifyCLI::DB.get(:host_theme_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def name
|
23
|
+
existing_name = ShopifyCLI::DB.get(:host_theme_name)
|
24
|
+
if existing_name.nil? || existing_name.length > API_NAME_LIMIT
|
25
|
+
generate_host_theme_name
|
26
|
+
else
|
27
|
+
existing_name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def ensure_exists!
|
32
|
+
if exists?
|
33
|
+
@ctx.debug("Using temporary host theme: ##{id} #{name}")
|
34
|
+
else
|
35
|
+
create
|
36
|
+
@ctx.debug("Created temporary host theme: #{@id}")
|
37
|
+
end
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.delete(ctx)
|
43
|
+
new(ctx, root: nil).delete
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete
|
47
|
+
delete_theme if exists? # Avoid deleting any existing development theme logic
|
48
|
+
|
49
|
+
ShopifyCLI::DB.del(:host_theme_id) if ShopifyCLI::DB.exists?(:host_theme_id)
|
50
|
+
ShopifyCLI::DB.del(:host_theme_name) if ShopifyCLI::DB.exists?(:host_theme_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def create
|
54
|
+
super
|
55
|
+
ShopifyCLI::DB.set(host_theme_id: @id)
|
56
|
+
|
57
|
+
generate_tmp_theme
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.find_or_create!(ctx)
|
61
|
+
new(ctx, root: nil).ensure_exists!
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def generate_host_theme_name
|
67
|
+
hostname = Socket.gethostname.split(".").shift
|
68
|
+
hash = SecureRandom.hex(3)
|
69
|
+
|
70
|
+
theme_name = "App Ext. Host ()"
|
71
|
+
hostname_character_limit = API_NAME_LIMIT - theme_name.length - hash.length - 1
|
72
|
+
identifier = encode_identifier("#{hash}-#{hostname[0, hostname_character_limit]}")
|
73
|
+
theme_name = "App Ext. Host (#{identifier})"
|
74
|
+
|
75
|
+
ShopifyCLI::DB.set(host_theme_name: theme_name)
|
76
|
+
|
77
|
+
theme_name
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate_tmp_theme
|
81
|
+
ctx = @ctx.dup
|
82
|
+
|
83
|
+
Dir.mktmpdir do |dir|
|
84
|
+
@root = Pathname.new(dir)
|
85
|
+
ctx.root = dir
|
86
|
+
|
87
|
+
syncer = ShopifyCLI::Theme::Syncer.new(ctx, theme: self)
|
88
|
+
|
89
|
+
begin
|
90
|
+
syncer.start_threads
|
91
|
+
::CLI::UI::Frame.open(ctx.message("theme.push.info.pushing", name, id, shop)) do
|
92
|
+
UI::HostThemeProgressBar.new(syncer, dir).progress(:upload_theme!, delete: false)
|
93
|
+
end
|
94
|
+
rescue Errno::ENOENT => e
|
95
|
+
ctx.debug(e.message)
|
96
|
+
ensure
|
97
|
+
syncer.shutdown
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "project_types/extension/loaders/project"
|
3
|
+
require "project_types/extension/loaders/specification_handler"
|
4
|
+
require "shopify_cli/partners_api"
|
5
|
+
require "shopify_cli/thread_pool/job"
|
6
|
+
|
7
|
+
module ShopifyCLI
|
8
|
+
module Theme
|
9
|
+
module Extension
|
10
|
+
class Syncer
|
11
|
+
class ExtensionServeJob < ThreadPool::Job
|
12
|
+
POLL_FREQUENCY = 0.5 # second
|
13
|
+
PUSH_INTERVAL = 5 # seconds
|
14
|
+
|
15
|
+
RESPONSE_FIELD = %w(data extensionUpdateDraft)
|
16
|
+
VERSION_FIELD = "extensionVersion"
|
17
|
+
USER_ERRORS_FIELD = "userErrors"
|
18
|
+
ERROR_FILE_REGEX = /\[([^\]\[]*)\]/
|
19
|
+
|
20
|
+
def initialize(ctx, syncer:, extension:, project:, specification_handler:)
|
21
|
+
super(POLL_FREQUENCY)
|
22
|
+
|
23
|
+
@ctx = ctx
|
24
|
+
@extension = extension
|
25
|
+
@project = project
|
26
|
+
@specification_handler = specification_handler
|
27
|
+
|
28
|
+
@syncer = syncer
|
29
|
+
@syncer_mutex = Mutex.new
|
30
|
+
|
31
|
+
@job_in_progress = false
|
32
|
+
@job_in_progress_mutex = Mutex.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def perform!
|
36
|
+
return unless @syncer.any_operation?
|
37
|
+
return if job_in_progress?
|
38
|
+
return if recently_synced? && !@syncer.any_blocking_operation?
|
39
|
+
|
40
|
+
job_in_progress!
|
41
|
+
|
42
|
+
input = {
|
43
|
+
api_key: @project.app.api_key,
|
44
|
+
registration_id: @project.registration_id,
|
45
|
+
config: JSON.generate(@specification_handler.config(@ctx)),
|
46
|
+
extension_context: @specification_handler.extension_context(@ctx),
|
47
|
+
}
|
48
|
+
response = ShopifyCLI::PartnersAPI.query(@ctx, "extension_update_draft", **input).dig(*RESPONSE_FIELD)
|
49
|
+
user_errors = response.dig(USER_ERRORS_FIELD)
|
50
|
+
|
51
|
+
if user_errors
|
52
|
+
@ctx.puts(error_message(@project.title))
|
53
|
+
error_files = erroneous_files(user_errors)
|
54
|
+
print_items(error_files)
|
55
|
+
else
|
56
|
+
@ctx.puts(success_message(@project.title))
|
57
|
+
print_items({}.freeze)
|
58
|
+
::Extension::Tasks::Converters::VersionConverter.from_hash(@ctx, response.dig(VERSION_FIELD))
|
59
|
+
end
|
60
|
+
|
61
|
+
@syncer_mutex.synchronize do
|
62
|
+
@syncer.pending_operations.clear
|
63
|
+
@syncer.latest_sync = Time.now
|
64
|
+
end
|
65
|
+
|
66
|
+
ensure
|
67
|
+
job_in_progress!(false)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def job_in_progress!(in_progress = true)
|
73
|
+
@job_in_progress_mutex.synchronize { @job_in_progress = in_progress }
|
74
|
+
end
|
75
|
+
|
76
|
+
def job_in_progress?
|
77
|
+
@job_in_progress
|
78
|
+
end
|
79
|
+
|
80
|
+
def recently_synced?
|
81
|
+
Time.now - @syncer.latest_sync < PUSH_INTERVAL
|
82
|
+
end
|
83
|
+
|
84
|
+
def timestamp
|
85
|
+
Time.now.strftime("%T")
|
86
|
+
end
|
87
|
+
|
88
|
+
def success_message(project)
|
89
|
+
"#{timestamp} {{green:Pushed}} {{>}} {{blue:'#{project}'}} to a draft"
|
90
|
+
end
|
91
|
+
|
92
|
+
def error_message(project)
|
93
|
+
"#{timestamp} {{red:Error}} {{>}} {{blue:'#{project}'}} could not be pushed:"
|
94
|
+
end
|
95
|
+
|
96
|
+
def print_file_success(file)
|
97
|
+
@ctx.puts("{{blue:- #{file.relative_path}}}")
|
98
|
+
end
|
99
|
+
|
100
|
+
def print_file_error(file, err)
|
101
|
+
@ctx.puts("{{red:- #{file.relative_path}}}")
|
102
|
+
@ctx.puts("{{red: - Cause: #{err}}}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def erroneous_files(errors)
|
106
|
+
files = {}
|
107
|
+
errors.each do |e|
|
108
|
+
path = e["message"][ERROR_FILE_REGEX, 1]
|
109
|
+
file = @extension[path]
|
110
|
+
files[file] = e["message"]
|
111
|
+
end
|
112
|
+
files
|
113
|
+
end
|
114
|
+
|
115
|
+
def print_items(erroneous_files)
|
116
|
+
@syncer.pending_files.each do |file|
|
117
|
+
err = erroneous_files.dig(file)
|
118
|
+
if err
|
119
|
+
print_file_error(file, err)
|
120
|
+
erroneous_files.delete(file)
|
121
|
+
else
|
122
|
+
print_file_success(file)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
erroneous_files.each do |file, err|
|
126
|
+
print_file_error(file, err)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module ShopifyCLI
|
6
|
+
module Theme
|
7
|
+
module Extension
|
8
|
+
class Syncer
|
9
|
+
class Operation < Struct.new(:file, :kind)
|
10
|
+
def delete?
|
11
|
+
kind == :delete
|
12
|
+
end
|
13
|
+
|
14
|
+
def create?
|
15
|
+
kind == :create
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "syncer/extension_serve_job"
|
4
|
+
require_relative "syncer/operation"
|
5
|
+
|
6
|
+
require "shopify_cli/thread_pool"
|
7
|
+
|
8
|
+
module ShopifyCLI
|
9
|
+
module Theme
|
10
|
+
module Extension
|
11
|
+
class Syncer
|
12
|
+
attr_accessor :pending_operations, :latest_sync
|
13
|
+
|
14
|
+
def initialize(ctx, extension:, project:, specification_handler:)
|
15
|
+
@ctx = ctx
|
16
|
+
@extension = extension
|
17
|
+
@project = project
|
18
|
+
@specification_handler = specification_handler
|
19
|
+
|
20
|
+
@pool = ThreadPool.new(pool_size: 1)
|
21
|
+
@pending_operations = extension.extension_files.map { |file| Operation.new(file, :update) }
|
22
|
+
@pending_operations_mutex = Mutex.new
|
23
|
+
@latest_sync = Time.now - ExtensionServeJob::PUSH_INTERVAL
|
24
|
+
end
|
25
|
+
|
26
|
+
def enqueue_creates(files)
|
27
|
+
operations = files.map { |file| Operation.new(file, :create) }
|
28
|
+
enqueue_operations(operations)
|
29
|
+
end
|
30
|
+
|
31
|
+
def enqueue_updates(files)
|
32
|
+
operations = files.map { |file| Operation.new(file, :update) }
|
33
|
+
enqueue_operations(operations)
|
34
|
+
end
|
35
|
+
|
36
|
+
def enqueue_deletes(files)
|
37
|
+
operations = files.map { |file| Operation.new(file, :delete) }
|
38
|
+
enqueue_operations(operations)
|
39
|
+
end
|
40
|
+
|
41
|
+
def start
|
42
|
+
@pool.schedule(job)
|
43
|
+
end
|
44
|
+
|
45
|
+
def shutdown
|
46
|
+
@pool.shutdown
|
47
|
+
end
|
48
|
+
|
49
|
+
def pending_files
|
50
|
+
pending_operations.map(&:file)
|
51
|
+
end
|
52
|
+
|
53
|
+
def any_operation?
|
54
|
+
pending_operations.any?
|
55
|
+
end
|
56
|
+
|
57
|
+
def any_blocking_operation?
|
58
|
+
pending_operations.any? { |operation| operation.delete? || operation.create? }
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def enqueue_operations(operations)
|
64
|
+
@pending_operations_mutex.synchronize do
|
65
|
+
operations.each { |f| @pending_operations << f unless @pending_operations.include?(f) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def job
|
70
|
+
ExtensionServeJob.new(
|
71
|
+
@ctx,
|
72
|
+
syncer: self,
|
73
|
+
extension: @extension,
|
74
|
+
project: @project,
|
75
|
+
specification_handler: @specification_handler
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
module ShopifyCLI
|
3
|
+
module Theme
|
4
|
+
module Extension
|
5
|
+
module UI
|
6
|
+
class HostThemeProgressBar
|
7
|
+
GIT_CLONE_PROGRESS_SHARE = 0.2
|
8
|
+
SYNC_PROGRESS_SHARE = 0.8
|
9
|
+
|
10
|
+
def initialize(syncer, dir)
|
11
|
+
@syncer = syncer
|
12
|
+
@dir = dir
|
13
|
+
end
|
14
|
+
|
15
|
+
def progress(method, **args)
|
16
|
+
@syncer.lock_io!
|
17
|
+
CLI::UI::Progress.progress do |bar|
|
18
|
+
Git.public_send(:raw_clone, "https://github.com/Shopify/dawn.git", @dir) do |percent|
|
19
|
+
bar.tick(set_percent: percent * GIT_CLONE_PROGRESS_SHARE)
|
20
|
+
end
|
21
|
+
|
22
|
+
@syncer.public_send(method, **args) do |left, total|
|
23
|
+
next if total == 0
|
24
|
+
bar.tick(set_percent: (1 - left.to_f / total) * SYNC_PROGRESS_SHARE + GIT_CLONE_PROGRESS_SHARE)
|
25
|
+
end
|
26
|
+
|
27
|
+
bar.tick(set_percent: 1)
|
28
|
+
end
|
29
|
+
@syncer.unlock_io!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCLI
|
4
|
+
module Theme
|
5
|
+
module IgnoreHelper
|
6
|
+
def ignore_operation?(operation)
|
7
|
+
path = operation.file_path
|
8
|
+
ignore_path?(path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def ignore_file?(file)
|
12
|
+
path = file.relative_path
|
13
|
+
ignore_path?(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def ignore_path?(path)
|
17
|
+
ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def ignored_by_ignore_filter?(path)
|
23
|
+
ignore_filter&.ignore?(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ignored_by_include_filter?(path)
|
27
|
+
!!include_filter && !include_filter.match?(path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "file"
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module ShopifyCLI
|
6
|
+
module Theme
|
7
|
+
class Root
|
8
|
+
attr_reader :root, :ctx
|
9
|
+
|
10
|
+
def initialize(ctx, root:)
|
11
|
+
@ctx = ctx
|
12
|
+
@root = Pathname.new(root) if root
|
13
|
+
end
|
14
|
+
|
15
|
+
def static_asset_files
|
16
|
+
glob("assets/*", raise_on_dir: true).reject(&:liquid?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def liquid_files
|
20
|
+
glob("**/*.liquid")
|
21
|
+
end
|
22
|
+
|
23
|
+
def json_files
|
24
|
+
glob("**/*.json")
|
25
|
+
end
|
26
|
+
|
27
|
+
def glob(pattern, raise_on_dir: false)
|
28
|
+
root
|
29
|
+
.glob(pattern)
|
30
|
+
.select { |path| file?(path, raise_on_dir) }
|
31
|
+
.map { |path| File.new(path, root) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def static_asset_file?(file)
|
35
|
+
static_asset_files.include?(self[file])
|
36
|
+
end
|
37
|
+
|
38
|
+
def static_asset_paths
|
39
|
+
static_asset_files.map(&:relative_path)
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](file)
|
43
|
+
case file
|
44
|
+
when File
|
45
|
+
file
|
46
|
+
when Pathname
|
47
|
+
File.new(file, root)
|
48
|
+
when String
|
49
|
+
File.new(root.join(file), root)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def file?(path, raise_on_dir = false)
|
54
|
+
if raise_on_dir && ::File.directory?(path)
|
55
|
+
@ctx.abort(@ctx.message("theme.serve.error.invalid_subdirectory", path.to_s))
|
56
|
+
end
|
57
|
+
|
58
|
+
::File.file?(path)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|