shopify-cli 2.7.1 → 2.8.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/.github/CODEOWNERS +2 -2
- data/.github/workflows/shopify.yml +1 -1
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +43 -0
- data/Codespace.dockerfile +2 -2
- data/Gemfile.lock +4 -4
- data/Tests.dockerfile +2 -2
- data/dev.yml +3 -3
- data/ext/javy/hashes/javy-arm-macos-v0.1.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-linux-v0.1.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-macos-v0.1.0.gz.sha256 +1 -0
- data/ext/javy/hashes/javy-x86_64-windows-v0.1.0.gz.sha256 +1 -0
- data/ext/javy/javy.rb +30 -12
- data/lib/graphql/get_extension_registrations.graphql +27 -0
- data/lib/project_types/extension/cli.rb +27 -2
- data/lib/project_types/extension/commands/build.rb +10 -15
- data/lib/project_types/extension/commands/create.rb +3 -6
- data/lib/project_types/extension/commands/push.rb +36 -8
- data/lib/project_types/extension/extension_project.rb +1 -1
- data/lib/project_types/extension/features/argo_serve.rb +6 -5
- data/lib/project_types/extension/forms/questions/ask_registration.rb +6 -2
- data/lib/project_types/extension/loaders/project.rb +29 -0
- data/lib/project_types/extension/loaders/specification_handler.rb +22 -0
- data/lib/project_types/extension/messages/messages.rb +4 -2
- data/lib/project_types/extension/models/app.rb +1 -1
- data/lib/project_types/extension/models/development_server.rb +2 -2
- data/lib/project_types/extension/models/specification_handlers/default.rb +4 -0
- data/lib/project_types/extension/tasks/convert_server_config.rb +3 -1
- data/lib/project_types/extension/tasks/execute_commands/base.rb +13 -0
- data/lib/project_types/extension/tasks/execute_commands/build.rb +29 -0
- data/lib/project_types/extension/tasks/execute_commands/create.rb +33 -0
- data/lib/project_types/extension/tasks/execute_commands/serve.rb +35 -0
- data/lib/project_types/extension/tasks/merge_server_config.rb +33 -22
- data/lib/project_types/rails/commands/create.rb +2 -4
- data/lib/project_types/script/cli.rb +8 -1
- data/lib/project_types/script/commands/connect.rb +19 -0
- data/lib/project_types/script/commands/create.rb +1 -3
- data/lib/project_types/script/commands/javy.rb +0 -2
- data/lib/project_types/script/commands/push.rb +2 -1
- data/lib/project_types/script/config/extension_points.yml +10 -28
- data/lib/project_types/script/forms/ask_app.rb +32 -0
- data/lib/project_types/script/forms/ask_org.rb +30 -0
- data/lib/project_types/script/forms/ask_script_uuid.rb +22 -0
- data/lib/project_types/script/forms/run_against_shopify_org.rb +14 -0
- data/lib/project_types/script/graphql/app_script_set.graphql +2 -2
- data/lib/project_types/script/layers/application/build_script.rb +0 -1
- data/lib/project_types/script/layers/application/connect_app.rb +79 -0
- data/lib/project_types/script/layers/application/create_script.rb +17 -17
- data/lib/project_types/script/layers/application/push_script.rb +1 -1
- data/lib/project_types/script/layers/domain/errors.rb +1 -4
- data/lib/project_types/script/layers/domain/push_package.rb +3 -3
- data/lib/project_types/script/layers/domain/{script_json.rb → script_config.rb} +2 -2
- data/lib/project_types/script/layers/domain/script_project.rb +5 -1
- data/lib/project_types/script/layers/infrastructure/errors.rb +28 -6
- data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +0 -4
- data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +0 -4
- data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -2
- data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +125 -27
- data/lib/project_types/script/layers/infrastructure/script_service.rb +11 -11
- data/lib/project_types/script/messages/messages.rb +20 -5
- data/lib/project_types/script/ui/error_handler.rb +30 -20
- data/lib/project_types/theme/commands/pull.rb +3 -0
- data/lib/project_types/theme/commands/push.rb +7 -1
- data/lib/project_types/theme/commands/serve.rb +1 -1
- data/lib/project_types/theme/messages/messages.rb +10 -0
- data/lib/project_types/theme/ui/sync_progress_bar.rb +2 -2
- data/lib/shopify_cli/command/project_command.rb +20 -7
- data/lib/shopify_cli/command.rb +6 -0
- data/lib/shopify_cli/commands/app/create/node.rb +1 -3
- data/lib/shopify_cli/commands/app/create/rails.rb +1 -3
- data/lib/shopify_cli/commands/login.rb +1 -1
- data/lib/shopify_cli/commands/switch.rb +1 -1
- data/lib/shopify_cli/constants.rb +7 -0
- data/lib/shopify_cli/context.rb +11 -1
- data/lib/shopify_cli/environment.rb +4 -0
- data/lib/shopify_cli/form.rb +2 -0
- data/lib/shopify_cli/git.rb +2 -0
- data/lib/shopify_cli/identity_auth.rb +18 -0
- data/lib/shopify_cli/messages/messages.rb +8 -1
- data/lib/shopify_cli/partners_api/app_extensions/job.rb +36 -0
- data/lib/shopify_cli/partners_api/app_extensions.rb +46 -0
- data/lib/shopify_cli/partners_api/organizations.rb +2 -5
- data/lib/shopify_cli/partners_api.rb +2 -8
- data/lib/shopify_cli/project.rb +8 -7
- data/lib/shopify_cli/resources/env_file.rb +13 -5
- data/lib/shopify_cli/services/app/create/rails_service.rb +1 -1
- data/lib/shopify_cli/services/app/serve/node_service.rb +1 -1
- data/lib/shopify_cli/services/app/serve/rails_service.rb +1 -1
- data/lib/shopify_cli/tasks/ensure_authenticated.rb +9 -3
- data/lib/shopify_cli/theme/dev_server/cdn_fonts.rb +73 -0
- data/lib/shopify_cli/theme/dev_server/hot-reload.js +38 -9
- data/lib/shopify_cli/theme/dev_server/proxy/template_param_builder.rb +84 -0
- data/lib/shopify_cli/theme/dev_server/proxy.rb +9 -15
- data/lib/shopify_cli/theme/dev_server.rb +6 -4
- data/lib/shopify_cli/theme/syncer/error_reporter.rb +45 -0
- data/lib/shopify_cli/theme/syncer/operation.rb +56 -0
- data/lib/shopify_cli/theme/syncer/standard_reporter.rb +32 -0
- data/lib/shopify_cli/theme/syncer.rb +40 -39
- data/lib/shopify_cli/theme/theme.rb +31 -19
- data/lib/shopify_cli/thread_pool/job.rb +27 -0
- data/lib/shopify_cli/thread_pool.rb +37 -0
- data/lib/shopify_cli/tunnel.rb +9 -10
- data/lib/shopify_cli/version.rb +1 -1
- data/shopify-cli.gemspec +1 -1
- data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +3 -1
- metadata +31 -8
- data/lib/graphql/all_orgs_with_extensions.graphql +0 -37
- data/lib/project_types/extension/tasks/run_extension_command.rb +0 -82
- data/lib/project_types/script/tasks/ensure_env.rb +0 -106
|
@@ -17,19 +17,48 @@
|
|
|
17
17
|
|
|
18
18
|
connect();
|
|
19
19
|
|
|
20
|
+
function isRefreshRequired(files) {
|
|
21
|
+
return files.some((file) => !isCssFile(file) && !isSectionFile(file));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function refreshFile(file) {
|
|
25
|
+
if (isCssFile(file)) {
|
|
26
|
+
reloadCssFile(file);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isSectionFile(file)) {
|
|
31
|
+
reloadSection(file);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setHotReloadCookie(files) {
|
|
37
|
+
var date = new Date();
|
|
38
|
+
|
|
39
|
+
// Hot reload cookie expires in 3 seconds
|
|
40
|
+
date.setSeconds(date.getSeconds() + 3);
|
|
41
|
+
|
|
42
|
+
var sections = files.join(',');
|
|
43
|
+
var expires = date.toUTCString();
|
|
44
|
+
|
|
45
|
+
document.cookie = `hot_reload_sections=${sections}; expires=${expires}; path=/`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function refreshPage(files) {
|
|
49
|
+
setHotReloadCookie(files);
|
|
50
|
+
console.log('[HotReload] Refreshing entire page');
|
|
51
|
+
window.location.reload();
|
|
52
|
+
}
|
|
53
|
+
|
|
20
54
|
function handleUpdate(message) {
|
|
21
55
|
var data = JSON.parse(message.data);
|
|
56
|
+
var modifiedFiles = data.modified;
|
|
22
57
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (isCssFile(modified)) {
|
|
27
|
-
reloadCssFile(modified)
|
|
28
|
-
} else if (isSectionFile(modified)) {
|
|
29
|
-
reloadSection(modified);
|
|
58
|
+
if (isRefreshRequired(modifiedFiles)) {
|
|
59
|
+
refreshPage(modifiedFiles);
|
|
30
60
|
} else {
|
|
31
|
-
|
|
32
|
-
window.location.reload();
|
|
61
|
+
modifiedFiles.forEach(refreshFile);
|
|
33
62
|
}
|
|
34
63
|
}
|
|
35
64
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module ShopifyCLI
|
|
6
|
+
module Theme
|
|
7
|
+
module DevServer
|
|
8
|
+
class Proxy
|
|
9
|
+
class TemplateParamBuilder
|
|
10
|
+
def build
|
|
11
|
+
# Core doesn't support replace_templates
|
|
12
|
+
return {} if core?(current_path)
|
|
13
|
+
|
|
14
|
+
(syncer_templates + request_templates)
|
|
15
|
+
.select { |file| file.liquid? || file.json? }
|
|
16
|
+
.uniq(&:relative_path)
|
|
17
|
+
.map { |file| as_param(file) }
|
|
18
|
+
.to_h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def with_core_endpoints(core_endpoints)
|
|
22
|
+
@core_endpoints = core_endpoints
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_syncer(syncer)
|
|
27
|
+
@syncer = syncer
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_rack_env(rack_env)
|
|
32
|
+
@rack_env = rack_env
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def with_theme(theme)
|
|
37
|
+
@theme = theme
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def as_param(file)
|
|
44
|
+
["replace_templates[#{file.relative_path}]", file.read]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def syncer_templates
|
|
48
|
+
@syncer&.pending_updates || []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def request_templates
|
|
52
|
+
cookie_sections
|
|
53
|
+
.map { |section| @theme[section] unless @theme.nil? }
|
|
54
|
+
.compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cookie_sections
|
|
58
|
+
CGI::Cookie.parse(cookie)["hot_reload_sections"].join.split(",") || []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def core?(path)
|
|
62
|
+
core_endpoints.include?(path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def current_path
|
|
66
|
+
rack_env["PATH_INFO"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cookie
|
|
70
|
+
rack_env["HTTP_COOKIE"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def core_endpoints
|
|
74
|
+
@core_endpoints || []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rack_env
|
|
78
|
+
@rack_env || {}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
require "net/http"
|
|
3
3
|
require "stringio"
|
|
4
4
|
require "time"
|
|
5
|
+
require "cgi"
|
|
6
|
+
|
|
7
|
+
require_relative "proxy/template_param_builder"
|
|
5
8
|
|
|
6
9
|
module ShopifyCLI
|
|
7
10
|
module Theme
|
|
@@ -112,21 +115,12 @@ module ShopifyCLI
|
|
|
112
115
|
end
|
|
113
116
|
|
|
114
117
|
def build_replace_templates_param(env)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Only replace Liquid or JSON files
|
|
122
|
-
file.liquid? || file.json?
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
pending_templates.each do |path|
|
|
126
|
-
params["replace_templates[#{path.relative_path}]"] = path.read
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
params
|
|
118
|
+
TemplateParamBuilder.new
|
|
119
|
+
.with_core_endpoints(@core_endpoints)
|
|
120
|
+
.with_syncer(@syncer)
|
|
121
|
+
.with_theme(@theme)
|
|
122
|
+
.with_rack_env(env)
|
|
123
|
+
.build
|
|
130
124
|
end
|
|
131
125
|
|
|
132
126
|
def add_session_cookie(cookie_header)
|
|
@@ -3,6 +3,7 @@ require_relative "development_theme"
|
|
|
3
3
|
require_relative "ignore_filter"
|
|
4
4
|
require_relative "syncer"
|
|
5
5
|
|
|
6
|
+
require_relative "dev_server/cdn_fonts"
|
|
6
7
|
require_relative "dev_server/hot_reload"
|
|
7
8
|
require_relative "dev_server/header_hash"
|
|
8
9
|
require_relative "dev_server/local_assets"
|
|
@@ -24,7 +25,7 @@ module ShopifyCLI
|
|
|
24
25
|
class << self
|
|
25
26
|
attr_accessor :ctx
|
|
26
27
|
|
|
27
|
-
def start(ctx, root,
|
|
28
|
+
def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false)
|
|
28
29
|
@ctx = ctx
|
|
29
30
|
theme = DevelopmentTheme.new(ctx, root: root)
|
|
30
31
|
ignore_filter = IgnoreFilter.from_path(root)
|
|
@@ -33,10 +34,11 @@ module ShopifyCLI
|
|
|
33
34
|
|
|
34
35
|
# Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
|
|
35
36
|
@app = Proxy.new(ctx, theme: theme, syncer: @syncer)
|
|
37
|
+
@app = CdnFonts.new(@app, theme: theme)
|
|
36
38
|
@app = LocalAssets.new(ctx, @app, theme: theme)
|
|
37
39
|
@app = HotReload.new(ctx, @app, theme: theme, watcher: watcher, ignore_filter: ignore_filter)
|
|
38
40
|
stopped = false
|
|
39
|
-
address = "http://#{
|
|
41
|
+
address = "http://#{host}:#{port}"
|
|
40
42
|
|
|
41
43
|
theme.ensure_exists!
|
|
42
44
|
|
|
@@ -70,7 +72,7 @@ module ShopifyCLI
|
|
|
70
72
|
watcher.start
|
|
71
73
|
WebServer.run(
|
|
72
74
|
@app,
|
|
73
|
-
BindAddress:
|
|
75
|
+
BindAddress: host,
|
|
74
76
|
Port: port,
|
|
75
77
|
Logger: logger,
|
|
76
78
|
AccessLog: [],
|
|
@@ -83,7 +85,7 @@ module ShopifyCLI
|
|
|
83
85
|
rescue Errno::EADDRINUSE
|
|
84
86
|
abort_address_already_in_use(address)
|
|
85
87
|
rescue Errno::EADDRNOTAVAIL
|
|
86
|
-
raise AddressBindingError, "Error binding to the address #{
|
|
88
|
+
raise AddressBindingError, "Error binding to the address #{host}."
|
|
87
89
|
end
|
|
88
90
|
|
|
89
91
|
def stop
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
module Theme
|
|
5
|
+
class Syncer
|
|
6
|
+
##
|
|
7
|
+
# ShopifyCLI::Theme::Syncer::ErrorReporter allows delaying log of errors,
|
|
8
|
+
# mainly to not break the progress bar.
|
|
9
|
+
#
|
|
10
|
+
class ErrorReporter
|
|
11
|
+
attr_reader :ctx, :delayed_errors
|
|
12
|
+
|
|
13
|
+
def initialize(ctx)
|
|
14
|
+
@ctx = ctx
|
|
15
|
+
@has_any_error = false
|
|
16
|
+
@delay_errors = false
|
|
17
|
+
@delayed_errors = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def disable!
|
|
21
|
+
@delay_errors = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enable!
|
|
25
|
+
@delay_errors = false
|
|
26
|
+
@delayed_errors.each { |error| report(error) }
|
|
27
|
+
@delayed_errors.clear
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def report(error_message)
|
|
31
|
+
if @delay_errors
|
|
32
|
+
@delayed_errors << error_message
|
|
33
|
+
else
|
|
34
|
+
@has_any_error = true
|
|
35
|
+
@ctx.error(error_message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def has_any_error?
|
|
40
|
+
@has_any_error
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
module Theme
|
|
5
|
+
class Syncer
|
|
6
|
+
class Operation
|
|
7
|
+
attr_accessor :method, :file
|
|
8
|
+
|
|
9
|
+
COLOR_BY_STATUS = {
|
|
10
|
+
error: :red,
|
|
11
|
+
synced: :green,
|
|
12
|
+
fixed: :cyan,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def initialize(ctx, method, file)
|
|
16
|
+
@ctx = ctx
|
|
17
|
+
@method = method
|
|
18
|
+
@file = file
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
"#{method} #{file_path}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def as_error_message
|
|
26
|
+
as_message_with(status: :error)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def as_synced_message
|
|
30
|
+
as_message_with(status: :synced)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def as_fix_message
|
|
34
|
+
as_message_with(status: :fixed)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def file_path
|
|
38
|
+
file&.relative_path.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def as_message_with(status:)
|
|
44
|
+
status_color = COLOR_BY_STATUS[status]
|
|
45
|
+
status_text = @ctx.message("theme.serve.operation.status.#{status}").ljust(6)
|
|
46
|
+
|
|
47
|
+
"#{timestamp} {{#{status_color}:#{status_text}}} {{>}} {{blue:#{self}}}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def timestamp
|
|
51
|
+
Time.now.strftime("%T")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
module Theme
|
|
5
|
+
class Syncer
|
|
6
|
+
##
|
|
7
|
+
# ShopifyCLI::Theme::Syncer::StdReporter allows disabling/enabling
|
|
8
|
+
# messages reported in the standard output (ShopifyCLI::Context#puts).
|
|
9
|
+
#
|
|
10
|
+
class StandardReporter
|
|
11
|
+
attr_reader :ctx
|
|
12
|
+
|
|
13
|
+
def initialize(ctx)
|
|
14
|
+
@enabled = true
|
|
15
|
+
@ctx = ctx
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def disable!
|
|
19
|
+
@enabled = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def enable!
|
|
23
|
+
@enabled = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def report(message)
|
|
27
|
+
ctx.puts(message) if @enabled
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -2,24 +2,31 @@
|
|
|
2
2
|
require "thread"
|
|
3
3
|
require "json"
|
|
4
4
|
require "base64"
|
|
5
|
+
require "forwardable"
|
|
6
|
+
|
|
7
|
+
require_relative "syncer/error_reporter"
|
|
8
|
+
require_relative "syncer/standard_reporter"
|
|
9
|
+
require_relative "syncer/operation"
|
|
5
10
|
|
|
6
11
|
module ShopifyCLI
|
|
7
12
|
module Theme
|
|
8
13
|
class Syncer
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"#{method} #{file&.relative_path}"
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
+
extend Forwardable
|
|
15
|
+
|
|
14
16
|
API_VERSION = "unstable"
|
|
15
17
|
|
|
16
18
|
attr_reader :checksums
|
|
17
19
|
attr_accessor :ignore_filter
|
|
18
20
|
|
|
21
|
+
def_delegators :@error_reporter, :has_any_error?
|
|
22
|
+
|
|
19
23
|
def initialize(ctx, theme:, ignore_filter: nil)
|
|
20
24
|
@ctx = ctx
|
|
21
25
|
@theme = theme
|
|
22
26
|
@ignore_filter = ignore_filter
|
|
27
|
+
@error_reporter = ErrorReporter.new(ctx)
|
|
28
|
+
@standard_reporter = StandardReporter.new(ctx)
|
|
29
|
+
@reporters = [@error_reporter, @standard_reporter]
|
|
23
30
|
|
|
24
31
|
# Queue of `Operation`s waiting to be picked up from a thread for processing.
|
|
25
32
|
@queue = Queue.new
|
|
@@ -30,12 +37,19 @@ module ShopifyCLI
|
|
|
30
37
|
# Mutex used to pause all threads when backing-off when hitting API rate limits
|
|
31
38
|
@backoff_mutex = Mutex.new
|
|
32
39
|
|
|
33
|
-
# Allows delaying log of errors, mainly to not break the progress bar.
|
|
34
|
-
@delay_errors = false
|
|
35
|
-
@delayed_errors = []
|
|
36
|
-
|
|
37
40
|
# Latest theme assets checksums. Updated on each upload.
|
|
38
41
|
@checksums = {}
|
|
42
|
+
|
|
43
|
+
# Checksums of assets with errors.
|
|
44
|
+
@error_checksums = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def lock_io!
|
|
48
|
+
@reporters.each { |reporter| reporter.disable! }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unlock_io!
|
|
52
|
+
@reporters.each { |reporter| reporter.enable! }
|
|
39
53
|
end
|
|
40
54
|
|
|
41
55
|
def enqueue_updates(files)
|
|
@@ -103,25 +117,14 @@ module ShopifyCLI
|
|
|
103
117
|
break if operation.nil? # shutdown was called
|
|
104
118
|
perform(operation)
|
|
105
119
|
rescue Exception => e
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
120
|
+
error_suffix = ": #{e}"
|
|
121
|
+
error_suffix += + "\n\t#{e.backtrace.join("\n\t")}" if @ctx.debug?
|
|
122
|
+
report_error(operation, error_suffix)
|
|
110
123
|
end
|
|
111
124
|
end
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
|
|
115
|
-
def delay_errors!
|
|
116
|
-
@delay_errors = true
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def report_errors!
|
|
120
|
-
@delay_errors = false
|
|
121
|
-
@delayed_errors.each { |error| report_error(error) }
|
|
122
|
-
@delayed_errors.clear
|
|
123
|
-
end
|
|
124
|
-
|
|
125
128
|
def upload_theme!(delay_low_priority_files: false, delete: true, &block)
|
|
126
129
|
fetch_checksums!
|
|
127
130
|
|
|
@@ -177,21 +180,27 @@ module ShopifyCLI
|
|
|
177
180
|
|
|
178
181
|
private
|
|
179
182
|
|
|
183
|
+
def report_error(operation, error_suffix = "")
|
|
184
|
+
@error_checksums << @checksums[operation.file_path]
|
|
185
|
+
@error_reporter.report("#{operation.as_error_message}#{error_suffix}")
|
|
186
|
+
end
|
|
187
|
+
|
|
180
188
|
def enqueue(method, file)
|
|
181
189
|
raise ArgumentError, "file required" unless file
|
|
182
190
|
|
|
183
|
-
operation = Operation.new(method, @theme[file])
|
|
191
|
+
operation = Operation.new(@ctx, method, @theme[file])
|
|
184
192
|
|
|
185
193
|
# Already enqueued
|
|
186
194
|
return if @pending.include?(operation)
|
|
187
195
|
|
|
188
|
-
if @ignore_filter&.ignore?(operation.
|
|
189
|
-
@ctx.debug("ignore #{operation.
|
|
196
|
+
if @ignore_filter&.ignore?(operation.file_path)
|
|
197
|
+
@ctx.debug("ignore #{operation.file_path}")
|
|
190
198
|
return
|
|
191
199
|
end
|
|
192
200
|
|
|
193
201
|
if [:update, :get].include?(method) && operation.file.exist? && !file_has_changed?(operation.file)
|
|
194
|
-
|
|
202
|
+
is_fixed = !!@error_checksums.delete(operation.file.checksum)
|
|
203
|
+
@standard_reporter.report(operation.as_fix_message) if is_fixed
|
|
195
204
|
return
|
|
196
205
|
end
|
|
197
206
|
|
|
@@ -206,16 +215,16 @@ module ShopifyCLI
|
|
|
206
215
|
|
|
207
216
|
response = send(operation.method, operation.file)
|
|
208
217
|
|
|
218
|
+
@standard_reporter.report(operation.as_synced_message)
|
|
219
|
+
|
|
209
220
|
# Check if the API told us we're near the rate limit
|
|
210
221
|
if !backingoff? && (limit = response["x-shopify-shop-api-call-limit"])
|
|
211
222
|
used, total = limit.split("/").map(&:to_i)
|
|
212
223
|
backoff_if_near_limit!(used, total)
|
|
213
224
|
end
|
|
214
225
|
rescue ShopifyCLI::API::APIRequestError => e
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
parse_api_errors(e).join("\n ")
|
|
218
|
-
)
|
|
226
|
+
error_suffix = ":\n " + parse_api_errors(e).join("\n ")
|
|
227
|
+
report_error(operation, error_suffix)
|
|
219
228
|
ensure
|
|
220
229
|
@pending.delete(operation)
|
|
221
230
|
end
|
|
@@ -295,14 +304,6 @@ module ShopifyCLI
|
|
|
295
304
|
file.checksum != @checksums[file.relative_path.to_s]
|
|
296
305
|
end
|
|
297
306
|
|
|
298
|
-
def report_error(error)
|
|
299
|
-
if @delay_errors
|
|
300
|
-
@delayed_errors << error
|
|
301
|
-
else
|
|
302
|
-
@ctx.puts(error)
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
|
|
306
307
|
def parse_api_errors(exception)
|
|
307
308
|
parsed_body = JSON.parse(exception&.response&.body)
|
|
308
309
|
message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
|
|
@@ -162,26 +162,38 @@ module ShopifyCLI
|
|
|
162
162
|
}
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
ctx
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
class << self
|
|
166
|
+
def all(ctx, root: nil)
|
|
167
|
+
_status, body = fetch_themes(ctx)
|
|
168
|
+
|
|
169
|
+
body["themes"]
|
|
170
|
+
.sort_by { |theme_attrs| Time.parse(theme_attrs["updated_at"]) }
|
|
171
|
+
.reverse
|
|
172
|
+
.map { |theme_attrs| new(ctx, root: root, **allowed_attrs(theme_attrs)) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def live(ctx, root: nil)
|
|
176
|
+
_status, body = fetch_themes(ctx)
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
178
|
+
body["themes"]
|
|
179
|
+
.find { |theme_attrs| theme_attrs["role"] == "main" }
|
|
180
|
+
.tap { |theme_attrs| break new(ctx, root: root, **allowed_attrs(theme_attrs)) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def allowed_attrs(attrs)
|
|
186
|
+
attrs.slice("id", "name", "role").transform_keys(&:to_sym)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def fetch_themes(ctx)
|
|
190
|
+
AdminAPI.rest_request(
|
|
191
|
+
ctx,
|
|
192
|
+
shop: AdminAPI.get_shop_or_abort(ctx),
|
|
193
|
+
path: "themes.json",
|
|
194
|
+
api_version: "unstable",
|
|
195
|
+
)
|
|
196
|
+
end
|
|
185
197
|
end
|
|
186
198
|
|
|
187
199
|
private
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
class ThreadPool
|
|
5
|
+
class Job
|
|
6
|
+
attr_reader :error
|
|
7
|
+
|
|
8
|
+
def perform!
|
|
9
|
+
raise "`#{self.class.name}#perform!` must be defined"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
perform!
|
|
14
|
+
rescue StandardError => error
|
|
15
|
+
@error = error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def success?
|
|
19
|
+
!@error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def error?
|
|
23
|
+
!!@error
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyCLI
|
|
4
|
+
class ThreadPool
|
|
5
|
+
attr_reader :errors
|
|
6
|
+
|
|
7
|
+
def initialize(pool_size: 10)
|
|
8
|
+
@jobs = Queue.new
|
|
9
|
+
@pool = Array.new(pool_size) { spawn_thread }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def schedule(job)
|
|
13
|
+
@jobs << job
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def shutdown
|
|
17
|
+
@pool.size.times do
|
|
18
|
+
schedule(-> { throw(:stop_thread) })
|
|
19
|
+
end
|
|
20
|
+
@pool.map(&:join)
|
|
21
|
+
ensure
|
|
22
|
+
@jobs.close
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def spawn_thread
|
|
28
|
+
Thread.new do
|
|
29
|
+
catch(:stop_thread) do
|
|
30
|
+
loop do
|
|
31
|
+
@jobs.pop.call
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|