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
11 changes: 10 additions & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6316,6 +6316,15 @@
"descriptionWithMarkdown": "Creates a new app development store in your organization.",
"enableJsonFlag": false,
"flags": {
"country": {
"description": "Two-letter country code for the store, such as US, CA, or GB.",
"env": "SHOPIFY_FLAG_STORE_COUNTRY",
"hasDynamicHelp": false,
"multiple": false,
"name": "country",
"required": false,
"type": "option"
},
"feature-preview": {
"description": "The handle of a feature preview to enable on the new development store.",
"env": "SHOPIFY_FLAG_STORE_FEATURE_PREVIEW",
Expand Down Expand Up @@ -6414,7 +6423,7 @@
"flags": {
"country": {
"description": "Two-letter country code for the store, such as US, CA, or GB.",
"env": "SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY",
"env": "SHOPIFY_FLAG_STORE_COUNTRY",
"hasDynamicHelp": false,
"multiple": false,
"name": "country",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type CreateAppDevelopmentStoreMutationVariables = Types.Exact<{
priceLookupKey: Types.Scalars['String']['input']
prepopulateTestData?: Types.InputMaybe<Types.Scalars['Boolean']['input']>
developerPreviewHandle?: Types.InputMaybe<Types.Scalars['String']['input']>
country?: Types.InputMaybe<Types.Scalars['String']['input']>
}>

export type CreateAppDevelopmentStoreMutation = {
Expand Down Expand Up @@ -46,6 +47,11 @@ export const CreateAppDevelopmentStore = {
variable: {kind: 'Variable', name: {kind: 'Name', value: 'developerPreviewHandle'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'country'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
],
selectionSet: {
kind: 'SelectionSet',
Expand Down Expand Up @@ -74,6 +80,11 @@ export const CreateAppDevelopmentStore = {
name: {kind: 'Name', value: 'developerPreviewHandle'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'developerPreviewHandle'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'country'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'country'}},
},
],
selectionSet: {
kind: 'SelectionSet',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
mutation CreateAppDevelopmentStore($shopName: String!, $priceLookupKey: String!, $prepopulateTestData: Boolean, $developerPreviewHandle: String) {
mutation CreateAppDevelopmentStore($shopName: String!, $priceLookupKey: String!, $prepopulateTestData: Boolean, $developerPreviewHandle: String, $country: String) {
Comment thread
amcaplan marked this conversation as resolved.
createAppDevelopmentStore(
shopName: $shopName
priceLookupKey: $priceLookupKey
prepopulateTestData: $prepopulateTestData
developerPreviewHandle: $developerPreviewHandle
country: $country
) {
shopAdminUrl
shopDomain
Expand Down
41 changes: 41 additions & 0 deletions packages/store/src/cli/commands/store/create/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('store create dev command', () => {
plan: 'plus',
featurePreview: undefined,
withDemoData: false,
country: undefined,
json: false,
})
})
Expand All @@ -54,6 +55,7 @@ describe('store create dev command', () => {
plan: 'plus',
featurePreview: undefined,
withDemoData: false,
country: undefined,
json: true,
})
})
Expand All @@ -77,10 +79,48 @@ describe('store create dev command', () => {
plan: 'basic',
featurePreview: 'extended_variants',
withDemoData: true,
country: undefined,
json: false,
})
})

test('normalizes and passes the --country flag through to the service', async () => {
await StoreCreateDev.run([
'--name',
'my-test-store',
'--plan',
'plus',
'--organization-id',
'12345',
'--country',
'ca',
])

expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({country: 'CA'}))
})

test('rejects an invalid --country value without calling the service', async () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit')
}) as never)

await expect(
StoreCreateDev.run([
'--name',
'my-test-store',
'--plan',
'plus',
'--organization-id',
'12345',
'--country',
'USA',
]),
).rejects.toThrow()
expect(createDevStore).not.toHaveBeenCalled()

mockExit.mockRestore()
})

test('prompts for the name when --name is omitted in an interactive environment', async () => {
vi.mocked(storeNamePrompt).mockResolvedValue('prompted-store')

Expand Down Expand Up @@ -141,6 +181,7 @@ describe('store create dev command', () => {
expect(StoreCreateDev.flags.plan).toBeDefined()
expect(StoreCreateDev.flags['feature-preview']).toBeDefined()
expect(StoreCreateDev.flags['with-demo-data']).toBeDefined()
expect(StoreCreateDev.flags.country).toBeDefined()
expect(StoreCreateDev.flags.json).toBeDefined()
})

Expand Down
4 changes: 3 additions & 1 deletion packages/store/src/cli/commands/store/create/dev.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createDevStore} from '../../../services/store/create/dev.js'
import {devStorePlanHandles, DevStorePlan} from '../../../services/store/constants.js'
import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js'
import {storeFlags} from '../../../flags.js'
import {countryFlag, storeFlags} from '../../../flags.js'
import {selectOrg} from '@shopify/organizations'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
Expand Down Expand Up @@ -40,6 +40,7 @@ export default class StoreCreateDev extends Command {
default: false,
env: 'SHOPIFY_FLAG_STORE_WITH_DEMO_DATA',
}),
country: countryFlag,
}

