shopify-cli 2.1.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +0 -4
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile.lock +4 -4
  5. data/lib/graphql/get_variant_id.graphql +16 -0
  6. data/lib/project_types/extension/cli.rb +9 -1
  7. data/lib/project_types/extension/commands/push.rb +0 -1
  8. data/lib/project_types/extension/commands/serve.rb +8 -2
  9. data/lib/project_types/extension/extension_project.rb +21 -1
  10. data/lib/project_types/extension/features/argo.rb +1 -11
  11. data/lib/project_types/extension/features/argo_runtime.rb +6 -38
  12. data/lib/project_types/extension/features/argo_serve.rb +30 -1
  13. data/lib/project_types/extension/features/runtimes/admin.rb +29 -0
  14. data/lib/project_types/extension/features/runtimes/base.rb +19 -0
  15. data/lib/project_types/extension/features/runtimes/checkout_post_purchase.rb +23 -0
  16. data/lib/project_types/extension/features/runtimes/checkout_ui_extension.rb +29 -0
  17. data/lib/project_types/extension/messages/messages.rb +9 -0
  18. data/lib/project_types/extension/models/product.rb +12 -0
  19. data/lib/project_types/extension/models/specification_handlers/checkout_post_purchase.rb +10 -0
  20. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +11 -3
  21. data/lib/project_types/extension/models/specification_handlers/default.rb +13 -4
  22. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +12 -0
  23. data/lib/project_types/extension/tasks/configure_features.rb +1 -0
  24. data/lib/project_types/extension/tasks/converters/product_converter.rb +21 -0
  25. data/lib/project_types/extension/tasks/get_product.rb +22 -0
  26. data/lib/project_types/script/commands/create.rb +1 -1
  27. data/lib/project_types/script/graphql/app_script_set.graphql +40 -0
  28. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +0 -44
  29. data/lib/project_types/script/graphql/module_upload_url_generate.graphql +9 -0
  30. data/lib/project_types/script/layers/domain/push_package.rb +1 -2
  31. data/lib/project_types/script/layers/infrastructure/errors.rb +2 -0
  32. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +0 -1
  33. data/lib/project_types/script/layers/infrastructure/script_service.rb +95 -45
  34. data/lib/project_types/script/messages/messages.rb +3 -0
  35. data/lib/project_types/script/ui/error_handler.rb +5 -0
  36. data/lib/project_types/theme/commands/pull.rb +6 -0
  37. data/lib/project_types/theme/commands/push.rb +6 -0
  38. data/lib/project_types/theme/forms/select.rb +1 -1
  39. data/lib/shopify-cli/core/monorail.rb +2 -1
  40. data/lib/shopify-cli/process_supervision.rb +14 -14
  41. data/lib/shopify-cli/theme/dev_server/header_hash.rb +4 -0
  42. data/lib/shopify-cli/theme/dev_server/proxy.rb +13 -1
  43. data/lib/shopify-cli/theme/file.rb +11 -3
  44. data/lib/shopify-cli/theme/ignore_filter.rb +7 -0
  45. data/lib/shopify-cli/theme/syncer.rb +1 -1
  46. data/lib/shopify-cli/version.rb +1 -1
  47. data/shopify-cli.gemspec +1 -1
  48. metadata +14 -4
@@ -0,0 +1,29 @@
1
+ module Extension
2
+ module Features
3
+ module Runtimes
4
+ class CheckoutUiExtension < Base
5
+ CHECKOUT_UI_EXTENSIONS_RUN = "@shopify/checkout-ui-extensions-run"
6
+
7
+ IDENTIFIERS = [
8
+ "CHECKOUT_ARGO_EXTENSION",
9
+ "CHECKOUT_UI_EXTENSION",
10
+ ]
11
+
12
+ AVAILABLE_FLAGS = [
13
+ :port,
14
+ :public_url,
15
+ :resource_url,
16
+ :shop,
17
+ ]
18
+
19
+ def available_flags
20
+ AVAILABLE_FLAGS
21
+ end
22
+
23
+ def active_runtime?(cli_package, identifier)
24
+ cli_package.name == CHECKOUT_UI_EXTENSIONS_RUN && IDENTIFIERS.include?(identifier)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -166,12 +166,18 @@ module Extension
166
166
  tasks: {
167
167
  errors: {
168
168
  parse_error: "Unable to parse response from Partners Dashboard.",
169
+ store_error: "There was an error getting store data. Try again later.",
169
170
  },
170
171
  },
