shopify-cli 1.10.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +1 -0
  3. data/CHANGELOG.md +9 -1
  4. data/Gemfile.lock +1 -1
  5. data/lib/project_types/extension/cli.rb +6 -2
  6. data/lib/project_types/extension/commands/serve.rb +69 -1
  7. data/lib/project_types/extension/commands/tunnel.rb +3 -1
  8. data/lib/project_types/extension/extension_project.rb +1 -0
  9. data/lib/project_types/extension/features/argo.rb +15 -24
  10. data/lib/project_types/extension/features/argo_runtime.rb +63 -0
  11. data/lib/project_types/extension/features/argo_serve.rb +35 -25
  12. data/lib/project_types/extension/features/argo_serve_options.rb +40 -0
  13. data/lib/project_types/extension/messages/messages.rb +3 -0
  14. data/lib/project_types/extension/models/npm_package.rb +14 -0
  15. data/lib/project_types/extension/models/specification.rb +1 -0
  16. data/lib/project_types/extension/models/specification_handlers/checkout_argo_extension.rb +18 -0
  17. data/lib/project_types/extension/models/specification_handlers/default.rb +28 -3
  18. data/lib/project_types/extension/tasks/choose_next_available_port.rb +36 -0
  19. data/lib/project_types/extension/tasks/configure_features.rb +2 -0
  20. data/lib/project_types/extension/tasks/find_npm_packages.rb +106 -0
  21. data/lib/project_types/script/cli.rb +1 -0
  22. data/lib/project_types/script/layers/domain/errors.rb +0 -2
  23. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +12 -17
  24. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +13 -7
  25. data/lib/project_types/script/layers/infrastructure/command_runner.rb +19 -0
  26. data/lib/project_types/script/layers/infrastructure/errors.rb +12 -3
  27. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +4 -4
  28. data/lib/project_types/script/layers/infrastructure/rust_project_creator.rb +9 -10
  29. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +5 -6
  30. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +2 -28
  31. data/lib/project_types/script/layers/infrastructure/script_service.rb +1 -1
  32. data/lib/project_types/script/messages/messages.rb +6 -4
  33. data/lib/project_types/script/tasks/ensure_env.rb +10 -2
  34. data/lib/project_types/script/ui/error_handler.rb +7 -6
  35. data/lib/shopify-cli/messages/messages.rb +47 -43
  36. data/lib/shopify-cli/method_object.rb +4 -4
  37. data/lib/shopify-cli/oauth.rb +7 -1
  38. data/lib/shopify-cli/partners_api/organizations.rb +3 -3
  39. data/lib/shopify-cli/tasks/select_org_and_shop.rb +6 -4
  40. data/lib/shopify-cli/tunnel.rb +22 -1
  41. data/lib/shopify-cli/version.rb +1 -1
  42. metadata +10 -4
  43. data/lib/project_types/extension/features/argo_renderer_package.rb +0 -47
@@ -56,8 +56,10 @@ module Extension
56
56
  },
57
57
  serve: {
58
58
  frame_title: "Serving extension...",
59
+ no_available_ports_found: "No available ports found to run extension.",
59
60
  serve_failure_message: "Failed to run extension code.",
60
61
  serve_missing_information: "Missing shop or api_key.",
62
+ tunnel_already_running: "A tunnel running on another port has been detected. Close the tunnel and try again.",
61
63
  },
62
64
  tunnel: {
63
65
  missing_token: "{{x}} {{red:auth requires a token argument}}. "\
@@ -115,6 +117,7 @@ module Extension
115
117
  },
116
118
  errors: {
117
119
  unknown_type: "Unknown extension type %s",
120
+ package_not_found: "`%s` package not found.",
118
121
  },
119
122
  }
120
123
 
@@ -0,0 +1,14 @@
1
+ require "semantic/semantic"
2
+
3
+ module Extension
4
+ module Models
5
+ NpmPackage = Struct.new(:name, :version, keyword_init: true) do
6
+ include Comparable
7
+
8
+ def <=>(other)
9
+ return nil unless name == other.name
10
+ Semantic::Version.new(version) <=> Semantic::Version.new(other.version)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -12,6 +12,7 @@ module Extension
12
12
  property! :git_template, converts: :to_str