async run(): Promise<void> {
Expand All @@ -57,6 +58,7 @@ export default class StoreCreateDev extends Command {
plan,
featurePreview: flags['feature-preview'],
withDemoData: flags['with-demo-data'],
country: flags.country,
json: flags.json,
})
} catch (error) {
Expand Down
8 changes: 2 additions & 6 deletions packages/store/src/cli/commands/store/create/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {isCountryCode, previewStoreFlags} from '../../../flags.js'
import {countryFlag} from '../../../flags.js'
import {type CreatePreviewStoreResult, createPreviewStoreCommand} from '../../../services/store/create/preview/index.js'
import {writeCreatePreviewStoreResult} from '../../../services/store/create/preview/result.js'
import StoreCommand from '../../../utilities/store-command.js'
Expand Down Expand Up @@ -30,16 +30,12 @@ export default class StoreCreatePreview extends StoreCommand {
env: 'SHOPIFY_FLAG_PREVIEW_STORE_NAME',
required: false,
}),
country: previewStoreFlags.country,
country: countryFlag,
}

public async run(): Promise<void> {
const {flags} = await this.parse(StoreCreatePreview)

if (flags.country !== undefined && !isCountryCode(flags.country)) {
this.error('Country must be a two-letter country code, for example: US.')
}

const result = await renderSingleTask<CreatePreviewStoreResult>({
title: outputContent`Creating store`,
task: async () => createPreviewStoreCommand({name: flags.name, country: flags.country}),
Expand Down
35 changes: 23 additions & 12 deletions packages/store/src/cli/flags.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {normalizeBulkOperationId} from '@shopify/cli-kit/node/api/bulk-operations'
import {resolvePath} from '@shopify/cli-kit/node/path'
import {AbortError} from '@shopify/cli-kit/node/error'
import {Flags} from '@oclif/core'

function countryFlag(env: string) {
return Flags.string({
description: 'Two-letter country code for the store, such as US, CA, or GB.',
env,
required: false,
parse: async (value) => value.trim().toUpperCase(),
})
}
// Error message shown when a `--country` flag value is not a two-letter code.
const invalidCountryCodeMessage = 'Country must be a two-letter country code, for example: US.'

export function isCountryCode(value: string): boolean {
// Matches a two-letter (ISO 3166-1 alpha-2 shaped) country code after normalization.
function isCountryCode(value: string): boolean {
return /^[A-Z]{2}$/.test(value)
}

export const previewStoreFlags = {
country: countryFlag('SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY'),
}
/**
* Reusable `--country` flag shared by every store-creation command. The value
* is normalized to an uppercase, trimmed string and validated during parsing,
* so invalid codes are rejected with the same error before a command's `run`
* body executes.
*/
export const countryFlag = Flags.string({
description: 'Two-letter country code for the store, such as US, CA, or GB.',
env: 'SHOPIFY_FLAG_STORE_COUNTRY',
required: false,
Comment on lines +21 to +24

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should migrate. This probably isn't a significant enough change and create preview store was just introduced, I'm not super worried.

parse: async (value) => {
const normalized = value.trim().toUpperCase()
if (!isCountryCode(normalized)) {
throw new AbortError(invalidCountryCodeMessage)
}
return normalized
},
})

export const storeFlags = {
store: Flags.string({
Expand Down
79 changes: 79 additions & 0 deletions packages/store/src/cli/services/store/create/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe('createDevStore', () => {
priceLookupKey: 'SHOPIFY_PLUS_APP_DEVELOPMENT',
prepopulateTestData: false,
developerPreviewHandle: undefined,
country: undefined,
},
}),
)
Expand Down Expand Up @@ -161,6 +162,84 @@ describe('createDevStore', () => {
expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"demoData": true'))
})

test('passes the country to the mutation', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc)
.mockResolvedValueOnce(defaultMutationResult)
.mockResolvedValueOnce({
organization: {id: '123', storeCreation: {status: 'COMPLETE'}},
})

await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', country: 'CA', json: false})

expect(businessPlatformOrganizationsRequestDoc).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({country: 'CA'}),
Comment thread
amcaplan marked this conversation as resolved.
}),
)
})

