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/create-dev-store-from-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': minor
---

Allow `app dev` to create a development store when the organization has none.
1 change: 1 addition & 0 deletions bin/get-graphql-schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const schemas = [
pathToFile: 'areas/platforms/organizations/db/graphql/organizations_schema.graphql',
localPaths: [
'./packages/app/src/cli/api/graphql/business-platform-organizations/organizations_schema.graphql',
'./packages/organizations/src/cli/api/graphql/business-platform-organizations/organizations_schema.graphql',
'./packages/store/src/cli/api/graphql/business-platform-organizations/organizations_schema.graphql',
],
},
Expand Down
5 changes: 5 additions & 0 deletions graphql.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export default {
functions: projectFactory('functions', 'functions_cli_schema.graphql', 'app'),
adminAsApp: projectFactory('admin', 'admin_schema.graphql'),
organizationsDestinations: projectFactory('business-platform-destinations', 'destinations_schema.graphql', 'organizations'),
organizationsBusinessPlatformOrganizations: projectFactory(
'business-platform-organizations',
'organizations_schema.graphql',
'organizations',
),
storeBusinessPlatformDestinations: projectFactory('business-platform-destinations', 'destinations_schema.graphql', 'store'),
storeBusinessPlatformOrganizations: projectFactory('business-platform-organizations', 'organizations_schema.graphql', 'store'),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type DevStoreCapReachedQueryVariables = Types.Exact<{[key: string]: never}>

export type DevStoreCapReachedQuery = {organization?: {devStoreCapReached: boolean} | null}

export const DevStoreCapReached = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: {kind: 'Name', value: 'DevStoreCapReached'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'organization'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'devStoreCapReached'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
],
},
},
],
} as unknown as DocumentNode<DevStoreCapReachedQuery, DevStoreCapReachedQueryVariables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query DevStoreCapReached {
organization {
devStoreCapReached
}
}
1 change: 1 addition & 0 deletions packages/app/src/cli/commands/app/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('app dev command', () => {
tunnelUrl: undefined,
localhostPort: undefined,
})
expect(storeContext).toHaveBeenCalledWith(expect.objectContaining({storeCreationMode: 'when-empty'}))
expect(dev).toHaveBeenCalledWith(expect.objectContaining({installMkcert: false, tunnel: {mode: 'auto'}}))
})
})
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/commands/app/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export default class Dev extends AppLinkedCommand {
appContextResult,
storeFqdn: flags.store,
forceReselectStore: flags.reset,
storeCreationMode: 'when-empty',
})