171
172
  errors: {
172
173
  unknown_type: "Unknown extension type %s",
173
174
  package_not_found: "`%s` package not found.",
174
175
  },
176
+ warnings: {
177
+ resource_url_auto_generation_failed: "{{*}} {{yellow:Warning:}} Unable to auto generate " \
178
+ "the extension resource URL because %s does not have any products. " \
179
+ "Please run {{bold:shopify populate products}} to generate sample products.",
180
+ },
175
181
  }
176
182
 
177
183
  TYPES = {
@@ -200,6 +206,9 @@ module Extension
200
206
  {{*}} You’re ready to start building {{green:%s}}!
201
207
  MESSAGE
202
208
  },
209
+ serve: {
210
+ unsupported: "shopify extension serve is not supported for theme app extensions",
211
+ },
203
212
  },
204
213
  },
205
214
  }
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Extension
4
+ module Models
5
+ class Product
6
+ include SmartProperties
7
+
8
+ property :variant_id, accepts: Integer, converts: :to_i
9
+ property :quantity, accepts: Integer, default: 1
10
+ end
11
+ end
12
+ end
@@ -6,6 +6,7 @@ module Extension
6
6
  module SpecificationHandlers
7
7
  class CheckoutPostPurchase < Default
8
8
  PERMITTED_CONFIG_KEYS = [:metafields]
9
+ RENDERER_PACKAGE_NAME = "@shopify/post-purchase-ui-extensions"
9
10
 
10
11
  def config(context)
11
12
  {
@@ -13,6 +14,15 @@ module Extension
13
14
  **argo.config(context),
14
15
  }
15
16
  end
17
+
18
+ protected
19
+
20
+ def argo
21
+ Features::Argo.new(
22
+ git_template: specification.features.argo.git_template,
23
+ renderer_package_name: RENDERER_PACKAGE_NAME
24
+ )
25
+ end
16
26
  end
17
27
  end
18
28
  end
@@ -4,7 +4,7 @@ module Extension
4
4
  module Models
5
5
  module SpecificationHandlers
6
6
  class CheckoutUiExtension < Default
7
- PERMITTED_CONFIG_KEYS = [:metafields, :extension_points]
7
+ PERMITTED_CONFIG_KEYS = [:extension_points, :metafields, :name]
8
8
 
9
9
  def config(context)
10
10
  {
@@ -12,9 +12,17 @@ module Extension
12
12
  **argo.config(context),
13
13
  }
14
14
  end
15
- end
16
15
 
17
- CheckoutArgoExtension = CheckoutUiExtension
16
+ def supplies_resource_url?
17
+ true
18
+ end
19
+
20
+ def build_resource_url(context:, shop:)
21
+ product = Tasks::GetProduct.call(context, shop)
22
+ return unless product
23
+ format("/cart/%<variant_id>d:%<quantity>d", variant_id: product.variant_id, quantity: 1)
24
+ end
25
+ end
18
26
  end
19
27
  end
20
28
  end
@@ -50,13 +50,14 @@ module Extension
50
50
  argo_runtime(context).supports?(:public_url)
51
51
  end
52
52
 
53
- def serve(context:, port:, tunnel_url:)
53
+ def serve(context:, port:, tunnel_url:, resource_url:)
54
54
  Features::ArgoServe.new(
55
55
  specification_handler: self,
56
56
  argo_runtime: argo_runtime(context),
57
57
  context: context,
58
58
  port: port,
59
59
  tunnel_url: tunnel_url,
60
+ resource_url: resource_url
60
61
  ).call