test('includes the country in JSON output when provided', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc)
.mockResolvedValueOnce(defaultMutationResult)
.mockResolvedValueOnce({
organization: {id: '123', storeCreation: {status: 'COMPLETE'}},
})

await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'basic', country: 'CA', json: true})

expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"country": "CA"'))
})

test('includes the country in the success output when provided', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc)
.mockResolvedValueOnce(defaultMutationResult)
.mockResolvedValueOnce({
organization: {id: '123', storeCreation: {status: 'COMPLETE'}},
})

await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', country: 'CA', json: false})

expect(renderSuccess).toHaveBeenCalledWith(
expect.objectContaining({
customSections: [
expect.objectContaining({
body: expect.objectContaining({
tabularData: expect.arrayContaining([['Country', 'CA']]),
}),
}),
],
}),
)
})

test('renders Admin as N/A in the success output when the admin URL is missing', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc)
.mockResolvedValueOnce({
createAppDevelopmentStore: {
shopAdminUrl: null,
shopDomain: 'test-store.myshopify.com',
userErrors: [],
},
})
.mockResolvedValueOnce({
organization: {id: '123', storeCreation: {status: 'COMPLETE'}},
})

await createDevStore({name: 'test-store', organization: defaultOrg, plan: 'plus', json: false})

expect(renderSuccess).toHaveBeenCalledWith(
expect.objectContaining({
customSections: [
expect.objectContaining({
body: expect.objectContaining({
tabularData: expect.arrayContaining([['Admin', 'N/A']]),
}),
}),
],
}),
)
})

test('outputs JSON when --json flag is set', async () => {
vi.mocked(businessPlatformOrganizationsRequestDoc)
.mockResolvedValueOnce(defaultMutationResult)
Expand Down
29 changes: 21 additions & 8 deletions packages/store/src/cli/services/store/create/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import {Organization} from '@shopify/organizations'
import {businessPlatformOrganizationsRequestDoc} from '@shopify/cli-kit/node/api/business-platform'
import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session'
import {renderSingleTask, renderSuccess} from '@shopify/cli-kit/node/ui'
import {renderSingleTask, renderSuccess, type InlineToken} from '@shopify/cli-kit/node/ui'
import {outputContent, outputResult} from '@shopify/cli-kit/node/output'
import {AbortError} from '@shopify/cli-kit/node/error'
import {sleep} from '@shopify/cli-kit/node/system'
Expand All @@ -22,6 +22,7 @@ interface CreateDevStoreOptions {
organization: Organization
featurePreview?: string
withDemoData?: boolean
country?: string
json: boolean
}

Expand Down Expand Up @@ -64,6 +65,7 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise<vo
priceLookupKey: DEV_STORE_PLANS[plan],
prepopulateTestData: options.withDemoData ?? false,
developerPreviewHandle: options.featurePreview,
country: options.country,
},
unauthorizedHandler,
})
Expand Down Expand Up @@ -132,6 +134,7 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise<vo
adminUrl: shopAdminUrl,
plan,
...(options.featurePreview ? {featurePreview: options.featurePreview} : {}),
...(options.country ? {country: options.country} : {}),
demoData: options.withDemoData ?? false,
},
organization: {
Expand All @@ -144,15 +147,25 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise<vo
),
)
} else {
const rows: InlineToken[][] = []
pushRow(rows, 'Domain', shopDomain)
// Admin always renders, falling back to 'N/A' when the URL is missing, so the
// summary never silently drops this commonly expected field.
rows.push(['Admin', shopAdminUrl ? {link: {label: shopAdminUrl, url: shopAdminUrl}} : 'N/A'])
pushRow(rows, 'Plan', plan)
pushRow(rows, 'Feature preview', options.featurePreview)
pushRow(rows, 'Country', options.country)
pushRow(rows, 'Demo data', options.withDemoData ? 'enabled' : 'disabled')

renderSuccess({
headline: `Development store "${name}" created successfully.`,
body: [
`Domain: ${shopDomain}`,
`Admin: ${shopAdminUrl ?? 'N/A'}`,
`Plan: ${plan}`,
...(options.featurePreview ? [`Feature preview: ${options.featurePreview}`] : []),
`Demo data: ${options.withDemoData ? 'enabled' : 'disabled'}`,
],
customSections: [{body: {tabularData: rows, firstColumnSubdued: true}}],
})
}
}

function pushRow(rows: InlineToken[][], label: string, value: InlineToken | undefined): void {
if (value !== undefined && value !== null && value !== '') {
rows.push([label, value])
}
}
Loading