const devOptions: DevOptions = {
Expand Down
30 changes: 30 additions & 0 deletions packages/app/src/cli/prompts/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,36 @@ describe('selectStore', () => {
expect(outputMock.output()).toMatch('Using your default dev store, store1, to preview your project')
})

test('creates directly when the list is empty and a creation handler is provided', async () => {
const onCreateStoreWhenEmpty = vi.fn().mockResolvedValue(STORE1)

const got = await selectStorePrompt({
stores: [],
showDomainOnPrompt: defaultShowDomainOnPrompt,
onCreateStoreWhenEmpty,
})

expect(got).toEqual(STORE1)
expect(onCreateStoreWhenEmpty).toHaveBeenCalledOnce()
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
})

test('returns the only store without creating when a creation handler is provided', async () => {
const onCreateStoreWhenEmpty = vi.fn().mockResolvedValue(STORE2)
const outputMock = mockAndCaptureOutput()

const got = await selectStorePrompt({
stores: [STORE1],
showDomainOnPrompt: defaultShowDomainOnPrompt,
onCreateStoreWhenEmpty,
})

expect(got).toEqual(STORE1)
expect(onCreateStoreWhenEmpty).not.toHaveBeenCalled()
expect(renderAutocompletePrompt).not.toBeCalled()
expect(outputMock.output()).toMatch('Using your default dev store, store1, to preview your project')
})

test('returns store if user selects one', async () => {
// Given
const stores: OrganizationStore[] = [STORE1, STORE2]
Expand Down
17 changes: 16 additions & 1 deletion packages/app/src/cli/prompts/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@ import {getTomls} from '../utilities/app/config/getTomls.js'
import {Paginateable} from '../utilities/developer-platform-client.js'
import {APP_NAME_MAX_LENGTH} from '../models/app/validation/common.js'
import {ApplicationURLs} from '../services/dev/urls.js'
import {
devStoreNamePrompt as sharedDevStoreNamePrompt,
devStorePlanPrompt as sharedDevStorePlanPrompt,
} from '@shopify/organizations'
import {
RenderAutocompleteOptions,
renderAutocompletePrompt,
renderConfirmationPrompt,
renderTextPrompt,
} from '@shopify/cli-kit/node/ui'
import {outputCompleted} from '@shopify/cli-kit/node/output'
import type {DevStorePlan} from '@shopify/organizations'

export function devStoreNamePrompt(): Promise<string> {
return sharedDevStoreNamePrompt()
}

export function devStorePlanPrompt(): Promise<DevStorePlan> {
return sharedDevStorePlanPrompt()
}

export async function selectAppPrompt(
onSearchForAppsByName: (term: string) => Promise<{apps: MinimalOrganizationApp[]; hasMorePages: boolean}>,
Expand Down Expand Up @@ -56,6 +69,7 @@ interface SelectStorePromptOptions {
stores: OrganizationStore[]
hasMorePages?: boolean
showDomainOnPrompt: boolean
onCreateStoreWhenEmpty?: () => Promise<OrganizationStore | undefined>
}

interface ExtraAutoCompletePropsForStoreSelect {
Expand All @@ -67,8 +81,9 @@ export async function selectStorePrompt({
hasMorePages = false,
onSearchForStoresByName,
showDomainOnPrompt = true,
onCreateStoreWhenEmpty,
}: SelectStorePromptOptions): Promise<OrganizationStore | undefined> {
if (stores.length === 0) return undefined
if (stores.length === 0) return onCreateStoreWhenEmpty?.()
if (stores.length === 1) {
outputCompleted(`Using your default dev store, ${stores[0]!.shopName}, to preview your project.`)
return stores[0]
Expand Down
51 changes: 51 additions & 0 deletions packages/app/src/cli/services/dev/cap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {devStoreCapReached} from './cap.js'
import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js'
import {ClientName, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {describe, expect, test, vi} from 'vitest'

describe('devStoreCapReached', () => {
test('returns the cap value for an app-management client', async () => {
const client = testDeveloperPlatformClient({
clientName: ClientName.AppManagement,
devStoreCapReached: vi.fn().mockResolvedValue(true),
})

await expect(devStoreCapReached('1', client)).resolves.toBe(true)
expect(client.devStoreCapReached).toHaveBeenCalledWith('1')
})

test('keeps the client receiver when the cap query uses this', async () => {
const client = testDeveloperPlatformClient({clientName: ClientName.AppManagement})
client.devStoreCapReached = async function (this: DeveloperPlatformClient) {
return this.clientName === ClientName.AppManagement
}

await expect(devStoreCapReached('1', client)).resolves.toBe(true)
})

test('fails open when the cap request fails', async () => {
const client = testDeveloperPlatformClient({
clientName: ClientName.AppManagement,
devStoreCapReached: vi.fn().mockRejectedValue(new Error('field is unavailable')),
})

await expect(devStoreCapReached('1', client)).resolves.toBe(false)
})

test('fails open when the app-management client does not expose the cap query', async () => {
const client = testDeveloperPlatformClient({clientName: ClientName.AppManagement})

expect(client.devStoreCapReached).toBeUndefined()
await expect(devStoreCapReached('1', client)).resolves.toBe(false)
})

test('does not query Partners clients', async () => {
const client = testDeveloperPlatformClient({
clientName: ClientName.Partners,
devStoreCapReached: vi.fn().mockResolvedValue(true),
})

await expect(devStoreCapReached('1', client)).resolves.toBe(false)
expect(client.devStoreCapReached).not.toHaveBeenCalled()
})
})
17 changes: 17 additions & 0 deletions packages/app/src/cli/services/dev/cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {ClientName, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'

export async function devStoreCapReached(
organizationId: string,
developerPlatformClient: DeveloperPlatformClient,
): Promise<boolean> {
if (developerPlatformClient.clientName !== ClientName.AppManagement || !developerPlatformClient.devStoreCapReached) {
return false
}

try {
return await developerPlatformClient.devStoreCapReached(organizationId)
// eslint-disable-next-line no-catch-all/no-catch-all
} catch {
return false
}
}
Loading
Loading