shopify-cli 2.1.0 → 2.3.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.
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