13
13
  property! :required_fields, accepts: Array, default: -> { [] }
14
14
  property! :required_shop_beta_flags, accepts: Array, default: -> { [] }
15
+ property! :cli_package_name, accepts: String, converts: :to_str, default: ""
15
16
  end
16
17
 
17
18
  def self.build(feature_set_attributes)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Extension
4
+ module Models
5
+ module SpecificationHandlers
6
+ class CheckoutArgoExtension < Default
7
+ PERMITTED_CONFIG_KEYS = [:metafields, :extension_points]
8
+
9
+ def config(context)
10
+ {
11
+ **Features::ArgoConfig.parse_yaml(context, PERMITTED_CONFIG_KEYS),
12
+ **argo.config(context),
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -42,20 +42,45 @@ module Extension
42
42
  []
43
43
  end
44
44
 
45
- def serve(context)
46
- Features::ArgoServe.new(specification_handler: self, context: context).call
45
+ def choose_port?(context)
46
+ argo_runtime(context).accepts_port?
47
+ end
48
+
49
+ def establish_tunnel?(context)
50
+ argo_runtime(context).accepts_tunnel_url?
51
+ end
52
+
53
+ def serve(context:, port:, tunnel_url:)
54
+ Features::ArgoServe.new(specification_handler: self, argo_runtime: argo_runtime(context),
55
+ context: context, port: port, tunnel_url: tunnel_url).call
47
56
  end
48
57
 
49
58
  def renderer_package(context)
50
59
  argo.renderer_package(context)
51
60
  end
52
61
 
62
+ def argo_runtime(context)
63
+ @argo_runtime ||= Features::ArgoRuntime.new(
64
+ renderer: renderer_package(context),
65
+ cli: cli_package(context)
66
+ )
67
+ end
68
+
69
+ def cli_package(context)
70
+ cli_package_name = specification.features.argo&.cli_package_name
71
+ return unless cli_package_name
72
+
73
+ js_system = ShopifyCli::JsSystem.new(ctx: context)
74
+ Tasks::FindNpmPackages.exactly_one_of(cli_package_name, js_system: js_system)
75
+ .unwrap { |_e| context.abort(context.message("errors.package_not_found", cli_package_name)) }
76
+ end
77
+
53
78
  protected
54
79
 
55
80
  def argo
56
81
  Features::Argo.new(
57
82
  git_template: specification.features.argo.git_template,
58
- renderer_package_name: specification.features.argo.renderer_package_name,
83
+ renderer_package_name: specification.features.argo.renderer_package_name
59
84
  )
60
85
  end
61
86
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ require "shopify_cli"
3
+ require "socket"
4
+
5
+ module Extension
6
+ module Tasks
7
+ class ChooseNextAvailablePort
8
+ include ShopifyCli::MethodObject
9
+
10
+ property! :from
11
+ property! :to, default: -> { from + 10 }
12
+ property! :host, default: "localhost"
13
+
14
+ def call
15
+ available_port = port_range(from: from, to: to).find { |p| available?(host, p) }
16
+ raise ArgumentError, "Ports between #{from} and #{to} are unavailable" if available_port.nil?
17
+ available_port
18
+ end
19
+
20
+ private
21
+
22
+ def port_range(from:, to:)
23
+ (from..to)
24
+ end
25
+
26
+ def available?(host, port)
27
+ Socket.tcp(host, port, connect_timeout: 1) do |socket|
28
+ socket.close
29
+ false
30
+ end
31
+ rescue Errno::ECONNREFUSED
32
+ true
33
+ end
34
+ end
35
+ end
36
+ end
@@ -42,10 +42,12 @@ module Extension
42
42
  renderer_package_name: "@shopify/argo-admin",
43
43
  required_fields: [:shop, :api_key],
44
44
  required_shop_beta_flags: [:argo_admin_beta],
45
+ cli_package_name: "@shopify/argo-admin-cli",
45
46
  },
