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.
- 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
|