shopify-cli 2.24.0 → 2.26.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 +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/dev.yml +0 -3
- 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/script/cli.rb +0 -79
- data/lib/project_types/script/commands/connect.rb +3 -8
- data/lib/project_types/script/commands/create.rb +4 -29
- data/lib/project_types/script/commands/javy.rb +3 -8
- data/lib/project_types/script/commands/push.rb +4 -41
- data/lib/project_types/script/messages/messages.rb +1 -258
- data/lib/project_types/theme/commands/common/shop_helper.rb +13 -0
- data/lib/project_types/theme/commands/delete.rb +4 -1
- data/lib/project_types/theme/commands/list.rb +3 -4
- data/lib/project_types/theme/commands/open.rb +4 -1
- data/lib/project_types/theme/commands/publish.rb +4 -1
- data/lib/project_types/theme/commands/pull.rb +3 -1
- data/lib/project_types/theme/commands/push.rb +3 -1
- data/lib/project_types/theme/commands/serve.rb +15 -3
- data/lib/project_types/theme/messages/messages.rb +9 -7
- 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/messages/messages.rb +1 -1
- data/lib/shopify_cli/packager.rb +12 -3
- data/lib/shopify_cli/project.rb +1 -1
- data/lib/shopify_cli/release.rb +4 -2
- 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
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +1 -1
- metadata +28 -53
- data/.github/workflows/triage.yml +0 -22
- data/lib/project_types/script/config/extension_points.yml +0 -45
- data/lib/project_types/script/errors.rb +0 -10
- data/lib/project_types/script/forms/ask_app.rb +0 -27
- data/lib/project_types/script/forms/ask_org.rb +0 -30
- data/lib/project_types/script/forms/ask_script_uuid.rb +0 -22
- data/lib/project_types/script/forms/create.rb +0 -33
- data/lib/project_types/script/forms/run_against_shopify_org.rb +0 -14
- data/lib/project_types/script/graphql/app_script_set.graphql +0 -46
- data/lib/project_types/script/graphql/get_app_scripts.graphql +0 -6
- data/lib/project_types/script/graphql/module_upload_url_generate.graphql +0 -13
- data/lib/project_types/script/graphql/script_service_proxy.graphql +0 -7
- data/lib/project_types/script/layers/application/build_script.rb +0 -25
- data/lib/project_types/script/layers/application/connect_app.rb +0 -86
- data/lib/project_types/script/layers/application/create_script.rb +0 -90
- data/lib/project_types/script/layers/application/extension_points.rb +0 -66
- data/lib/project_types/script/layers/application/project_dependencies.rb +0 -26
- data/lib/project_types/script/layers/application/push_script.rb +0 -74
- data/lib/project_types/script/layers/domain/app_bridge.rb +0 -16
- data/lib/project_types/script/layers/domain/errors.rb +0 -47
- data/lib/project_types/script/layers/domain/extension_point.rb +0 -81
- data/lib/project_types/script/layers/domain/metadata.rb +0 -46
- data/lib/project_types/script/layers/domain/push_package.rb +0 -41
- data/lib/project_types/script/layers/domain/script_config.rb +0 -32
- data/lib/project_types/script/layers/domain/script_project.rb +0 -61
- data/lib/project_types/script/layers/infrastructure/api_clients/partners_proxy_api_client.rb +0 -53
- data/lib/project_types/script/layers/infrastructure/api_clients/script_service_api_client.rb +0 -35
- data/lib/project_types/script/layers/infrastructure/command_runner.rb +0 -19
- data/lib/project_types/script/layers/infrastructure/errors.rb +0 -211
- data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +0 -37
- data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +0 -62
- data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +0 -47
- data/lib/project_types/script/layers/infrastructure/languages/tool_version_checker.rb +0 -26
- data/lib/project_types/script/layers/infrastructure/languages/typescript_project_creator.rb +0 -45
- data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +0 -103
- data/lib/project_types/script/layers/infrastructure/languages/wasm_project_creator.rb +0 -12
- data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +0 -32
- data/lib/project_types/script/layers/infrastructure/metadata_repository.rb +0 -18
- data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +0 -36
- data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +0 -273
- data/lib/project_types/script/layers/infrastructure/script_service.rb +0 -135
- data/lib/project_types/script/layers/infrastructure/script_uploader.rb +0 -40
- data/lib/project_types/script/layers/infrastructure/service_locator.rb +0 -20
- data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +0 -35
- data/lib/project_types/script/ui/error_handler.rb +0 -331
- data/lib/project_types/script/ui/printing_spinner.rb +0 -75
- data/lib/project_types/script/ui/strict_spinner.rb +0 -20
- 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
|