Repository: Shopify/shopify-cli (Ruby)

Issue:
No ability to set a custom port for ngrok tunnel
# Issue summary

There's currently no ability to set change the port from 8081 when running `shopify node serve`. As a result, there's no change in the ngrok URL that's generated, which may be in use by another app that's being locally developed.



## Expected behavior

Either by use of a `--port` flag or reading the `PORT` environment variable in the .env file, a different ngrok tunnel should be started based on the specified port (with perhaps a fallback to 8081).



## Actual behavior

What actually happens?

The port is hard coded to 8081 regardless of what is set in the .env file, which creates the same tunnel previously created. It (of course) crashes if you attempt to add a `--port` flag that doesn't exist.



## Steps to reproduce the problem

1. run `shopify node serve` without a specified port
2. ngrok starts up on port 8081 (or continues running on port 8081 if previously started)



## Specifications

- App type: `node`
- Shell: `zsh`
- Ruby version (ruby -v): `ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [arm64-darwin20]`



Discussion:
I noticed that other serve commands (e.g. rails serve) have the same issue, we will probably have to add the custom port for those as well.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58a29cfa55..4920582fea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 Unreleased
 ------
+* [#1567](https://github.com/Shopify/shopify-cli/pull/1567): Add ability to set custom port for ngrok tunnel in node serve.
 * [#1584](https://github.com/Shopify/shopify-cli/issues/1584): Fixed extended help message not showing.
 * [#1566](https://github.com/Shopify/shopify-cli/pull/1566): Fix bug when running `npm | yarn list` for extension package resolution.
 * [#1557](https://github.com/Shopify/shopify-cli/pull/1557): **Breaking** Move app commands under `shopify app`.
diff --git a/lib/project_types/node/commands/serve.rb b/lib/project_types/node/commands/serve.rb
index a2584ec19c..c7d1477e4c 100644
--- a/lib/project_types/node/commands/serve.rb
+++ b/lib/project_types/node/commands/serve.rb
@@ -9,6 +9,7 @@ class Serve < ShopifyCLI::Command::AppSubCommand
         parser.on("--host=HOST") do |h|
           flags[:host] = h.gsub('"', "")
         end
+        parser.on("--port=PORT") { |port| flags[:port] = port }
       end
 
       def call(*)
@@ -29,11 +30,18 @@ def call(*)
 
         CLI::UI::Frame.open(@ctx.message("node.serve.running_server")) do
           env = project.env.to_h
-          env["PORT"] = ShopifyCLI::Tunnel::PORT.to_s
+          env["PORT"] = port.to_s
           @ctx.system("npm run dev", env: env)
         end
       end
 
+      def port
+        return ShopifyCLI::Tunnel::PORT.to_s unless options.flags.key?(:port)
+        port = options.flags[:port].to_i
+        @ctx.abort(@ctx.message("node.serve.error.invalid_port", options.flags[:port])) unless port > 0
+        port
+      end
+
       def self.help
         ShopifyCLI::Context.message("node.serve.help", ShopifyCLI::TOOL_NAME)
       end
diff --git a/lib/project_types/node/messages/messages.rb b/lib/project_types/node/messages/messages.rb
index c9eeeb7279..a17ab931d3 100644
--- a/lib/project_types/node/messages/messages.rb
+++ b/lib/project_types/node/messages/messages.rb
@@ -119,10 +119,12 @@ module Messages
           extended_help: <<~HELP,
             {{bold:Options:}}
               {{cyan:--host=HOST}}: Bypass running tunnel and use custom host. HOST must be HTTPS url.
+              {{cyan:--port=PORT}}: Use custom port.
           HELP
 
           error: {
             host_must_be_https: "HOST must be a HTTPS url.",
+            invalid_port: "%s is not a valid port.",
           },
 
           open_info: <<~MESSAGE,
@@ -154,6 +156,7 @@ module Messages
           error: {
             token_argument_missing: "{{x}} {{red:auth requires a token argument}}\n\n",
           },
+
         },
 
         forms: {
diff --git a/lib/shopify_cli/messages/messages.rb b/lib/shopify_cli/messages/messages.rb
index d5b8081536..0b45a7fc15 100644
--- a/lib/shopify_cli/messages/messages.rb
+++ b/lib/shopify_cli/messages/messages.rb
@@ -462,7 +462,6 @@ module Messages
             ngrok: "Something went wrong with ngrok installation,"\
               "please make sure %s exists within %s before trying again",
           },
-
           installing: "Installing ngrok…",
           not_running: "{{green:x}} ngrok tunnel not running",
           prereq_command_location: "%s @ %s",


Test patch:
diff --git a/test/project_types/node/commands/serve_test.rb b/test/project_types/node/commands/serve_test.rb
index a252455f22..56b8d113eb 100644
--- a/test/project_types/node/commands/serve_test.rb
+++ b/test/project_types/node/commands/serve_test.rb
@@ -75,6 +75,35 @@ def test_update_env_with_host
         )
         run_cmd('app node serve --host="https://example-foo.com"')
       end
+
+      def test_server_command_when_port_passed
+        ShopifyCLI::Tunnel.stubs(:start).returns("https://example.com")
+        ShopifyCLI::Tasks::UpdateDashboardURLS.expects(:call)
+        ShopifyCLI::Resources::EnvFile.any_instance.expects(:update)
+        @context.expects(:system).with(
+          "npm run dev",
+          env: {
+            "SHOPIFY_API_KEY" => "mykey",
+            "SHOPIFY_API_SECRET" => "mysecretkey",
+            "SHOP" => "my-test-shop.myshopify.com",
+            "SCOPES" => "read_products",
+            "HOST" => "https://example.com",
+            "PORT" => "5000",
+          }
+        )
+        run_cmd("app node serve --port=5000")
+      end
+
+      def test_server_command_when_invalid_port_passed
+        invalid_port = "NOT_PORT"
+        ShopifyCLI::Tunnel.stubs(:start).returns("https://example.com")
+        ShopifyCLI::Tasks::UpdateDashboardURLS.expects(:call)
+        ShopifyCLI::Resources::EnvFile.any_instance.expects(:update)
+        @context.expects(:abort).with(
+          @context.message("node.serve.error.invalid_port", invalid_port)
+        )
+        run_cmd("app node serve --port=#{invalid_port}")
+      end
     end
   end
 end