61
62
  end
62
63
 
@@ -65,9 +66,9 @@ module Extension
65
66
  end
66
67
 
67
68
  def argo_runtime(context)
68
- @argo_runtime ||= Features::ArgoRuntime.new(
69
- renderer: renderer_package(context),
70
- cli: cli_package(context),
69
+ @argo_runtime ||= Features::ArgoRuntime.find(
70
+ cli_package: cli_package(context),
71
+ identifier: identifier
71
72
  )
72
73
  end
73
74
 
@@ -90,6 +91,14 @@ module Extension
90
91
  end
91
92
  end
92
93
 
94
+ def supplies_resource_url?
95
+ false
96
+ end
97
+
98
+ def build_resource_url(shop)
99
+ raise NotImplementedError
100
+ end
101
+
93
102
  protected
94
103
 
95
104
  def argo
@@ -60,6 +60,18 @@ module Extension
60
60
  "Theme App Extension"
61
61
  end
62
62
 
63
+ def choose_port?(ctx)
64
+ ctx.abort(ctx.message("serve.unsupported"))
65
+ end
66
+
67
+ def establish_tunnel?(ctx)
68
+ ctx.abort(ctx.message("serve.unsupported"))
69
+ end
70
+
71
+ def serve(ctx)
72
+ ctx.abort(ctx.message("serve.unsupported"))
73
+ end
74
+
63
75
  private
64
76
 
65
77
  def validate(filename)
@@ -46,6 +46,7 @@ module Extension
46
46
  checkout: {
47
47
  git_template: "https://github.com/Shopify/checkout-ui-extensions-template",
48
48
  renderer_package_name: "@shopify/checkout-ui-extensions",
49
+ required_fields: [:shop],
49
50
  cli_package_name: "@shopify/checkout-ui-extensions-run",
50
51
  },
51
52
  }
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require "shopify_cli"
3
+
4
+ module Extension
5
+ module Tasks
6
+ module Converters
7
+ module ProductConverter
8
+ VARIANT_PATH = ["data", "products", "edges", 0, "node", "variants", "edges", 0, "node", "id"]
9
+
10
+ def self.from_hash(hash)
11
+ return nil if hash.nil?
12
+ variant = hash.dig(*VARIANT_PATH)
13
+ return unless variant
14
+ Models::Product.new(
15
+ variant_id: ShopifyCli::API.gid_to_id(variant)
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ require "shopify_cli"
3
+
4
+ module Extension
5
+ module Tasks
6
+ class GetProduct < ShopifyCli::Task
7
+ API_VERSION = "2021-07"
8
+ GRAPHQL_FILE = "get_variant_id"
9
+
10
+ def call(context, shop)
11
+ response = ShopifyCli::AdminAPI.query(
12
+ context,
13
+ GRAPHQL_FILE,
14
+ shop: shop,
15
+ api_version: API_VERSION
16
+ )
17
+ context.abort(context.message("tasks.errors.store_error")) if response.nil?
18
+ Converters::ProductConverter.from_hash(response)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -34,7 +34,7 @@ module Script
34
34
  end
35
35
 
36
36
  def self.help
37
- allowed_values = Script::Layers::Application::ExtensionPoints.types.map { |type| "{{cyan:#{type}}}" }
37
+ allowed_values = Script::Layers::Application::ExtensionPoints.available_types.map { |type| "{{cyan:#{type}}}" }
38
38
  ShopifyCli::Context.message("script.create.help", ShopifyCli::TOOL_NAME, allowed_values.join(", "))
39
39
  end
40
40
  end
@@ -0,0 +1,40 @@
1
+ mutation AppScriptSet(
2
+ $uuid: String
3
+ $extensionPointName: ExtensionPointName!,
4
+ $title: String!,
5
+ $description: String,
6
+ $force: Boolean,
7
+ $schemaMajorVersion: String,
8
+ $schemaMinorVersion: String,
9
+ $scriptJsonVersion: String!,
10
+ $configurationUi: Boolean!,
11
+ $configurationDefinition: String!,
12
+ $moduleUploadUrl: String!,
13
+ ) {
14
+ appScriptSet(
15
+ uuid: $uuid
16
+ extensionPointName: $extensionPointName
17
+ title: $title
18
+ description: $description
19
+ force: $force
20
+ schemaMajorVersion: $schemaMajorVersion
21
+ schemaMinorVersion: $schemaMinorVersion,
22
+ scriptJsonVersion: $scriptJsonVersion,
23
+ configurationUi: $configurationUi,
24
+ configurationDefinition: $configurationDefinition,
25
+ moduleUploadUrl: $moduleUploadUrl,
26
+ ) {
27
+ userErrors {
28
+ field
29
+ message
30
+ tag
31
+ }
32
+ appScript {
33
+ uuid
34
+ appKey
35
+ configSchema
36
+ extensionPointName
37
+ title
38
+ }
39
+ }
40
+ }
@@ -1,44 +0,0 @@
1
- mutation AppScriptUpdateOrCreate(
2
- $extensionPointName: ExtensionPointName!,
3
- $title: String,
4
- $description: String,
5
- $sourceCode: String,
6
- $language: String,
7
- $force: Boolean,
8
- $schemaMajorVersion: String,
9
- $schemaMinorVersion: String,
10
- $useMsgpack: Boolean,
11
- $uuid: String,
12
- $configurationUi: Boolean!,
13
- $scriptJsonVersion: String!,
14
- $configurationDefinition: String!,
15
- ) {
16
- appScriptUpdateOrCreate(
17
- extensionPointName: $extensionPointName
18
- title: $title
19
- description: $description
20
- sourceCode: $sourceCode
21
- language: $language
22
- force: $force
23
- schemaMajorVersion: $schemaMajorVersion
24
- schemaMinorVersion: $schemaMinorVersion
25
- useMsgpack: $useMsgpack,
26
- uuid: $uuid
27
- configurationUi: $configurationUi
28
- scriptJsonVersion: $scriptJsonVersion
29
- configurationDefinition: $configurationDefinition
30
- ) {
31
- userErrors {
32
- field
33
- message
34
- tag
35
- }
36
- appScript {
37
- uuid
38
- appKey
39
- configSchema
40
- extensionPointName
41
- title
42
- }
43
- }
44
- }
@@ -0,0 +1,9 @@
1
+ mutation moduleUploadUrlGenerate {
2
+ moduleUploadUrlGenerate {
3
+ url
4
+ userErrors {
5
+ field
6
+ message
7
+ }
8
+ }
9
+ }
@@ -17,7 +17,7 @@ module Script
17
17
  uuid:,
18
18
  extension_point_type:,
19
19
  script_content:,
20
- compiled_type:,
20
+ compiled_type: nil,
21
21
  metadata:,
22
22
  script_json:
23
23
  )
