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
7 changes: 0 additions & 7 deletions .changeset/clickable-options-and-tabs.md

This file was deleted.

7 changes: 7 additions & 0 deletions .changeset/filter-app-dev-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@shopify/app': minor
'@shopify/cli': minor
'@shopify/cli-kit': minor
---

Full-screen layout for `shopify app dev`, including a log filter.
10 changes: 9 additions & 1 deletion packages/app/src/cli/commands/app/dev.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Dev from './dev.js'
import {dev} from '../../services/dev.js'
import {dev, type DevOptions} from '../../services/dev.js'
import {linkedAppContext} from '../../services/app-context.js'
import {storeContext} from '../../services/store-context.js'
import {getTunnelMode} from '../../services/dev/tunnel-mode.js'
Expand All @@ -24,13 +24,20 @@ vi.mock('../../models/app/loader.js')
vi.mock('@shopify/cli-kit/node/metadata')

describe('app dev command', () => {
let resolvedDevOptions: DevOptions | undefined

beforeEach(() => {
vi.mocked(dev).mockReset()
vi.mocked(dev).mockImplementation(async (commandOptions) => {
resolvedDevOptions = commandOptions
return resolvedDevOptions.app
})
vi.mocked(linkedAppContext).mockReset()
vi.mocked(storeContext).mockReset()
vi.mocked(getTunnelMode).mockReset()
vi.mocked(checkFolderIsValidApp).mockReset()
vi.mocked(addPublicMetadata).mockReset()
resolvedDevOptions = undefined
})

test('does not require --use-localhost when --install-mkcert is not passed', async () => {
Expand Down Expand Up @@ -59,6 +66,7 @@ describe('app dev command', () => {
localhostPort: undefined,
})
expect(dev).toHaveBeenCalledWith(expect.objectContaining({installMkcert: false, tunnel: {mode: 'auto'}}))
expect(resolvedDevOptions).toEqual(expect.objectContaining({installMkcert: false, tunnel: {mode: 'auto'}}))
})
})

Expand Down
90 changes: 47 additions & 43 deletions packages/app/src/cli/commands/app/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,54 +108,58 @@ export default class Dev extends AppLinkedCommand {
public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(Dev)

const tunnelMode = await getTunnelMode({
useLocalhost: flags['use-localhost'] ?? false,
tunnelUrl: flags['tunnel-url'],
localhostPort: flags['localhost-port'],
})
const prepareDevOptions = async () => {
const tunnelMode = await getTunnelMode({
useLocalhost: flags['use-localhost'] ?? false,
tunnelUrl: flags['tunnel-url'],
localhostPort: flags['localhost-port'],
})

await addPublicMetadata(() => {
return {
cmd_app_dependency_installation_skipped: flags['skip-dependencies-installation'],
cmd_app_reset_used: flags.reset,
cmd_dev_tunnel_type: tunnelMode.mode,
}
})
await addPublicMetadata(() => {
return {
cmd_app_dependency_installation_skipped: flags['skip-dependencies-installation'],
cmd_app_reset_used: flags.reset,
cmd_dev_tunnel_type: tunnelMode.mode,
}
})

await checkFolderIsValidApp(flags.path)

await checkFolderIsValidApp(flags.path)
const appContextResult = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})
const store = await storeContext({
appContextResult,
storeFqdn: flags.store,
forceReselectStore: flags.reset,
})

const appContextResult = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})
const store = await storeContext({
appContextResult,
storeFqdn: flags.store,
forceReselectStore: flags.reset,
})
const devOptions: DevOptions = {
...appContextResult,
store,
directory: flags.path,
update: !flags['no-update'],
skipDependenciesInstallation: flags['skip-dependencies-installation'],
commandConfig: this.config,
subscriptionProductUrl: flags['subscription-product-url'],
checkoutCartUrl: flags['checkout-cart-url'],
theme: flags.theme,
themeExtensionPort: flags['theme-app-extension-port'],
storePassword: flags['store-password'],
notify: flags.notify,
graphiqlPort: flags['graphiql-port'],
graphiqlKey: flags['graphiql-key'],
installMkcert: flags['install-mkcert'] ?? false,
tunnel: tunnelMode,
}

const devOptions: DevOptions = {
...appContextResult,
store,
directory: flags.path,
update: !flags['no-update'],
skipDependenciesInstallation: flags['skip-dependencies-installation'],
commandConfig: this.config,
subscriptionProductUrl: flags['subscription-product-url'],
checkoutCartUrl: flags['checkout-cart-url'],
theme: flags.theme,
themeExtensionPort: flags['theme-app-extension-port'],
storePassword: flags['store-password'],
notify: flags.notify,
graphiqlPort: flags['graphiql-port'],
graphiqlKey: flags['graphiql-key'],
installMkcert: flags['install-mkcert'] ?? false,
tunnel: tunnelMode,
return devOptions
}

