shopify-cli 1.13.0 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -1
- data/.github/CONTRIBUTING.md +7 -7
- data/.github/DESIGN.md +3 -3
- data/.github/workflows/build.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +57 -24
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/LICENSE +4 -1
- data/README.md +94 -26
- data/RELEASING.md +31 -7
- data/Rakefile +2 -2
- data/SECURITY.md +1 -1
- data/THEMEKIT_MIGRATION.md +18 -0
- data/bin/load_shopify.rb +1 -1
- data/bin/shopify +3 -3
- data/dev.yml +1 -1
- data/docs/app/node/index.md +1 -1
- data/docs/app/rails/index.md +1 -1
- data/docs/core/index.md +1 -1
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/install/index.md +1 -1
- data/docs/getting-started/migrate/index.md +1 -1
- data/docs/getting-started/uninstall/index.md +1 -1
- data/docs/getting-started/upgrade/index.md +1 -1
- data/docs/help/start-app/index.md +1 -1
- data/docs/index.md +1 -1
- data/ext/shopify-cli/extconf.rb +17 -5
- data/install.sh +1 -1
- data/lib/docgen/index_template.md.erb +2 -2
- data/lib/graphql/all_orgs_with_extensions.graphql +37 -0
- data/lib/graphql/api_versions.graphql +1 -1
- data/lib/graphql/find_organization.graphql +2 -1
- data/lib/project_types/extension/cli.rb +18 -15
- data/lib/project_types/extension/commands/build.rb +4 -5
- data/lib/project_types/extension/commands/connect.rb +35 -0
- data/lib/project_types/extension/commands/create.rb +12 -16
- data/lib/project_types/extension/commands/extension_command.rb +2 -2
- data/lib/project_types/extension/commands/info.rb +86 -0
- data/lib/project_types/extension/commands/push.rb +8 -7
- data/lib/project_types/extension/commands/register.rb +4 -5
- data/lib/project_types/extension/commands/serve.rb +5 -8
- data/lib/project_types/extension/commands/tunnel.rb +3 -1
- data/lib/project_types/extension/errors.rb +9 -0
- data/lib/project_types/extension/extension_project.rb +17 -1
- data/lib/project_types/extension/extension_project_keys.rb +1 -0
- data/lib/project_types/extension/features/argo.rb +6 -6
- data/lib/project_types/extension/features/argo_runtime.rb +22 -56
- data/lib/project_types/extension/features/argo_serve.rb +25 -18
- data/lib/project_types/extension/forms/connect.rb +42 -0
- data/lib/project_types/extension/forms/questions/ask_name.rb +14 -6
- data/lib/project_types/extension/forms/questions/ask_registration.rb +51 -0
- data/lib/project_types/extension/messages/messages.rb +80 -16
- data/lib/project_types/extension/models/specification.rb +1 -0
- data/lib/project_types/extension/models/specification_handlers/{checkout_argo_extension.rb → checkout_ui_extension.rb} +3 -1
- data/lib/project_types/extension/models/specification_handlers/default.rb +13 -3
- data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +89 -0
- data/lib/project_types/extension/models/specifications.rb +1 -0
- data/lib/project_types/extension/tasks/configure_features.rb +6 -7
- data/lib/project_types/extension/tasks/configure_options.rb +20 -0
- data/lib/project_types/extension/tasks/get_extensions.rb +32 -0
- data/lib/project_types/node/cli.rb +9 -21
- data/lib/project_types/node/commands/connect.rb +8 -2
- data/lib/project_types/node/commands/create.rb +9 -5
- data/lib/project_types/node/commands/deploy.rb +15 -5
- data/lib/project_types/node/commands/deploy/heroku.rb +29 -29
- data/lib/project_types/node/commands/generate.rb +4 -2
- data/lib/project_types/node/commands/open.rb +4 -2
- data/lib/project_types/node/commands/serve.rb +3 -2
- data/lib/project_types/node/commands/tunnel.rb +4 -2
- data/lib/project_types/node/messages/messages.rb +47 -90
- data/lib/project_types/rails/cli.rb +9 -21
- data/lib/project_types/rails/commands/connect.rb +8 -2
- data/lib/project_types/rails/commands/create.rb +10 -6
- data/lib/project_types/rails/commands/deploy.rb +15 -5
- data/lib/project_types/rails/commands/deploy/heroku.rb +84 -82
- data/lib/project_types/rails/commands/generate.rb +15 -5
- data/lib/project_types/rails/commands/generate/webhook.rb +28 -26
- data/lib/project_types/rails/commands/open.rb +4 -2
- data/lib/project_types/rails/commands/serve.rb +3 -2
- data/lib/project_types/rails/commands/tunnel.rb +4 -2
- data/lib/project_types/rails/messages/messages.rb +72 -119
- data/lib/project_types/script/cli.rb +6 -8
- data/lib/project_types/script/commands/create.rb +3 -1
- data/lib/project_types/script/commands/push.rb +12 -5
- data/lib/project_types/script/graphql/app_script_update_or_create.graphql +9 -3
- data/lib/project_types/script/layers/application/create_script.rb +4 -3
- data/lib/project_types/script/layers/domain/errors.rb +6 -11
- data/lib/project_types/script/layers/domain/push_package.rb +4 -8
- data/lib/project_types/script/layers/domain/script_json.rb +32 -0
- data/lib/project_types/script/layers/domain/script_project.rb +1 -1
- data/lib/project_types/script/layers/infrastructure/errors.rb +13 -17
- data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +29 -21
- data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +2 -4
- data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +45 -34
- data/lib/project_types/script/layers/infrastructure/script_service.rb +37 -16
- data/lib/project_types/script/messages/messages.rb +66 -55
- data/lib/project_types/script/tasks/ensure_env.rb +22 -1
- data/lib/project_types/script/ui/error_handler.rb +32 -32
- data/lib/project_types/theme/cli.rb +16 -27
- data/lib/project_types/theme/commands/check.rb +33 -0
- data/lib/project_types/theme/commands/delete.rb +64 -0
- data/lib/project_types/theme/commands/init.rb +42 -0
- data/lib/project_types/theme/commands/language_server.rb +16 -0
- data/lib/project_types/theme/commands/package.rb +55 -0
- data/lib/project_types/theme/commands/publish.rb +43 -0
- data/lib/project_types/theme/commands/pull.rb +51 -0
- data/lib/project_types/theme/commands/push.rb +58 -32
- data/lib/project_types/theme/commands/serve.rb +8 -16
- data/lib/project_types/theme/forms/confirm_store.rb +15 -0
- data/lib/project_types/theme/forms/select.rb +59 -0
- data/lib/project_types/theme/messages/messages.rb +118 -103
- data/lib/project_types/theme/ui/sync_progress_bar.rb +20 -0
- data/lib/shopify-cli/admin_api.rb +57 -38
- data/lib/shopify-cli/admin_api/populate_resource_command.rb +6 -14
- data/lib/shopify-cli/admin_api/schema.rb +1 -10
- data/lib/shopify-cli/api.rb +29 -14
- data/lib/shopify-cli/command.rb +15 -3
- data/lib/shopify-cli/commands.rb +7 -2
- data/lib/shopify-cli/commands/help.rb +2 -29
- data/lib/shopify-cli/commands/login.rb +95 -0
- data/lib/shopify-cli/commands/logout.rb +24 -8
- data/lib/shopify-cli/commands/populate.rb +23 -0
- data/lib/{project_types/node → shopify-cli}/commands/populate/customer.rb +2 -8
- data/lib/{project_types/node → shopify-cli}/commands/populate/draft_order.rb +2 -2
- data/lib/{project_types/node → shopify-cli}/commands/populate/product.rb +2 -8
- data/lib/shopify-cli/commands/store.rb +15 -0
- data/lib/shopify-cli/commands/switch.rb +39 -0
- data/lib/shopify-cli/commands/system.rb +12 -0
- data/lib/shopify-cli/commands/whoami.rb +28 -0
- data/lib/shopify-cli/connect.rb +32 -0
- data/lib/shopify-cli/context.rb +65 -4
- data/lib/shopify-cli/core/entry_point.rb +3 -22
- data/lib/shopify-cli/core/monorail.rb +6 -2
- data/lib/shopify-cli/db.rb +4 -4
- data/lib/shopify-cli/http_request.rb +16 -0
- data/lib/shopify-cli/identity_auth.rb +282 -0
- data/lib/shopify-cli/{oauth → identity_auth}/servlet.rb +11 -12
- data/lib/shopify-cli/messages/messages.rb +140 -46
- data/lib/shopify-cli/packager.rb +5 -5
- data/lib/shopify-cli/partners_api.rb +21 -44
- data/lib/shopify-cli/partners_api/organizations.rb +8 -0
- data/lib/shopify-cli/project_commands.rb +16 -0
- data/lib/shopify-cli/project_type.rb +0 -31
- data/lib/shopify-cli/shopifolk.rb +8 -11
- data/lib/shopify-cli/sub_command.rb +1 -0
- data/lib/shopify-cli/tasks.rb +3 -0
- data/lib/shopify-cli/tasks/confirm_store.rb +18 -0
- data/lib/shopify-cli/tasks/create_api_client.rb +2 -2
- data/lib/shopify-cli/tasks/ensure_authenticated.rb +13 -0
- data/lib/shopify-cli/tasks/ensure_loopback_url.rb +1 -1
- data/lib/shopify-cli/tasks/ensure_project_type.rb +12 -0
- data/lib/shopify-cli/tasks/select_org_and_shop.rb +0 -3
- data/lib/shopify-cli/theme/dev_server.rb +98 -0
- data/lib/shopify-cli/theme/dev_server/certificate_manager.rb +79 -0
- data/lib/shopify-cli/theme/dev_server/header_hash.rb +94 -0
- data/lib/shopify-cli/theme/dev_server/hot-reload.js +93 -0
- data/lib/shopify-cli/theme/dev_server/hot_reload.rb +76 -0
- data/lib/shopify-cli/theme/dev_server/local_assets.rb +87 -0
- data/lib/shopify-cli/theme/dev_server/proxy.rb +205 -0
- data/lib/shopify-cli/theme/dev_server/sse.rb +75 -0
- data/lib/shopify-cli/theme/dev_server/watcher.rb +59 -0
- data/lib/shopify-cli/theme/dev_server/web_server.rb +140 -0
- data/lib/shopify-cli/theme/development_theme.rb +69 -0
- data/lib/shopify-cli/theme/file.rb +112 -0
- data/lib/shopify-cli/theme/ignore_filter.rb +109 -0
- data/lib/shopify-cli/theme/mime_type.rb +34 -0
- data/lib/shopify-cli/theme/syncer.rb +332 -0
- data/lib/shopify-cli/theme/theme.rb +204 -0
- data/lib/shopify-cli/tunnel.rb +1 -1
- data/lib/shopify-cli/version.rb +1 -1
- data/lib/shopify_cli.rb +18 -11
- data/shopify-cli.gemspec +12 -5
- data/shopify.fish +1 -1
- data/shopify.sh +1 -1
- metadata +91 -35
- data/.github/workflows/release.yml +0 -59
- data/lib/project_types/extension/features/argo_serve_options.rb +0 -41
- data/lib/project_types/node/commands/populate.rb +0 -23
- data/lib/project_types/rails/commands/populate.rb +0 -23
- data/lib/project_types/rails/commands/populate/customer.rb +0 -31
- data/lib/project_types/rails/commands/populate/draft_order.rb +0 -28
- data/lib/project_types/rails/commands/populate/product.rb +0 -30
- data/lib/project_types/script/layers/domain/config_ui.rb +0 -16
- data/lib/project_types/theme/commands/connect.rb +0 -54
- data/lib/project_types/theme/commands/create.rb +0 -48
- data/lib/project_types/theme/commands/deploy.rb +0 -38
- data/lib/project_types/theme/commands/generate.rb +0 -20
- data/lib/project_types/theme/commands/generate/env.rb +0 -79
- data/lib/project_types/theme/forms/connect.rb +0 -34
- data/lib/project_types/theme/forms/create.rb +0 -22
- data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +0 -78
- data/lib/project_types/theme/themekit.rb +0 -113
- data/lib/shopify-cli/commands/connect.rb +0 -64
- data/lib/shopify-cli/commands/create.rb +0 -50
- data/lib/shopify-cli/oauth.rb +0 -198
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyCli
|
4
|
+
module Theme
|
5
|
+
class IgnoreFilter
|
6
|
+
FILE = ".shopifyignore"
|
7
|
+
|
8
|
+
DEFAULT_REGEXES = [
|
9
|
+
/\.git/,
|
10
|
+
/\.hg/,
|
11
|
+
/\.bzr/,
|
12
|
+
/\.svn/,
|
13
|
+
/_darcs/,
|
14
|
+
/CVS/,
|
15
|
+
/\.sublime-(project|workspace)/,
|
16
|
+
/\.DS_Store/,
|
17
|
+
/\.sass-cache/,
|
18
|
+
/Thumbs\.db/,
|
19
|
+
/desktop\.ini/,
|
20
|
+
/config.yml/,
|
21
|
+
/node_modules/,
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
DEFAULT_GLOBS = [].freeze
|
25
|
+
|
26
|
+
attr_reader :root, :globs, :regexes
|
27
|
+
|
28
|
+
def self.from_path(root)
|
29
|
+
root = Pathname.new(root)
|
30
|
+
ignore_file = root.join(FILE)
|
31
|
+
patterns = if ignore_file.file?
|
32
|
+
parse_ignore_file(ignore_file)
|
33
|
+
else
|
34
|
+
[]
|
35
|
+
end
|
36
|
+
new(root, patterns: patterns)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parse_ignore_file(file)
|
40
|
+
patterns = []
|
41
|
+
|
42
|
+
file.each_line do |line|
|
43
|
+
line.strip!
|
44
|
+
|
45
|
+
next if line.empty? || line.start_with?("#")
|
46
|
+
|
47
|
+
patterns << line
|
48
|
+
end
|
49
|
+
|
50
|
+
patterns
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(root, patterns: [])
|
54
|
+
@root = root
|
55
|
+
|
56
|
+
regexes, globs = patterns_to_regexes_and_globs(patterns)
|
57
|
+
|
58
|
+
@regexes = regexes
|
59
|
+
@globs = globs
|
60
|
+
end
|
61
|
+
|
62
|
+
def match?(path)
|
63
|
+
path = path.to_s
|
64
|
+
|
65
|
+
return true if path.empty?
|
66
|
+
|
67
|
+
regexes.each do |regex|
|
68
|
+
return true if regex.match(path)
|
69
|
+
end
|
70
|
+
|
71
|
+
globs.each do |glob|
|
72
|
+
return true if ::File.fnmatch?(glob, path)
|
73
|
+
end
|
74
|
+
|
75
|
+
false
|
76
|
+
end
|
77
|
+
alias_method :ignore?, :match?
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Take in string patterns and convert them to either
|
82
|
+
# regex patterns or glob patterns so that they are handled in an expected manner.
|
83
|
+
def patterns_to_regexes_and_globs(patterns)
|
84
|
+
new_regexes = DEFAULT_REGEXES.dup
|
85
|
+
new_globs = DEFAULT_GLOBS.dup
|
86
|
+
|
87
|
+
patterns.each do |pattern|
|
88
|
+
pattern = pattern.strip
|
89
|
+
|
90
|
+
if pattern.start_with?("/") && pattern.end_with?("/")
|
91
|
+
new_regexes << Regexp.new(pattern.gsub(%r{^\/|\/$}, ""))
|
92
|
+
next
|
93
|
+
end
|
94
|
+
|
95
|
+
# if specifying a directory, match everything below it
|
96
|
+
pattern += "*" if pattern.end_with?("/")
|
97
|
+
|
98
|
+
# The pattern will be scoped to root directory, so it should match anything
|
99
|
+
# within that space
|
100
|
+
pattern.prepend("*") unless pattern.start_with?("*")
|
101
|
+
|
102
|
+
new_globs << pattern
|
103
|
+
end
|
104
|
+
|
105
|
+
[new_regexes, new_globs]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "webrick"
|
3
|
+
|
4
|
+
module ShopifyCli
|
5
|
+
module Theme
|
6
|
+
class MimeType < Struct.new(:name)
|
7
|
+
MIME_TYPES = WEBrick::HTTPUtils::DefaultMimeTypes.merge(
|
8
|
+
"liquid" => "text/x-liquid",
|
9
|
+
)
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def by_filename(filename)
|
13
|
+
new(WEBrick::HTTPUtils.mime_type(filename.to_s, MIME_TYPES))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def text?
|
18
|
+
/text/.match?(name) || json? || javascript?
|
19
|
+
end
|
20
|
+
|
21
|
+
def json?
|
22
|
+
name == "application/json"
|
23
|
+
end
|
24
|
+
|
25
|
+
def javascript?
|
26
|
+
name == "application/javascript"
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "thread"
|
3
|
+
require "json"
|
4
|
+
require "base64"
|
5
|
+
|
6
|
+
module ShopifyCli
|
7
|
+
module Theme
|
8
|
+
class Syncer
|
9
|
+
class Operation < Struct.new(:method, :file)
|
10
|
+
def to_s
|
11
|
+
"#{method} #{file&.relative_path}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
API_VERSION = "unstable"
|
15
|
+
|
16
|
+
attr_reader :checksums
|
17
|
+
attr_accessor :ignore_filter
|
18
|
+
|
19
|
+
def initialize(ctx, theme:, ignore_filter: nil)
|
20
|
+
@ctx = ctx
|
21
|
+
@theme = theme
|
22
|
+
@ignore_filter = ignore_filter
|
23
|
+
|
24
|
+
# Queue of `Operation`s waiting to be picked up from a thread for processing.
|
25
|
+
@queue = Queue.new
|
26
|
+
# `Operation`s will be removed from this Array completed.
|
27
|
+
@pending = []
|
28
|
+
# Thread making the API requests.
|
29
|
+
@threads = []
|
30
|
+
# Mutex used to pause all threads when backing-off when hitting API rate limits
|
31
|
+
@backoff_mutex = Mutex.new
|
32
|
+
|
33
|
+
# Allows delaying log of errors, mainly to not break the progress bar.
|
34
|
+
@delay_errors = false
|
35
|
+
@delayed_errors = []
|
36
|
+
|
37
|
+
# Latest theme assets checksums. Updated on each upload.
|
38
|
+
@checksums = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def enqueue_updates(files)
|
42
|
+
files.each { |file| enqueue(:update, file) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def enqueue_get(files)
|
46
|
+
files.each { |file| enqueue(:get, file) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def enqueue_deletes(files)
|
50
|
+
files.each { |file| enqueue(:delete, file) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def size
|
54
|
+
@pending.size
|
55
|
+
end
|
56
|
+
|
57
|
+
def empty?
|
58
|
+
@pending.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def pending_updates
|
62
|
+
@pending.select { |op| op.method == :update }.map(&:file)
|
63
|
+
end
|
64
|
+
|
65
|
+
def remote_file?(file)
|
66
|
+
checksums.key?(@theme[file].relative_path.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
def wait!
|
70
|
+
raise ThreadError, "No syncer threads" if @threads.empty?
|
71
|
+
total = size
|
72
|
+
last_size = size
|
73
|
+
until empty? || @queue.closed?
|
74
|
+
if block_given? && last_size != size
|
75
|
+
yield size, total
|
76
|
+
last_size = size
|
77
|
+
end
|
78
|
+
Thread.pass
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def fetch_checksums!
|
83
|
+
_status, response = ShopifyCli::AdminAPI.rest_request(
|
84
|
+
@ctx,
|
85
|
+
shop: @theme.shop,
|
86
|
+
path: "themes/#{@theme.id}/assets.json",
|
87
|
+
api_version: API_VERSION,
|
88
|
+
)
|
89
|
+
update_checksums(response)
|
90
|
+
end
|
91
|
+
|
92
|
+
def shutdown
|
93
|
+
@queue.close unless @queue.closed?
|
94
|
+
ensure
|
95
|
+
@threads.each { |thread| thread.join if thread.alive? }
|
96
|
+
end
|
97
|
+
|
98
|
+
def start_threads(count = 2)
|
99
|
+
count.times do
|
100
|
+
@threads << Thread.new do
|
101
|
+
loop do
|
102
|
+
operation = @queue.pop
|
103
|
+
break if operation.nil? # shutdown was called
|
104
|
+
perform(operation)
|
105
|
+
rescue Exception => e
|
106
|
+
report_error(
|
107
|
+
"{{red:ERROR}} {{blue:#{operation}}}: #{e}" +
|
108
|
+
(@ctx.debug? ? "\n\t#{e.backtrace.join("\n\t")}" : "")
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
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
|
+
def upload_theme!(delay_low_priority_files: false, delete: true, &block)
|
126
|
+
fetch_checksums!
|
127
|
+
|
128
|
+
if delete
|
129
|
+
# Delete remote files not present locally
|
130
|
+
removed_files = checksums.keys - @theme.theme_files.map { |file| file.relative_path.to_s }
|
131
|
+
enqueue_deletes(removed_files)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Some files must be uploaded after the other ones
|
135
|
+
delayed_config_files = [
|
136
|
+
@theme["config/settings_schema.json"],
|
137
|
+
@theme["config/settings_data.json"],
|
138
|
+
]
|
139
|
+
|
140
|
+
enqueue_updates(@theme.liquid_files)
|
141
|
+
enqueue_updates(@theme.json_files - delayed_config_files)
|
142
|
+
enqueue_updates(delayed_config_files)
|
143
|
+
|
144
|
+
if delay_low_priority_files
|
145
|
+
# Wait for liquid & JSON files to upload, because those are rendered remotely
|
146
|
+
wait!(&block)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Process lower-priority files in the background
|
150
|
+
|
151
|
+
# Assets are served locally, so can be uploaded in the background
|
152
|
+
enqueue_updates(@theme.static_asset_files)
|
153
|
+
|
154
|
+
unless delay_low_priority_files
|
155
|
+
wait!(&block)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def download_theme!(delete: true, &block)
|
160
|
+
fetch_checksums!
|
161
|
+
|
162
|
+
if delete
|
163
|
+
# Delete local files not present remotely
|
164
|
+
missing_files = @theme.theme_files
|
165
|
+
.reject { |file| checksums.key?(file.relative_path.to_s) }.uniq
|
166
|
+
.reject { |file| @ignore_filter&.ignore?(file) }
|
167
|
+
missing_files.each do |file|
|
168
|
+
@ctx.debug("rm #{file.relative_path}")
|
169
|
+
file.delete
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
enqueue_get(checksums.keys)
|
174
|
+
|
175
|
+
wait!(&block)
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def enqueue(method, file)
|
181
|
+
raise ArgumentError, "file required" unless file
|
182
|
+
|
183
|
+
operation = Operation.new(method, @theme[file])
|
184
|
+
|
185
|
+
# Already enqueued
|
186
|
+
return if @pending.include?(operation)
|
187
|
+
|
188
|
+
if @ignore_filter&.ignore?(operation.file.relative_path)
|
189
|
+
@ctx.debug("ignore #{operation.file.relative_path}")
|
190
|
+
return
|
191
|
+
end
|
192
|
+
|
193
|
+
if [:update, :get].include?(method) && operation.file.exist? && !file_has_changed?(operation.file)
|
194
|
+
@ctx.debug("skip #{operation}")
|
195
|
+
return
|
196
|
+
end
|
197
|
+
|
198
|
+
@pending << operation
|
199
|
+
@queue << operation unless @queue.closed?
|
200
|
+
end
|
201
|
+
|
202
|
+
def perform(operation)
|
203
|
+
return if @queue.closed?
|
204
|
+
wait_for_backoff!
|
205
|
+
@ctx.debug(operation.to_s)
|
206
|
+
|
207
|
+
response = send(operation.method, operation.file)
|
208
|
+
|
209
|
+
# Check if the API told us we're near the rate limit
|
210
|
+
if !backingoff? && (limit = response["x-shopify-shop-api-call-limit"])
|
211
|
+
used, total = limit.split("/").map(&:to_i)
|
212
|
+
backoff_if_near_limit!(used, total)
|
213
|
+
end
|
214
|
+
rescue ShopifyCli::API::APIRequestError => e
|
215
|
+
report_error(
|
216
|
+
"{{red:ERROR}} {{blue:#{operation}}}:\n " +
|
217
|
+
parse_api_errors(e).join("\n ")
|
218
|
+
)
|
219
|
+
ensure
|
220
|
+
@pending.delete(operation)
|
221
|
+
end
|
222
|
+
|
223
|
+
def update(file)
|
224
|
+
asset = { key: file.relative_path.to_s }
|
225
|
+
if file.text?
|
226
|
+
asset[:value] = file.read
|
227
|
+
else
|
228
|
+
asset[:attachment] = Base64.encode64(file.read)
|
229
|
+
end
|
230
|
+
|
231
|
+
_status, body, response = ShopifyCli::AdminAPI.rest_request(
|
232
|
+
@ctx,
|
233
|
+
shop: @theme.shop,
|
234
|
+
path: "themes/#{@theme.id}/assets.json",
|
235
|
+
method: "PUT",
|
236
|
+
api_version: API_VERSION,
|
237
|
+
body: JSON.generate(asset: asset)
|
238
|
+
)
|
239
|
+
|
240
|
+
update_checksums(body)
|
241
|
+
|
242
|
+
response
|
243
|
+
end
|
244
|
+
|
245
|
+
def get(file)
|
246
|
+
_status, body, response = ShopifyCli::AdminAPI.rest_request(
|
247
|
+
@ctx,
|
248
|
+
shop: @theme.shop,
|
249
|
+
path: "themes/#{@theme.id}/assets.json",
|
250
|
+
method: "GET",
|
251
|
+
api_version: API_VERSION,
|
252
|
+
query: URI.encode_www_form("asset[key]" => file.relative_path.to_s),
|
253
|
+
)
|
254
|
+
|
255
|
+
update_checksums(body)
|
256
|
+
|
257
|
+
attachment = body.dig("asset", "attachment")
|
258
|
+
value = if attachment
|
259
|
+
file.write(Base64.decode64(attachment), 0, mode: "wb")
|
260
|
+
else
|
261
|
+
file.write(body.dig("asset", "value"))
|
262
|
+
end
|
263
|
+
|
264
|
+
response
|
265
|
+
end
|
266
|
+
|
267
|
+
def delete(file)
|
268
|
+
_status, _body, response = ShopifyCli::AdminAPI.rest_request(
|
269
|
+
@ctx,
|
270
|
+
shop: @theme.shop,
|
271
|
+
path: "themes/#{@theme.id}/assets.json",
|
272
|
+
method: "DELETE",
|
273
|
+
api_version: API_VERSION,
|
274
|
+
body: JSON.generate(asset: {
|
275
|
+
key: file.relative_path.to_s
|
276
|
+
})
|
277
|
+
)
|
278
|
+
|
279
|
+
response
|
280
|
+
end
|
281
|
+
|
282
|
+
def update_checksums(api_response)
|
283
|
+
api_response.values.flatten.each do |asset|
|
284
|
+
if asset["key"]
|
285
|
+
@checksums[asset["key"]] = asset["checksum"]
|
286
|
+
end
|
287
|
+
end
|
288
|
+
# Generate .liquid asset files are reported twice in checksum:
|
289
|
+
# once of generated, once for .liquid. We only keep the .liquid, that's the one we have
|
290
|
+
# on disk.
|
291
|
+
@checksums.reject! { |key, _| @checksums.key?("#{key}.liquid") }
|
292
|
+
end
|
293
|
+
|
294
|
+
def file_has_changed?(file)
|
295
|
+
file.checksum != @checksums[file.relative_path.to_s]
|
296
|
+
end
|
297
|
+
|
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
|
+
def parse_api_errors(exception)
|
307
|
+
parsed_body = JSON.parse(exception&.response&.body)
|
308
|
+
message = parsed_body.dig("errors", "asset") || parsed_body["message"] || exception.message
|
309
|
+
# Truncate to first lines
|
310
|
+
[message].flatten.map { |message| message.split("\n", 2).first }
|
311
|
+
rescue JSON::ParserError
|
312
|
+
[exception.message]
|
313
|
+
end
|
314
|
+
|
315
|
+
def backoff_if_near_limit!(used, limit)
|
316
|
+
if used > limit - @threads.size
|
317
|
+
@ctx.debug("Near API call limit, waiting 2 sec…")
|
318
|
+
@backoff_mutex.synchronize { sleep 2 }
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def backingoff?
|
323
|
+
@backoff_mutex.locked?
|
324
|
+
end
|
325
|
+
|
326
|
+
def wait_for_backoff!
|
327
|
+
# Sleeping in the mutex in another thread. Wait for unlock
|
328
|
+
@backoff_mutex.synchronize {} if backingoff?
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|