@@ -35,7 +35,6 @@ module Script
35
35
  uuid: @uuid,
36
36
  extension_point_type: @extension_point_type,
37
37
  script_content: @script_content,
38
- compiled_type: @compiled_type,
39
38
  api_key: api_key,
40
39
  force: force,
41
40
  metadata: @metadata,
@@ -93,6 +93,8 @@ module Script
93
93
  super("WebAssembly binary not found")
94
94
  end
95
95
  end
96
+
97
+ class ScriptUploadError < ScriptProjectError; end
96
98
  end
97
99
  end
98
100
  end
@@ -32,7 +32,6 @@ module Script
32
32
  uuid: script_project.uuid,
33
33
  extension_point_type: script_project.extension_point_type,
34
34
  script_content: script_content,
35
- compiled_type: compiled_type,
36
35
  metadata: metadata,
37
36
  script_json: script_project.script_json,
38
37
  )
@@ -14,32 +14,31 @@ module Script
14
14
  uuid:,
15
15
  extension_point_type:,
16
16
  script_content:,
17
- compiled_type:,
18
17
  api_key: nil,
19
18
  force: false,
20
19
  metadata:,
21
20
  script_json:
22
21
  )
23
- query_name = "app_script_update_or_create"
22
+ url = UploadScript.new(ctx).call(api_key, script_content)
23
+
24
+ query_name = "app_script_set"
24
25
  variables = {
25
26
  uuid: uuid,
26
27
  extensionPointName: extension_point_type.upcase,
27
28
  title: script_json.title,
28
29
  description: script_json.description,
29
- sourceCode: Base64.encode64(script_content),
30
- language: compiled_type,
31
30
  force: force,
32
31
  schemaMajorVersion: metadata.schema_major_version.to_s, # API expects string value
33
32
  schemaMinorVersion: metadata.schema_minor_version.to_s, # API expects string value
34
- useMsgpack: metadata.use_msgpack,
35
33
  scriptJsonVersion: script_json.version,
36
34
  configurationUi: script_json.configuration_ui,
37
35
  configurationDefinition: script_json.configuration&.to_json,
36
+ moduleUploadUrl: url,
38
37
  }
