diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index d6882fba5b4..8f332df0300 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -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", @@ -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", diff --git a/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts b/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts index ed940362975..dc279d48a68 100644 --- a/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts +++ b/packages/store/src/cli/api/graphql/business-platform-organizations/generated/create_app_development_store.ts @@ -8,6 +8,7 @@ export type CreateAppDevelopmentStoreMutationVariables = Types.Exact<{ priceLookupKey: Types.Scalars['String']['input'] prepopulateTestData?: Types.InputMaybe developerPreviewHandle?: Types.InputMaybe + country?: Types.InputMaybe }> export type CreateAppDevelopmentStoreMutation = { @@ -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', @@ -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', diff --git a/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql b/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql index 4c2b7277baf..b6da18eb81e 100644 --- a/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql +++ b/packages/store/src/cli/api/graphql/business-platform-organizations/mutations/create_app_development_store.graphql @@ -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) { createAppDevelopmentStore( shopName: $shopName priceLookupKey: $priceLookupKey prepopulateTestData: $prepopulateTestData developerPreviewHandle: $developerPreviewHandle + country: $country ) { shopAdminUrl shopDomain diff --git a/packages/store/src/cli/commands/store/create/dev.test.ts b/packages/store/src/cli/commands/store/create/dev.test.ts index 28916e4c998..5e77f815bcc 100644 --- a/packages/store/src/cli/commands/store/create/dev.test.ts +++ b/packages/store/src/cli/commands/store/create/dev.test.ts @@ -41,6 +41,7 @@ describe('store create dev command', () => { plan: 'plus', featurePreview: undefined, withDemoData: false, + country: undefined, json: false, }) }) @@ -54,6 +55,7 @@ describe('store create dev command', () => { plan: 'plus', featurePreview: undefined, withDemoData: false, + country: undefined, json: true, }) }) @@ -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') @@ -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() }) diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index 3ba6dfbda3e..6b651fa6f77 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -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' @@ -40,6 +40,7 @@ export default class StoreCreateDev extends Command { default: false, env: 'SHOPIFY_FLAG_STORE_WITH_DEMO_DATA', }), + country: countryFlag, } async run(): Promise { @@ -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) { diff --git a/packages/store/src/cli/commands/store/create/preview.ts b/packages/store/src/cli/commands/store/create/preview.ts index f49c1ae4e2f..34c3ab927d3 100644 --- a/packages/store/src/cli/commands/store/create/preview.ts +++ b/packages/store/src/cli/commands/store/create/preview.ts @@ -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' @@ -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 { 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({ title: outputContent`Creating store`, task: async () => createPreviewStoreCommand({name: flags.name, country: flags.country}), diff --git a/packages/store/src/cli/flags.ts b/packages/store/src/cli/flags.ts index 49ce9304c0c..0529b035cf8 100644 --- a/packages/store/src/cli/flags.ts +++ b/packages/store/src/cli/flags.ts @@ -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, + parse: async (value) => { + const normalized = value.trim().toUpperCase() + if (!isCountryCode(normalized)) { + throw new AbortError(invalidCountryCodeMessage) + } + return normalized + }, +}) export const storeFlags = { store: Flags.string({ diff --git a/packages/store/src/cli/services/store/create/dev.test.ts b/packages/store/src/cli/services/store/create/dev.test.ts index c94971468dd..750c602a0c3 100644 --- a/packages/store/src/cli/services/store/create/dev.test.ts +++ b/packages/store/src/cli/services/store/create/dev.test.ts @@ -71,6 +71,7 @@ describe('createDevStore', () => { priceLookupKey: 'SHOPIFY_PLUS_APP_DEVELOPMENT', prepopulateTestData: false, developerPreviewHandle: undefined, + country: undefined, }, }), ) @@ -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'}), + }), + ) + }) + + 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) diff --git a/packages/store/src/cli/services/store/create/dev.ts b/packages/store/src/cli/services/store/create/dev.ts index 9929f0fcd9e..e92113204d6 100644 --- a/packages/store/src/cli/services/store/create/dev.ts +++ b/packages/store/src/cli/services/store/create/dev.ts @@ -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' @@ -22,6 +22,7 @@ interface CreateDevStoreOptions { organization: Organization featurePreview?: string withDemoData?: boolean + country?: string json: boolean } @@ -64,6 +65,7 @@ export async function createDevStore(options: CreateDevStoreOptions): Promise