await dev(devOptions)
return {app: appContextResult.app}
const app = await dev(await prepareDevOptions())
return {app}
}
}
85 changes: 84 additions & 1 deletion packages/app/src/cli/services/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {dev, blockIfMigrationIncomplete} from './dev.js'
import {setupDevProcesses} from './dev/processes/setup-dev-processes.js'
import {renderDev} from './dev/ui.js'
import {fetchAppRemoteConfiguration} from './app/select-app.js'
import {installAppDependencies} from './dependencies.js'
import {
testAppLinked,
testDeveloperPlatformClient,
Expand All @@ -11,20 +12,30 @@ import {
testProject,
} from '../models/app/app.test-data.js'
import metadata from '../metadata.js'
import {describe, expect, test, vi} from 'vitest'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {hashString} from '@shopify/cli-kit/node/crypto'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system'
import {renderSingleTask} from '@shopify/cli-kit/node/ui'

vi.mock('./dev/fetch.js')
vi.mock('./dev/processes/setup-dev-processes.js')
vi.mock('./dev/ui.js')
vi.mock('./app/select-app.js')
vi.mock('./dependencies.js')
vi.mock('@shopify/cli-kit/node/analytics')
vi.mock('@shopify/cli-kit/node/tcp')
vi.mock('../utilities/mkcert.js')
vi.mock('@shopify/cli-kit/node/system')
vi.mock('@shopify/cli-kit/node/ui')

describe('dev', () => {
beforeEach(() => {
vi.mocked(terminalSupportsPrompting).mockReturnValue(false)
vi.mocked(renderSingleTask).mockImplementation(async ({task}) => task(() => {}))
})

test('logs store domain metadata when launching dev', async () => {
const store = testOrganizationStore({shopDomain: 'dev-store.myshopify.com'})
const app = testAppLinked()
Expand Down Expand Up @@ -80,10 +91,82 @@ describe('dev', () => {
}),
)
expect(reportAnalyticsEvent).toHaveBeenCalledWith({config: {}, exitMode: 'ok'})
expect(renderSingleTask).not.toHaveBeenCalled()

addPublicMetadata.mockRestore()
addSensitiveMetadata.mockRestore()
})

test('renders the interactive layout before installing dependencies', async () => {
vi.mocked(terminalSupportsPrompting).mockReturnValue(true)
const app = testAppLinked()
const store = testOrganizationStore({shopDomain: 'dev-store.myshopify.com'})
let loadingIndicatorStarted = false
let loadingIndicatorFinished = false
let dependencyInstallationStarted = () => {}
const dependencyInstallationStart = new Promise<void>((resolve) => {
dependencyInstallationStarted = resolve
})
let finishDependencyInstallation = () => {}
const dependencyInstallationFinished = new Promise<void>((resolve) => {
finishDependencyInstallation = resolve
})

vi.mocked(fetchAppRemoteConfiguration).mockImplementation(async () => {
expect(loadingIndicatorStarted).toBe(true)
return {name: 'Remote app', application_url: '', embedded: true}
})
vi.mocked(getAvailableTCPPort).mockResolvedValue(3456)
vi.mocked(checkPortAvailability).mockResolvedValue(true)
vi.mocked(renderSingleTask).mockImplementation(async ({task}) => {
loadingIndicatorStarted = true
const result = await task(() => {})
loadingIndicatorFinished = true
return result
})
vi.mocked(installAppDependencies).mockImplementation(async () => {
dependencyInstallationStarted()
await dependencyInstallationFinished
})
vi.mocked(setupDevProcesses).mockResolvedValue({
processes: [],
previewUrl: 'https://dev-store.myshopify.com/admin/apps/api-key',
graphiqlUrl: undefined,
devSessionStatusManager: {} as any,
})
vi.mocked(renderDev).mockImplementation(async ({processes, abortController}) => {
expect(loadingIndicatorFinished).toBe(true)
await processes[0]?.action(process.stdout, process.stderr, abortController.signal)
})

const devPromise = dev({
app,
project: testProject({usesWorkspaces: false}),
remoteApp: testOrganizationApp({apiKey: 'api-key'}),
organization: testOrganization(),
specifications: [],
developerPlatformClient: testDeveloperPlatformClient(),
store,
directory: app.directory,
update: false,
commandConfig: {} as any,
skipDependenciesInstallation: false,
tunnel: {mode: 'custom', url: 'https://localhost:3456'},
})

await dependencyInstallationStart
expect(renderSingleTask).toHaveBeenCalledWith(
expect.objectContaining({title: expect.objectContaining({value: 'Starting dev preview'})}),
)
expect(renderSingleTask).toHaveBeenCalledOnce()
expect(renderDev).toHaveBeenCalledOnce()
expect(setupDevProcesses).not.toHaveBeenCalled()

finishDependencyInstallation()
await devPromise

expect(setupDevProcesses).toHaveBeenCalledOnce()
})
})

describe('blockIfMigrationIncomplete', () => {
Expand Down
Loading
Loading