39
- resp_hash = script_service_request(query_name: query_name, api_key: api_key, variables: variables)
40
- user_errors = resp_hash["data"]["appScriptUpdateOrCreate"]["userErrors"]
38
+ resp_hash = MakeRequest.new(ctx).call(query_name: query_name, api_key: api_key, variables: variables)
39
+ user_errors = resp_hash["data"]["appScriptSet"]["userErrors"]
41
40
 
42
- return resp_hash["data"]["appScriptUpdateOrCreate"]["appScript"]["uuid"] if user_errors.empty?
41
+ return resp_hash["data"]["appScriptSet"]["appScript"]["uuid"] if user_errors.empty?
43
42
 
44
43
  if user_errors.any? { |e| e["tag"] == "already_exists_error" }
45
44
  raise Errors::ScriptRepushError, uuid
@@ -67,11 +66,14 @@ module Script
67
66
  def get_app_scripts(api_key:, extension_point_type:)
68
67
  query_name = "get_app_scripts"
69
68
  variables = { appKey: api_key, extensionPointName: extension_point_type.upcase }
70
- script_service_request(query_name: query_name, api_key: api_key, variables: variables)["data"]["appScripts"]
69
+ response = MakeRequest.new(ctx).call(
70
+ query_name: query_name,
71
+ api_key: api_key,
72
+ variables: variables
73
+ )
74
+ response["data"]["appScripts"]
71
75
  end
72
76
 
73
- private
74
-
75
77
  class ScriptServiceAPI < ShopifyCli::API
76
78
  property(:api_key, accepts: String)
77
79
 
@@ -114,47 +116,95 @@ module Script
114
116
  end
115
117
  private_constant(:PartnersProxyAPI)
116
118
 
117
- def script_service_request(query_name:, variables: nil, **options)
118
- resp = if bypass_partners_proxy
119
- ScriptServiceAPI.query(ctx, query_name, variables: variables, **options)
120
- else
121
- proxy_through_partners(query_name: query_name, variables: variables, **options)
119
+ class MakeRequest
120
+ attr_reader :ctx
121
+
122
+ def initialize(ctx)
123
+ @ctx = ctx
122
124
  end
123
- raise_if_graphql_failed(resp)
124
- resp
125
- end
126
125
 
127
- def bypass_partners_proxy
128
- !ENV["BYPASS_PARTNERS_PROXY"].nil?
129
- end
126
+ def self.bypass_partners_proxy
127
+ !ENV["BYPASS_PARTNERS_PROXY"].nil?
128
+ end
130
129
 
