diff --git a/packages/app/src/cli/commands/app/dev.ts b/packages/app/src/cli/commands/app/dev.ts index b73934b42c3..72a46c3b07e 100644 --- a/packages/app/src/cli/commands/app/dev.ts +++ b/packages/app/src/cli/commands/app/dev.ts @@ -87,6 +87,11 @@ If you're using the Ruby app template, then you need to complete the following s default: false, exclusive: ['tunnel-url'], }), + host: Flags.string({ + description: 'Set which network interface the web server listens on. The default value is localhost.', + env: 'SHOPIFY_FLAG_HOST', + default: 'localhost', + }), 'localhost-port': Flags.integer({ description: 'Port to use for localhost.', env: 'SHOPIFY_FLAG_LOCALHOST_PORT', @@ -174,6 +179,7 @@ If you're using the Ruby app template, then you need to complete the following s notify: flags.notify, graphiqlPort: flags['graphiql-port'], graphiqlKey: flags['graphiql-key'], + host: flags.host, tunnel: tunnelMode, } diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index af0af066266..be0853758de 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -69,6 +69,7 @@ export interface DevOptions { notify?: string graphiqlPort?: number graphiqlKey?: string + host: string } export async function dev(commandOptions: DevOptions) { diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts index 12bfc659c4e..eb1d0dd8ca1 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts @@ -1,4 +1,4 @@ -import {DevConfig, setupDevProcesses, startProxyServer} from './setup-dev-processes.js' +import {DevConfig, setupDevProcesses, proxyService} from './setup-dev-processes.js' import {subscribeAndStartPolling} from './app-logs-polling.js' import {sendWebhook} from './uninstall-webhook.js' import {WebProcess, launchWebProcess} from './web.js' @@ -95,6 +95,7 @@ describe('setup-dev-processes', () => { commandConfig: new Config({root: ''}), skipDependenciesInstallation: false, tunnel: {mode: 'auto'}, + host: 'localhost', } const network: DevConfig['network'] = { proxyUrl: 'https://example.com/proxy', @@ -281,13 +282,14 @@ describe('setup-dev-processes', () => { expect(proxyServerProcess).toMatchObject({ type: 'proxy-server', prefix: 'proxy', - function: startProxyServer, + function: proxyService, options: { port: 444, localhostCert: { cert: 'cert', key: 'key', }, + host: 'localhost', rules: { '/extensions': `http://localhost:${previewExtensionPort}`, '/ping': `http://localhost:${hmrPort}`, @@ -298,6 +300,77 @@ describe('setup-dev-processes', () => { }) }) + test('proxy server process includes host parameter when configured for Docker', async () => { + // Given + const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({supportsDevSessions: false}) + const storeFqdn = 'store.myshopify.io' + const storeId = '123456789' + const remoteAppUpdated = true + const graphiqlPort = 1234 + const commandOptions: DevConfig['commandOptions'] = { + ...appContextResult, + commandConfig: new Config({root: ''}), + skipDependenciesInstallation: false, + tunnel: {mode: 'auto'}, + host: '0.0.0.0', // Docker host setting + directory: '', + update: false, + } + const network: DevConfig['network'] = { + proxyUrl: 'https://example.com/proxy', + proxyPort: 444, + frontendPort: 3000, + backendPort: 3001, + currentUrls: { + applicationUrl: 'https://example.com/proxy', + redirectUrlWhitelist: ['https://example.com/proxy/auth/callback'], + }, + reverseProxyCert: { + cert: 'cert', + key: 'key', + certPath: 'path', + }, + } + + // Create simple app without theme extensions to avoid the theme API calls + const localApp = testAppWithConfig({ + config: {}, + app: testAppLinked({ + allExtensions: [await testUIExtension({type: 'web_pixel_extension'})], + webs: [{ + directory: 'web', + configuration: { + roles: [WebType.Backend, WebType.Frontend], + commands: {dev: 'npm exec remix dev'}, + webhooks_path: '/webhooks', + hmr_server: { + http_paths: ['/ping'], + }, + }, + }], + }), + }) + vi.spyOn(loader, 'reloadApp').mockResolvedValue(localApp) + + // When + const res = await setupDevProcesses({ + localApp, + remoteAppUpdated, + remoteApp: testOrganizationApp(), + developerPlatformClient, + storeFqdn, + storeId, + commandOptions, + network, + partnerUrlsUpdated: true, + graphiqlPort, + }) + + // Then - Verify the proxy server process has the correct host setting + const proxyServerProcess = res.processes.find((process) => process.type === 'proxy-server') + expect(proxyServerProcess?.options.host).toBe('0.0.0.0') + }) + test('process list includes dev-session when useDevSession is true', async () => { const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({supportsDevSessions: true}) const storeFqdn = 'store.myshopify.io' @@ -311,6 +384,7 @@ describe('setup-dev-processes', () => { commandConfig: new Config({root: ''}), skipDependenciesInstallation: false, tunnel: {mode: 'auto'}, + host: 'localhost', } const network: DevConfig['network'] = { proxyUrl: 'https://example.com/proxy', @@ -322,7 +396,7 @@ describe('setup-dev-processes', () => { redirectUrlWhitelist: ['https://example.com/redirect'], }, } - const localApp = testAppWithConfig() + const localApp = testAppWithConfig({config: {}}) vi.spyOn(loader, 'reloadApp').mockResolvedValue(localApp) const remoteApp: DevConfig['remoteApp'] = { @@ -384,6 +458,7 @@ describe('setup-dev-processes', () => { commandConfig: new Config({root: ''}), skipDependenciesInstallation: false, tunnel: {mode: 'auto'}, + host: 'localhost', } const network: DevConfig['network'] = { proxyUrl: 'https://example.com/proxy', @@ -480,6 +555,7 @@ describe('setup-dev-processes', () => { commandConfig: new Config({root: ''}), skipDependenciesInstallation: false, tunnel: {mode: 'auto'}, + host: 'localhost', } const network: DevConfig['network'] = { proxyUrl: 'https://example.com/proxy', @@ -566,6 +642,7 @@ describe('setup-dev-processes', () => { commandConfig: new Config({root: ''}), skipDependenciesInstallation: false, tunnel: {mode: 'auto'}, + host: 'localhost', } const network: DevConfig['network'] = { proxyUrl: 'https://example.com/proxy', diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index 593ce7188fa..6c105b94975 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -30,6 +30,7 @@ interface ProxyServerProcess port: number rules: {[key: string]: string} localhostCert?: LocalhostCert + host: string }> { type: 'proxy-server' } @@ -201,7 +202,7 @@ export async function setupDevProcesses({ ].filter(stripUndefineds) // Add http server proxy & configure ports, for processes that need it - const processesWithProxy = await setPortsAndAddProxyProcess(processes, network.proxyPort, network.reverseProxyCert) + const processesWithProxy = await setPortsAndAddProxyProcess(processes, network.proxyPort, network.reverseProxyCert, commandOptions) return { processes: processesWithProxy, @@ -218,7 +219,8 @@ const stripUndefineds = (process: T | undefined | false): process is T => { async function setPortsAndAddProxyProcess( processes: DevProcesses, proxyPort: number, - reverseProxyCert?: LocalhostCert, + reverseProxyCert: LocalhostCert | undefined, + commandOptions: DevOptions, ): Promise { // Convert processes that use proxying to have a port number and register their mapping rules const processesAndRules = await Promise.all( @@ -251,11 +253,12 @@ async function setPortsAndAddProxyProcess( newProcesses.push({ type: 'proxy-server', prefix: 'proxy', - function: startProxyServer, + function: proxyService, options: { port: proxyPort, rules: allRules, localhostCert: reverseProxyCert, + host: commandOptions.host, }, }) } @@ -263,15 +266,16 @@ async function setPortsAndAddProxyProcess( return newProcesses } -export const startProxyServer: DevProcessFunction<{ +export const proxyService: DevProcessFunction<{ port: number rules: {[key: string]: string} localhostCert?: LocalhostCert -}> = async ({abortSignal, stdout}, {port, rules, localhostCert}) => { + host: string +}> = async ({abortSignal, stdout}, {port, rules, localhostCert, host}) => { const {server} = await getProxyingWebServer(rules, abortSignal, localhostCert, stdout) outputInfo( - `Proxy server started on port ${port} ${localhostCert ? `with certificate ${localhostCert.certPath}` : ''}`, + `Proxy server started on ${host}:${port} ${localhostCert ? `with certificate ${localhostCert.certPath}` : ''}`, stdout, ) - await server.listen(port, 'localhost') + await server.listen(port, host) } diff --git a/packages/cli/README.md b/packages/cli/README.md index f18d48887cb..b1c62d46e46 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -212,8 +212,8 @@ Run the app. ``` USAGE - $ shopify app dev [--checkout-cart-url ] [--client-id | -c ] [--localhost-port - ] [--no-color] [--no-update] [--notify ] [--path ] [--reset | ] + $ shopify app dev [--checkout-cart-url ] [--client-id | -c ] [--host ] + [--localhost-port ] [--no-color] [--no-update] [--notify ] [--path ] [--reset | ] [--skip-dependencies-installation] [-s ] [--subscription-product-url ] [-t ] [--theme-app-extension-port ] [--use-localhost | [--tunnel-url | ]] [--verbose] @@ -224,6 +224,8 @@ FLAGS --checkout-cart-url= Resource URL for checkout UI extension. Format: "/cart/{productVariantID}:{productQuantity}" --client-id= The Client ID of your app. + --host= [default: localhost] Set which network interface the web server listens on. + The default value is localhost. --localhost-port= Port to use for localhost. --no-color Disable color output. --no-update Skips the Partners Dashboard URL update step. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index d2a5ab041de..971f4be6ace 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -459,6 +459,15 @@ "name": "graphiql-port", "type": "option" }, + "host": { + "default": "localhost", + "description": "Set which network interface the web server listens on. The default value is localhost.", + "env": "SHOPIFY_FLAG_HOST", + "hasDynamicHelp": false, + "multiple": false, + "name": "host", + "type": "option" + }, "localhost-port": { "description": "Port to use for localhost.", "env": "SHOPIFY_FLAG_LOCALHOST_PORT",