Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/orange-boxes-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Add --host flag to shopify app dev command for Docker container development
6 changes: 6 additions & 0 deletions docs-shopify.dev/commands/interfaces/app-dev.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export interface appdev {
*/
'-c, --config <value>'?: string

/**
* Set which network interface the web server listens on. The default value is 127.0.0.1.
* @environment SHOPIFY_FLAG_HOST
*/
'--host <value>'?: string

/**
* Port to use for localhost.
* @environment SHOPIFY_FLAG_LOCALHOST_PORT
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/cli/commands/app/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 127.0.0.1.',
env: 'SHOPIFY_FLAG_HOST',
default: '127.0.0.1',
}),
'localhost-port': Flags.integer({
description: 'Port to use for localhost.',
env: 'SHOPIFY_FLAG_LOCALHOST_PORT',
Expand Down Expand Up @@ -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,
}

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface DevOptions {
notify?: string
graphiqlPort?: number
graphiqlKey?: string
host: string
}

export async function dev(commandOptions: DevOptions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -95,6 +95,7 @@ describe('setup-dev-processes', () => {
commandConfig: new Config({root: ''}),
skipDependenciesInstallation: false,
tunnel: {mode: 'auto'},
host: '127.0.0.1',
}
const network: DevConfig['network'] = {
proxyUrl: 'https://example.com/proxy',
Expand Down Expand Up @@ -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: '127.0.0.1',
rules: {
'/extensions': `http://localhost:${previewExtensionPort}`,
'/ping': `http://localhost:${hmrPort}`,
Expand All @@ -298,6 +300,79 @@ 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'
Expand All @@ -311,6 +386,7 @@ describe('setup-dev-processes', () => {
commandConfig: new Config({root: ''}),
skipDependenciesInstallation: false,
tunnel: {mode: 'auto'},
host: '127.0.0.1',
}
const network: DevConfig['network'] = {
proxyUrl: 'https://example.com/proxy',
Expand All @@ -322,7 +398,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'] = {
Expand Down Expand Up @@ -384,6 +460,7 @@ describe('setup-dev-processes', () => {
commandConfig: new Config({root: ''}),
skipDependenciesInstallation: false,
tunnel: {mode: 'auto'},
host: '127.0.0.1',
}
const network: DevConfig['network'] = {
proxyUrl: 'https://example.com/proxy',
Expand Down Expand Up @@ -480,6 +557,7 @@ describe('setup-dev-processes', () => {
commandConfig: new Config({root: ''}),
skipDependenciesInstallation: false,
tunnel: {mode: 'auto'},
host: '127.0.0.1',
}
const network: DevConfig['network'] = {
proxyUrl: 'https://example.com/proxy',
Expand Down Expand Up @@ -566,6 +644,7 @@ describe('setup-dev-processes', () => {
commandConfig: new Config({root: ''}),
skipDependenciesInstallation: false,
tunnel: {mode: 'auto'},
host: '127.0.0.1',
}
const network: DevConfig['network'] = {
proxyUrl: 'https://example.com/proxy',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface ProxyServerProcess
port: number
rules: {[key: string]: string}
localhostCert?: LocalhostCert
host: string
}> {
type: 'proxy-server'
}
Expand Down Expand Up @@ -201,7 +202,12 @@ 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,
Expand All @@ -218,7 +224,8 @@ const stripUndefineds = <T>(process: T | undefined | false): process is T => {
async function setPortsAndAddProxyProcess(
processes: DevProcesses,
proxyPort: number,
reverseProxyCert?: LocalhostCert,
reverseProxyCert: LocalhostCert | undefined,
commandOptions: DevOptions,
): Promise<DevProcesses> {
// Convert processes that use proxying to have a port number and register their mapping rules
const processesAndRules = await Promise.all(
Expand Down Expand Up @@ -251,27 +258,29 @@ async function setPortsAndAddProxyProcess(
newProcesses.push({
type: 'proxy-server',
prefix: 'proxy',
function: startProxyServer,
function: proxyService,
options: {
port: proxyPort,
rules: allRules,
localhostCert: reverseProxyCert,
host: commandOptions.host,
},
})
}

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)
}
6 changes: 4 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ Run the app.

```
USAGE
$ shopify app dev [--checkout-cart-url <value>] [--client-id <value> | -c <value>] [--localhost-port
<value>] [--no-color] [--no-update] [--notify <value>] [--path <value>] [--reset | ]
$ shopify app dev [--checkout-cart-url <value>] [--client-id <value> | -c <value>] [--host <value>]
[--localhost-port <value>] [--no-color] [--no-update] [--notify <value>] [--path <value>] [--reset | ]
[--skip-dependencies-installation] [-s <value>] [--subscription-product-url <value>] [-t <value>]
[--theme-app-extension-port <value>] [--use-localhost | [--tunnel-url <value> | ]] [--verbose]

Expand All @@ -224,6 +224,8 @@ FLAGS
--checkout-cart-url=<value> Resource URL for checkout UI extension. Format:
"/cart/{productVariantID}:{productQuantity}"
--client-id=<value> The Client ID of your app.
--host=<value> [default: 127.0.0.1] Set which network interface the web server listens on.
The default value is 127.0.0.1.
--localhost-port=<value> Port to use for localhost.
--no-color Disable color output.
--no-update Skips the Partners Dashboard URL update step.
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,15 @@
"name": "graphiql-port",
"type": "option"
},
"host": {
"default": "127.0.0.1",
"description": "Set which network interface the web server listens on. The default value is 127.0.0.1.",
"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",
Expand Down