131
- def proxy_through_partners(query_name:, variables: nil, **options)
132
- options[:variables] = variables.to_json if variables
133
- resp = PartnersProxyAPI.query(ctx, query_name, **options)
134
- raise_if_graphql_failed(resp)
135
- JSON.parse(resp["data"]["scriptServiceProxy"])
136
- end
130
+ def call(query_name:, variables: nil, **options)
131
+ resp = if MakeRequest.bypass_partners_proxy
132
+ ScriptServiceAPI.query(ctx, query_name, variables: variables, **options)
133
+ else
134
+ proxy_through_partners(query_name: query_name, variables: variables, **options)
135
+ end
136
+ raise_if_graphql_failed(resp)
137
+ resp
138
+ end
137
139
 
138
- def raise_if_graphql_failed(response)
139
- raise Errors::EmptyResponseError if response.nil?
140
-
141
- return unless response.key?("errors")
142
- case error_code(response["errors"])
143
- when "forbidden"
144
- raise Errors::ForbiddenError
145
- when "forbidden_on_shop"
146
- raise Errors::ShopAuthenticationError
147
- when "app_not_installed_on_shop"
148
- raise Errors::AppNotInstalledError
149
- else
150
- raise Errors::GraphqlError, response["errors"]
140
+ def proxy_through_partners(query_name:, variables: nil, **options)
141
+ options[:variables] = variables.to_json if variables
142
+ resp = PartnersProxyAPI.query(ctx, query_name, **options)
143
+ raise_if_graphql_failed(resp)
144
+ JSON.parse(resp["data"]["scriptServiceProxy"])
145
+ end
146
+
147
+ def raise_if_graphql_failed(response)
148
+ raise Errors::EmptyResponseError if response.nil?
149
+
150
+ return unless response.key?("errors")
151
+ case error_code(response["errors"])
152
+ when "forbidden"
153
+ raise Errors::ForbiddenError
154
+ when "forbidden_on_shop"
155
+ raise Errors::ShopAuthenticationError
156
+ when "app_not_installed_on_shop"
157
+ raise Errors::AppNotInstalledError
158
+ else
159
+ raise Errors::GraphqlError, response["errors"]
160
+ end
161
+ end
162
+
163
+ def error_code(errors)
164
+ errors.map do |e|
165
+ code = e.dig("extensions", "code")
166
+ return code if code
167
+ end
151
168
  end
152
169
  end
153
170
 
154
- def error_code(errors)
155
- errors.map do |e|
156
- code = e.dig("extensions", "code")
157
- return code if code
171
+ class UploadScript
172
+ attr_reader :ctx
173
+
174
+ def initialize(ctx)
175
+ @ctx = ctx
176
+ end
177
+
178
+ def call(api_key, script_content)
179
+ apply_module_upload_url(api_key).tap do |url|
180
+ upload(url, script_content)
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def apply_module_upload_url(api_key)
187
+ query_name = "module_upload_url_generate"
188
+ variables = {}
189
+ resp_hash = MakeRequest.new(ctx).call(query_name: query_name, api_key: api_key, variables: variables)
190
+ user_errors = resp_hash["data"]["moduleUploadUrlGenerate"]["userErrors"]
191
+
192
+ raise Errors::GraphqlError, user_errors if user_errors.any?
193
+ resp_hash["data"]["moduleUploadUrlGenerate"]["url"]
194
+ end
195
+
196
+ def upload(url, script_content)
197
+ url = URI(url)
198
+
199
+ https = Net::HTTP.new(url.host, url.port)
200
+ https.use_ssl = true
201
+
202
+ request = Net::HTTP::Put.new(url)
203
+ request["Content-Type"] = "application/wasm"
204
+ request.body = script_content
205
+
206
+ response = https.request(request)
207
+ raise Errors::ScriptUploadError unless response.code == "200"
158
208
  end
159
209
  end
160
210
  end