46
47
  checkout: {
47
48
  git_template: "https://github.com/Shopify/argo-checkout-template.git",
48
49
  renderer_package_name: "@shopify/argo-checkout",
50
+ cli_package_name: "@shopify/argo-run",
49
51
  },
50
52
  }
51
53
  end
@@ -0,0 +1,106 @@
1
+ module Extension
2
+ module Tasks
3
+ class FindNpmPackages
4
+ include ShopifyCli::MethodObject
5
+
6
+ property! :js_system, accepts: ShopifyCli::JsSystem
7
+ property! :production_only, accepts: [true, false], default: false, reader: :production_only?
8
+
9
+ def self.at_least_one_of(*package_names, **config)
10
+ new(**config).at_least_one_of(*package_names)
11
+ end
12
+
13
+ def self.all(*package_names, **config)
14
+ new(**config).all(*package_names)
15
+ end
16
+
17
+ def self.exactly_one_of(*package_names, **config)
18
+ new(**config).exactly_one_of(*package_names)
19
+ end
20
+
21
+ def all(*package_names)
22
+ call(*package_names) do |found_packages|
23
+ found_package_names = found_packages.map(&:name)
24
+ next found_packages if Set.new(found_package_names) == Set.new(package_names)
25
+ raise PackageResolutionFailed, format(
26
+ "Missing packages: %s",
27
+ (package_names - found_package_names).join(", ")
28
+ )
29
+ end
30
+ end
31
+
32
+ def at_least_one_of(*package_names)
33
+ call(*package_names) do |found_packages|
34
+ found_package_names = found_packages.map(&:name)
35
+ next found_packages unless (found_package_names & package_names).empty?
36
+ raise PackageResolutionFailed, format(
37
+ "Expected at least one of the following packages: %s",
38
+ package_names.join(", ")
39
+ )
40
+ end
41
+ end
42
+
43
+ def exactly_one_of(*package_names)
44
+ call(*package_names) do |found_packages|
45
+ case found_packages.count
46
+ when 0
47
+ raise PackageResolutionFailed, format(
48
+ "Expected one of the following packages: %s",
49
+ package_names.join(", ")
50
+ )
51
+ when 1
52
+ found_packages.first.tap do |found_package|
53
+ next found_package if package_names.include?(found_package.name)
54
+ raise PackageResolutionFailed, format(
55
+ "Expected the following package: %s",
56
+ found_package.name
57
+ )
58
+ end
59
+ else
60
+ raise PackageResolutionFailed, "Found more than one package"
61
+ end
62
+ end
63
+ end
64
+
65
+ def call(*package_names, &validate)
66
+ validate ||= ->(found_packages) { found_packages }
67
+
68
+ unless package_names.all? { |name| name.is_a?(String) }
69
+ raise ArgumentError, "Expected a list of package names"
70
+ end
71
+
72
+ ShopifyCli::Result
73
+ .call(&method(:list_packages))
74
+ .then(&method(:search_packages).curry[package_names])
75
+ .then(&method(:filter_duplicates))
76
+ .then(&validate)
77
+ end
78
+
79
+ def list_packages
80
+ result, error, status =
81
+ js_system.call(yarn: yarn_list, npm: npm_list, capture_response: true)
82
+ raise error unless status.success?
83
+ result
84
+ end
85
+
86
+ def yarn_list
87
+ production_only? ? %w[list --production] : %w[list]
88
+ end
89
+
90
+ def npm_list
91
+ production_only? ? %w[list --prod --depth=1] : %w[list --depth=1]
92
+ end
93
+
94
+ def search_packages(packages, package_list)
95
+ pattern = /(#{packages.join("|")})@(\d.*)$/
96
+ package_list.scan(pattern).map do |(name, version)|
97
+ Models::NpmPackage.new(name: name, version: version.strip)
98
+ end
99
+ end
100
+
101
+ def filter_duplicates(packages)
102
+ packages.reject { |p| p.version.match(/deduped/) }.uniq
103
+ end
104
+ end
105
+ end
106
+ end
@@ -54,6 +54,7 @@ module Script
54
54
  Project.project_filepath("layers/infrastructure/assemblyscript_project_creator")
55
55
  autoload :AssemblyScriptTaskRunner, Project.project_filepath("layers/infrastructure/assemblyscript_task_runner")
56
56
  autoload :AssemblyScriptTsConfig, Project.project_filepath("layers/infrastructure/assemblyscript_tsconfig")
57
+ autoload :CommandRunner, Project.project_filepath("layers/infrastructure/command_runner")
57
58
  autoload :RustProjectCreator,
58
59
  Project.project_filepath("layers/infrastructure/rust_project_creator.rb")
59
60
  autoload :RustTaskRunner, Project.project_filepath("layers/infrastructure/rust_task_runner")
@@ -39,8 +39,6 @@ module Script
39
39
  end
40
40
  end
41
41
 
42
- class ServiceFailureError < ScriptProjectError; end
43
-
44
42
  class MetadataNotFoundError < ScriptProjectError; end
45
43
 
46
44
  class MetadataValidationError < ScriptProjectError; end
@@ -12,7 +12,7 @@ module Script
12
12
 
13
13
  BOOTSTRAP = "npx --no-install shopify-scripts-toolchain-as bootstrap --from %{extension_point} --dest %{base}"
14
14
  BUILD = "shopify-scripts-toolchain-as build --src src/shopify_main.ts " \
15
- "--binary build/%{script_name}.wasm --metadata build/metadata.json"
15
+ "--binary build/script.wasm --metadata build/metadata.json"
16
16
  MIN_NODE_VERSION = "14.5.0"
17
17
  ASC_ARGS = "-- --lib node_modules --optimize --use Date="
18
18
 
@@ -22,28 +22,24 @@ module Script
22
22
  end
23
23
 
24
24
  def bootstrap
25
- out, status = ctx.capture2e(bootstap_command)
26
- raise Domain::Errors::ServiceFailureError, out unless status.success?
25
+ command_runner.call(bootstap_command)
27
26
  end
28
27
 
29
28
  private
30
29
 
30
+ def command_runner
31
+ @command_runner ||= CommandRunner.new(ctx: ctx)
32
+ end
33
+
31
34
  def write_npmrc
32
- ctx.system(
33
- "npm", "--userconfig", "./.npmrc", "config", "set", "@shopify:registry", "https://registry.npmjs.com"
34
- )
35
- ctx.system(
36
- "npm", "--userconfig", "./.npmrc", "config", "set", "engine-strict", "true"
37
- )
35
+ command_runner.call("npm --userconfig ./.npmrc config set @shopify:registry https://registry.npmjs.com")
36
+ command_runner.call("npm --userconfig ./.npmrc config set engine-strict true")
38
37
  end
39
38
 
40
39
  def extension_point_version
41
- if extension_point.sdks.assemblyscript.versioned?
42
- return extension_point.sdks.assemblyscript.version
43
- end
40
+ return extension_point.sdks.assemblyscript.version if extension_point.sdks.assemblyscript.versioned?
44
41
 
45
- out, status = ctx.capture2e("npm show #{extension_point.sdks.assemblyscript.package} version --json")
46
- raise Domain::Errors::ServiceFailureError, out unless status.success?
42
+ out = command_runner.call("npm show #{extension_point.sdks.assemblyscript.package} version --json")
47
43
  "^#{JSON.parse(out)}"
48
44
  end
49
45
 
@@ -85,13 +81,12 @@ module Script
85
81
 
86
82
  def build_command
87
83
  type = extension_point.dasherize_type
88
- base_command = format(BUILD, script_name: script_name)
89
84
  domain = extension_point.domain
90
85
 
91
86
  if domain.nil?
92
- "#{base_command} #{ASC_ARGS}"
87
+ "#{BUILD} #{ASC_ARGS}"
93
88
  else
94
- "#{base_command} --domain #{domain} --ep #{type} #{ASC_ARGS}"
89
+ "#{BUILD} --domain #{domain} --ep #{type} #{ASC_ARGS}"
95
90
  end
96
91
  end
97
92
  end
@@ -63,9 +63,7 @@ module Script
63
63
 
64
64
  def compile
65
65
  check_compilation_dependencies!
66
-
67
- out, status = ctx.capture2e(SCRIPT_SDK_BUILD)
68
- raise Domain::Errors::ServiceFailureError, out unless status.success?
66
+ CommandRunner.new(ctx: ctx).call(SCRIPT_SDK_BUILD)
69
67
  end
70
68
 
71
69
  def check_compilation_dependencies!
@@ -81,11 +79,19 @@ module Script
81
79
  end
82
80
 
83
81
  def bytecode
84
- filename = format(BYTECODE_FILE, name: script_name)
85
- raise Errors::WebAssemblyBinaryNotFoundError unless ctx.file_exist?(filename)
82
+ legacy_filename = format(BYTECODE_FILE, name: script_name)
83
+ filename = format(BYTECODE_FILE, name: "script")
84
+
85
+ bytecode_file = if ctx.file_exist?(filename)
86
+ filename
87
+ elsif ctx.file_exist?(legacy_filename)
88
+ legacy_filename
89
+ else
90
+ raise Errors::WebAssemblyBinaryNotFoundError
91
+ end
86
92
 
87
- contents = ctx.binread(filename)
88
- ctx.rm(filename)
93
+ contents = ctx.binread(bytecode_file)
94
+ ctx.rm(bytecode_file)
89
95
 
90
96
  contents
91
97
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Script
4
+ module Layers
5
+ module Infrastructure
6
+ class CommandRunner
7
+ include SmartProperties
8
+
9
+ property! :ctx, accepts: ShopifyCli::Context
10
+
11
+ def call(cmd)
12
+ out, status = ctx.capture2e(cmd)
13
+ raise Errors::SystemCallFailureError.new(out: out.chomp, cmd: cmd) unless status.success?
14
+ out
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -69,11 +69,20 @@ module Script
69
69
 
70
70
  class ProjectCreatorNotFoundError < ScriptProjectError; end
71
71
 
72
+ class SystemCallFailureError < ScriptProjectError
73
+ attr_reader :out, :cmd
74
+ def initialize(out:, cmd:)
75
+ super()
76
+ @out = out
77
+ @cmd = cmd
78
+ end
79
+ end
80
+
72
81
  class ScriptRepushError < ScriptProjectError
73
- attr_reader :api_key
74
- def initialize(api_key)
82
+ attr_reader :uuid
83
+ def initialize(uuid)
75
84
  super()
76
- @api_key = api_key
85
+ @uuid = uuid
77
86
  end
78
87
  end
79
88
 
@@ -8,7 +8,7 @@ module Script
8
8
  property! :ctx, accepts: ShopifyCli::Context
9
9
 
10
10
  def create_push_package(script_project:, script_content:, compiled_type:, metadata:)
11
- build_file_path = file_path(script_project.id, script_project.script_name, compiled_type)
11
+ build_file_path = file_path(script_project.id, compiled_type)
12
12
  write_to_path(build_file_path, script_content)
13
13
 
14
14
  Domain::PushPackage.new(
@@ -24,7 +24,7 @@ module Script
24
24
  end
25
25
 
26
26
  def get_push_package(script_project:, compiled_type:, metadata:)
27
- build_file_path = file_path(script_project.id, script_project.script_name, compiled_type)
27
+ build_file_path = file_path(script_project.id, compiled_type)
28
28
  raise Domain::PushPackageNotFoundError unless ctx.file_exist?(build_file_path)
29
29
 
30
30
  script_content = ctx.binread(build_file_path)
@@ -47,8 +47,8 @@ module Script
47
47
  ctx.binwrite(path, content)
48
48
  end
49
49
 
50
- def file_path(path_to_script, script_name, compiled_type)
51
- "#{path_to_script}/build/#{script_name}.#{compiled_type}"
50
+ def file_path(path_to_script, compiled_type)
51
+ "#{path_to_script}/build/script.#{compiled_type}"
52
52
  end
53
53
  end
54
54
  end